diff --git a/.gitignore b/.gitignore index 23105c4..44bf0bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ iap_curl +vendor diff --git a/Gopkg.lock b/Gopkg.lock index c3f2bb9..724852f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -7,39 +7,59 @@ revision = "2d3a6656c17a60b0815b7e06ab0be04eacb6e613" version = "v0.16.0" -[[projects]] - name = "github.com/agext/levenshtein" - packages = ["."] - revision = "5f10fee965225ac1eecdc234c09daf5cd9e7f7b6" - version = "v1.2.1" - [[projects]] branch = "master" name = "github.com/golang/protobuf" packages = ["proto"] revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845" +[[projects]] + branch = "master" + name = "github.com/mitchellh/go-homedir" + packages = ["."] + revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" + [[projects]] branch = "master" name = "golang.org/x/net" - packages = ["context","context/ctxhttp"] + packages = [ + "context", + "context/ctxhttp" + ] revision = "c7086645de248775cbf2373cf5ca4d2fa664b8c1" [[projects]] branch = "master" name = "golang.org/x/oauth2" - packages = [".","google","internal","jws","jwt"] + packages = [ + ".", + "google", + "internal", + "jws", + "jwt" + ] revision = "f95fa95eaa936d9d87489b15d1d18b97c1ba9c28" [[projects]] name = "google.golang.org/appengine" - packages = [".","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api","internal/urlfetch","urlfetch"] + packages = [ + ".", + "internal", + "internal/app_identity", + "internal/base", + "internal/datastore", + "internal/log", + "internal/modules", + "internal/remote_api", + "internal/urlfetch", + "urlfetch" + ] revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" version = "v1.0.0" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "716d347ac33eb70288a7be95096617af25bcfdcdb595a3ee216d2d644d497554" + inputs-digest = "74bdf21039b43c58251086b2713a1e8b7cd4ed59a2c88d6f0a210b2d6dd21c55" solver-name = "gps-cdcl" solver-version = 1 diff --git a/config.go b/config.go index 9d79da0..9e1e3fd 100644 --- a/config.go +++ b/config.go @@ -4,14 +4,19 @@ import ( "encoding/json" "fmt" "io/ioutil" - "math" neturl "net/url" "os" "os/exec" "path/filepath" "runtime" - "github.com/agext/levenshtein" + homedir "github.com/mitchellh/go-homedir" +) + +const ( + envCredentials = "GOOGLE_APPLICATION_CREDENTIALS" + envClientID = "IAP_CLIENT_ID" + envCurlCommand = "IAP_CURL_BIN" ) type Config struct { @@ -84,7 +89,7 @@ func (cfg *Config) LoadFile(file string) error { return json.NewEncoder(f).Encode(cfg) } -func (cfg *Config) GetEnv(url string) (env Env, err error) { +func (cfg *Config) getEnvFromFile(url string) (env Env, err error) { u1, _ := neturl.Parse(url) for _, service := range cfg.Services { u2, _ := neturl.Parse(service.URL) @@ -96,6 +101,36 @@ func (cfg *Config) GetEnv(url string) (env Env, err error) { return } +func (cfg *Config) GetEnv(url string) (env Env, err error) { + env, _ = cfg.getEnvFromFile(url) + credentials := os.Getenv(envCredentials) + clientID := os.Getenv(envClientID) + binary := os.Getenv(envCurlCommand) + if credentials == "" { + credentials, _ = homedir.Expand(env.Credentials) + } + if clientID == "" { + clientID = env.ClientID + } + if binary == "" { + binary = env.Binary + } + if credentials == "" { + return env, fmt.Errorf("%s is missing", envCredentials) + } + if clientID == "" { + return env, fmt.Errorf("%s is missing", envClientID) + } + if binary == "" { + binary = "curl" + } + return Env{ + Credentials: credentials, + ClientID: clientID, + Binary: binary, + }, nil +} + func (cfg *Config) GetURLs() (list []string) { for _, service := range cfg.Services { list = append(list, service.URL) @@ -122,19 +157,3 @@ func (cfg *Config) Edit() error { cmd.Stdin = os.Stdin return cmd.Run() } - -func (cfg *Config) SimilarURLs(url string) (urls []string) { - u1, _ := neturl.Parse(url) - for _, service := range cfg.Services { - u2, _ := neturl.Parse(service.URL) - degree := round(levenshtein.Similarity(u1.Host, u2.Host, nil) * 100) - if degree > 50 { - urls = append(urls, service.URL) - } - } - return -} - -func round(f float64) float64 { - return math.Floor(f + .5) -} diff --git a/curl.go b/curl.go deleted file mode 100644 index e16382a..0000000 --- a/curl.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "os" - "os/exec" - "runtime" -) - -func doCurl(binary string, args []string) error { - // Check if you have curl command - command := binary - if _, err := exec.LookPath(command); err != nil { - return err - } - for _, arg := range args { - command += " " + arg - } - var cmd *exec.Cmd - if runtime.GOOS == "windows" { - cmd = exec.Command("cmd", "/c", command) - } else { - cmd = exec.Command("sh", "-c", command) - } - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - cmd.Stdin = os.Stdin - return cmd.Run() -} diff --git a/iap.go b/iap.go index 14ba834..0c80eab 100644 --- a/iap.go +++ b/iap.go @@ -17,43 +17,33 @@ import ( "golang.org/x/oauth2/jws" ) -func readRsaPrivateKey(bytes []byte) (key *rsa.PrivateKey, err error) { - block, _ := pem.Decode(bytes) - if block == nil { - err = errors.New("invalid private key data") - return - } - - if block.Type == "RSA PRIVATE KEY" { - key, err = x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return - } - } else if block.Type == "PRIVATE KEY" { - keyInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - return nil, err - } - var ok bool - key, ok = keyInterface.(*rsa.PrivateKey) - if !ok { - return nil, errors.New("not RSA private key") - } - } else { - return nil, fmt.Errorf("invalid private key type: %s", block.Type) - } +const ( + // TokenURI is the base uri of google oauth API + TokenURI = "https://www.googleapis.com/oauth2/v4/token" +) - key.Precompute() +// IAP represents the information needed to access IAP-protected app +type IAP struct { + SA string + ID string +} - if err := key.Validate(); err != nil { - return nil, err +func newIAP(sa, id string) (*IAP, error) { + if sa == "" { + return &IAP{}, errors.New("Service Account is missing") } - - return + if id == "" { + return &IAP{}, errors.New("Client ID is missing") + } + return &IAP{ + SA: sa, + ID: id, + }, nil } -func getToken(saPath, clientID string) (token string, err error) { - sa, err := ioutil.ReadFile(saPath) +// GetToken returns JWT token for authz +func (c *IAP) GetToken() (token string, err error) { + sa, err := ioutil.ReadFile(c.SA) if err != nil { return } @@ -70,7 +60,7 @@ func getToken(saPath, clientID string) (token string, err error) { Iat: iat.Unix(), Exp: exp.Unix(), PrivateClaims: map[string]interface{}{ - "target_audience": clientID, + "target_audience": c.ID, }, } jwsHeader := &jws.Header{ @@ -111,3 +101,38 @@ func getToken(saPath, clientID string) (token string, err error) { token = tokenRes.IDToken return } + +func readRsaPrivateKey(bytes []byte) (key *rsa.PrivateKey, err error) { + block, _ := pem.Decode(bytes) + if block == nil { + err = errors.New("invalid private key data") + return + } + + if block.Type == "RSA PRIVATE KEY" { + key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return + } + } else if block.Type == "PRIVATE KEY" { + keyInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + var ok bool + key, ok = keyInterface.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("not RSA private key") + } + } else { + return nil, fmt.Errorf("invalid private key type: %s", block.Type) + } + + key.Precompute() + + if err := key.Validate(); err != nil { + return nil, err + } + + return +} diff --git a/main.go b/main.go index c6c561e..2e1d04b 100644 --- a/main.go +++ b/main.go @@ -1,123 +1,167 @@ package main import ( + "errors" "fmt" + "io" + "net/url" "os" + "os/exec" "path/filepath" + "runtime" "strings" - - homedir "github.com/mitchellh/go-homedir" ) const ( - TokenURI = "https://www.googleapis.com/oauth2/v4/token" - - GoogleApplicationCredentials = "GOOGLE_APPLICATION_CREDENTIALS" - IAPClientID = "IAP_CLIENT_ID" - IAPCurlBinary = "IAP_CURL_BIN" + app = "iap_curl" + version = "0.1.1" ) -const helpText string = `Usage: curl +// CLI represents the attributes for command-line interface +type CLI struct { + opt option + args []string + urls []url.URL + cfg Config -Extended options: - --list, --list-urls List service URLs - --edit, --edit-config Edit config file -` + stdout io.Writer + stderr io.Writer +} -var ( - credentials string - clientID string - binary string +type option struct { + list bool + edit bool - cfg Config -) + version bool +} func main() { + cli, err := newCLI(os.Args[1:]) + if err != nil { + panic(err) + } + os.Exit(cli.run()) +} + +func newCLI(args []string) (CLI, error) { + var c CLI + + // TODO: make it customizable + c.stdout = os.Stdout + c.stderr = os.Stderr + + for _, arg := range args { + switch arg { + case "--list", "--list-urls": + c.opt.list = true + case "--edit", "--edit-config": + c.opt.edit = true + case "--version": + c.opt.version = true + default: + u, err := url.Parse(arg) + if err == nil { + c.urls = append(c.urls, *u) + } else { + c.args = append(c.args, arg) + } + } + } + dir, _ := configDir() json := filepath.Join(dir, "config.json") - - if err := cfg.LoadFile(json); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) + if err := c.cfg.LoadFile(json); err != nil { + return c, err } - os.Exit(run(os.Args[1:])) + return c, nil } -func run(args []string) int { - if len(args) == 0 { - fmt.Fprintf(os.Stderr, "Error: too few arguments\n") - return 1 - } - switch args[0] { - case "-h", "--help": - fmt.Fprint(os.Stderr, helpText) - return 1 - case "--list-urls", "--list": - fmt.Println(strings.Join(cfg.GetURLs(), "\n")) +func (c CLI) exit(msg interface{}) int { + switch m := msg.(type) { + case string: + fmt.Fprintf(c.stdout, "%s\n", m) return 0 - case "--edit-config", "--edit": - if err := cfg.Edit(); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err.Error()) - return 1 - } + case error: + fmt.Fprintf(c.stderr, "[ERROR] %s: %s\n", app, m.Error()) + return 1 + case int: + return m + case nil: return 0 default: - // Ignore other arguments + panic(msg) } +} - // The last argument is regarded as an URL - url := args[len(args)-1] - env, err := cfg.GetEnv(url) - if err != nil { - similarURLs := cfg.SimilarURLs(url) - if len(similarURLs) > 0 { - fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) - fmt.Fprintf(os.Stderr, " similar urls found %q\n", similarURLs) - return 1 - } +func (c CLI) run() int { + if c.opt.version { + return c.exit(fmt.Sprintf("%s v%s (runtime: %s)", app, version, runtime.Version())) } - credentials = os.Getenv(GoogleApplicationCredentials) - clientID = os.Getenv(IAPClientID) - binary = os.Getenv(IAPCurlBinary) - if credentials == "" { - credentials, _ = homedir.Expand(env.Credentials) - } - if clientID == "" { - clientID = env.ClientID - } - if binary == "" { - binary = env.Binary + + if c.opt.list { + return c.exit(strings.Join(c.cfg.GetURLs(), "\n")) } - if credentials == "" { - fmt.Fprintf(os.Stderr, "Error: %s is missing\n", GoogleApplicationCredentials) - return 1 + if c.opt.edit { + return c.exit(c.cfg.Edit()) } - if clientID == "" { - fmt.Fprintf(os.Stderr, "Error: %s is missing\n", IAPClientID) - return 1 + + url := c.getURL() + if url == "" { + return c.exit(errors.New("invalid url or url not given")) } - if binary == "" { - binary = "curl" + + env, err := c.cfg.GetEnv(url) + if err != nil { + return c.exit(err) } - token, err := getToken(credentials, clientID) + iap, err := newIAP(env.Credentials, env.ClientID) if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err.Error()) - return 1 + return c.exit(err) + } + token, err := iap.GetToken() + if err != nil { + return c.exit(err) } authHeader := fmt.Sprintf("'Authorization: Bearer %s'", token) - curlArgs := append( + args := append( []string{"-H", authHeader}, // For IAP header - args..., // Original args + c.args..., // Original args ) + args = append(args, url) - if err := doCurl(binary, curlArgs); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err.Error()) - return 1 + return c.exit(runCommand(env.Binary, args)) +} + +func (c CLI) debug(a ...interface{}) { + fmt.Fprint(c.stderr, a...) +} + +func (c CLI) getURL() string { + if len(c.urls) == 0 { + return "" } + return c.urls[0].String() +} - return 0 +func runCommand(command string, args []string) error { + if _, err := exec.LookPath(command); err != nil { + return err + } + for _, arg := range args { + command += " " + arg + } + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command("cmd", "/c", command) + } else { + cmd = exec.Command("sh", "-c", command) + } + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + return cmd.Run() }