From 63f40263672c18a236e594e61bf84e9631ddaa82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 17 Oct 2019 01:52:08 +0200 Subject: [PATCH] :fire: github package --- github/client.go | 290 ----------------------- github/config.go | 397 ------------------------------- github/config_decoder.go | 61 ----- github/config_encoder.go | 52 ----- github/config_service.go | 43 ---- github/hosts.go | 64 ----- github/http.go | 490 --------------------------------------- test/helpers.go | 3 - 8 files changed, 1400 deletions(-) delete mode 100644 github/client.go delete mode 100644 github/config.go delete mode 100644 github/config_decoder.go delete mode 100644 github/config_encoder.go delete mode 100644 github/config_service.go delete mode 100644 github/hosts.go delete mode 100644 github/http.go diff --git a/github/client.go b/github/client.go deleted file mode 100644 index 6b90da1a6..000000000 --- a/github/client.go +++ /dev/null @@ -1,290 +0,0 @@ -package github - -import ( - "fmt" - "net/http" - "net/url" - "os" - "os/exec" - "strings" - - "github.com/github/gh-cli/version" -) - -const ( - GitHubHost string = "github.com" - OAuthAppURL string = "https://github.com/github/gh-cli" -) - -var userAgent = "GitHub CLI " + version.Version - -func NewClient(h string) *Client { - return NewClientWithHost(&Host{Host: h}) -} - -func NewClientWithHost(host *Host) *Client { - return &Client{Host: host} -} - -type Client struct { - Host *Host - cachedClient *simpleClient -} - -type User struct { - Login string `json:"login"` -} - -func (client *Client) CurrentUser() (user *User, err error) { - api, err := client.simpleApi() - if err != nil { - return - } - - res, err := api.Get("user") - if err = checkStatus(200, "getting current user", res, err); err != nil { - return - } - - user = &User{} - err = res.Unmarshal(user) - return -} - -type AuthorizationEntry struct { - Token string `json:"token"` -} - -func isToken(api *simpleClient, password string) bool { - api.PrepareRequest = func(req *http.Request) { - req.Header.Set("Authorization", "token "+password) - } - - res, _ := api.Get("user") - if res != nil && res.StatusCode == 200 { - return true - } - return false -} - -func (client *Client) FindOrCreateToken(user, password, twoFactorCode string) (token string, err error) { - api := client.apiClient() - - if len(password) >= 40 && isToken(api, password) { - return password, nil - } - - params := map[string]interface{}{ - "scopes": []string{"repo"}, - "note_url": OAuthAppURL, - } - - api.PrepareRequest = func(req *http.Request) { - req.SetBasicAuth(user, password) - if twoFactorCode != "" { - req.Header.Set("X-GitHub-OTP", twoFactorCode) - } - } - - count := 1 - maxTries := 9 - for { - params["note"], err = authTokenNote(count) - if err != nil { - return - } - - res, postErr := api.PostJSON("authorizations", params) - if postErr != nil { - err = postErr - break - } - - if res.StatusCode == 201 { - auth := &AuthorizationEntry{} - if err = res.Unmarshal(auth); err != nil { - return - } - token = auth.Token - break - } else if res.StatusCode == 422 && count < maxTries { - count++ - } else { - errInfo, e := res.ErrorInfo() - if e == nil { - err = errInfo - } else { - err = e - } - return - } - } - - return -} - -func (client *Client) ensureAccessToken() error { - if client.Host.AccessToken == "" { - host, err := CurrentConfig().PromptForHost(client.Host.Host) - if err != nil { - return err - } - client.Host = host - } - return nil -} - -func (client *Client) simpleApi() (c *simpleClient, err error) { - err = client.ensureAccessToken() - if err != nil { - return - } - - if client.cachedClient != nil { - c = client.cachedClient - return - } - - c = client.apiClient() - c.PrepareRequest = func(req *http.Request) { - clientDomain := normalizeHost(client.Host.Host) - if strings.HasPrefix(clientDomain, "api.github.") { - clientDomain = strings.TrimPrefix(clientDomain, "api.") - } - requestHost := strings.ToLower(req.URL.Host) - if requestHost == clientDomain || strings.HasSuffix(requestHost, "."+clientDomain) { - req.Header.Set("Authorization", "token "+client.Host.AccessToken) - } - } - - client.cachedClient = c - return -} - -func (client *Client) apiClient() *simpleClient { - unixSocket := os.ExpandEnv(client.Host.UnixSocket) - httpClient := newHttpClient(os.Getenv("HUB_TEST_HOST"), os.Getenv("HUB_VERBOSE") != "", unixSocket) - apiRoot := client.absolute(normalizeHost(client.Host.Host)) - if !strings.HasPrefix(apiRoot.Host, "api.github.") { - apiRoot.Path = "/api/v3/" - } - - return &simpleClient{ - httpClient: httpClient, - rootUrl: apiRoot, - } -} - -func (client *Client) absolute(host string) *url.URL { - u, err := url.Parse("https://" + host + "/") - if err != nil { - panic(err) - } else if client.Host != nil && client.Host.Protocol != "" { - u.Scheme = client.Host.Protocol - } - return u -} - -func normalizeHost(host string) string { - if host == "" { - return GitHubHost - } else if strings.EqualFold(host, GitHubHost) { - return "api.github.com" - } else if strings.EqualFold(host, "github.localhost") { - return "api.github.localhost" - } else { - return strings.ToLower(host) - } -} - -func checkStatus(expectedStatus int, action string, response *simpleResponse, err error) error { - if err != nil { - return fmt.Errorf("Error %s: %s", action, err.Error()) - } else if response.StatusCode != expectedStatus { - errInfo, err := response.ErrorInfo() - if err == nil { - return FormatError(action, errInfo) - } else { - return fmt.Errorf("Error %s: %s (HTTP %d)", action, err.Error(), response.StatusCode) - } - } else { - return nil - } -} - -func FormatError(action string, err error) (ee error) { - switch e := err.(type) { - default: - ee = err - case *errorInfo: - statusCode := e.Response.StatusCode - var reason string - if s := strings.SplitN(e.Response.Status, " ", 2); len(s) >= 2 { - reason = strings.TrimSpace(s[1]) - } - - errStr := fmt.Sprintf("Error %s: %s (HTTP %d)", action, reason, statusCode) - - var errorSentences []string - for _, err := range e.Errors { - switch err.Code { - case "custom": - errorSentences = append(errorSentences, err.Message) - case "missing_field": - errorSentences = append(errorSentences, fmt.Sprintf("Missing field: \"%s\"", err.Field)) - case "already_exists": - errorSentences = append(errorSentences, fmt.Sprintf("Duplicate value for \"%s\"", err.Field)) - case "invalid": - errorSentences = append(errorSentences, fmt.Sprintf("Invalid value for \"%s\"", err.Field)) - case "unauthorized": - errorSentences = append(errorSentences, fmt.Sprintf("Not allowed to change field \"%s\"", err.Field)) - } - } - - var errorMessage string - if len(errorSentences) > 0 { - errorMessage = strings.Join(errorSentences, "\n") - } else { - errorMessage = e.Message - if action == "getting current user" && e.Message == "Resource not accessible by integration" { - errorMessage = errorMessage + "\nYou must specify GITHUB_USER via environment variable." - } - } - - if errorMessage != "" { - errStr = fmt.Sprintf("%s\n%s", errStr, errorMessage) - } - - ee = fmt.Errorf(errStr) - } - - return -} - -func authTokenNote(num int) (string, error) { - n := os.Getenv("USER") - - if n == "" { - n = os.Getenv("USERNAME") - } - - if n == "" { - whoami := exec.Command("whoami") - whoamiOut, err := whoami.Output() - if err != nil { - return "", err - } - n = strings.TrimSpace(string(whoamiOut)) - } - - h, err := os.Hostname() - if err != nil { - return "", err - } - - if num > 1 { - return fmt.Sprintf("hub for %s@%s %d", n, h, num), nil - } - - return fmt.Sprintf("hub for %s@%s", n, h), nil -} diff --git a/github/config.go b/github/config.go deleted file mode 100644 index 420f56c19..000000000 --- a/github/config.go +++ /dev/null @@ -1,397 +0,0 @@ -package github - -import ( - "bufio" - "fmt" - "io/ioutil" - "net/url" - "os" - "os/signal" - "path/filepath" - "strconv" - "strings" - "syscall" - - "github.com/github/gh-cli/ui" - "github.com/github/gh-cli/utils" - "github.com/mitchellh/go-homedir" - "golang.org/x/crypto/ssh/terminal" -) - -type yamlHost struct { - User string `yaml:"user"` - OAuthToken string `yaml:"oauth_token"` - Protocol string `yaml:"protocol"` - UnixSocket string `yaml:"unix_socket,omitempty"` -} - -type Host struct { - Host string `toml:"host"` - User string `toml:"user"` - AccessToken string `toml:"access_token"` - Protocol string `toml:"protocol"` - UnixSocket string `toml:"unix_socket,omitempty"` -} - -type Config struct { - Hosts []*Host `toml:"hosts"` -} - -func (c *Config) PromptForHost(host string) (h *Host, err error) { - token := c.DetectToken() - tokenFromEnv := token != "" - - if host != GitHubHost { - if _, e := url.Parse("https://" + host); e != nil { - err = fmt.Errorf("invalid hostname: %q", host) - return - } - } - - h = c.Find(host) - if h != nil { - if h.User == "" { - utils.Check(CheckWriteable(configsFile())) - // User is missing from the config: this is a broken config probably - // because it was created with an old (broken) version of hub. Let's fix - // it now. See issue #1007 for details. - user := c.PromptForUser(host) - if user == "" { - utils.Check(fmt.Errorf("missing user")) - } - h.User = user - err := newConfigService().Save(configsFile(), c) - utils.Check(err) - } - if tokenFromEnv { - h.AccessToken = token - } else { - return - } - } else { - h = &Host{ - Host: host, - AccessToken: token, - Protocol: "https", - } - c.Hosts = append(c.Hosts, h) - } - - client := NewClientWithHost(h) - - if !tokenFromEnv { - utils.Check(CheckWriteable(configsFile())) - err = c.authorizeClient(client, host) - if err != nil { - return - } - } - - userFromEnv := os.Getenv("GITHUB_USER") - repoFromEnv := os.Getenv("GITHUB_REPOSITORY") - if userFromEnv == "" && repoFromEnv != "" { - repoParts := strings.SplitN(repoFromEnv, "/", 2) - if len(repoParts) > 0 { - userFromEnv = repoParts[0] - } - } - if tokenFromEnv && userFromEnv != "" { - h.User = userFromEnv - } else { - var currentUser *User - currentUser, err = client.CurrentUser() - if err != nil { - return - } - h.User = currentUser.Login - } - - if !tokenFromEnv { - err = newConfigService().Save(configsFile(), c) - } - - return -} - -func (c *Config) authorizeClient(client *Client, host string) (err error) { - user := c.PromptForUser(host) - pass := c.PromptForPassword(host, user) - - var code, token string - for { - token, err = client.FindOrCreateToken(user, pass, code) - if err == nil { - break - } - - if ae, ok := err.(*errorInfo); ok && strings.HasPrefix(ae.Response.Header.Get("X-GitHub-OTP"), "required;") { - if code != "" { - ui.Errorln("warning: invalid two-factor code") - } - code = c.PromptForOTP() - } else { - break - } - } - - if err == nil { - client.Host.AccessToken = token - } - - return -} - -func (c *Config) DetectToken() string { - return os.Getenv("GITHUB_TOKEN") -} - -func (c *Config) PromptForUser(host string) (user string) { - user = os.Getenv("GITHUB_USER") - if user != "" { - return - } - - ui.Printf("%s username: ", host) - user = c.scanLine() - - return -} - -func (c *Config) PromptForPassword(host, user string) (pass string) { - pass = os.Getenv("GITHUB_PASSWORD") - if pass != "" { - return - } - - ui.Printf("%s password for %s (never stored): ", host, user) - if ui.IsTerminal(os.Stdin) { - if password, err := getPassword(); err == nil { - pass = password - } - } else { - pass = c.scanLine() - } - - return -} - -func (c *Config) PromptForOTP() string { - fmt.Print("two-factor authentication code: ") - return c.scanLine() -} - -func (c *Config) scanLine() string { - var line string - scanner := bufio.NewScanner(os.Stdin) - if scanner.Scan() { - line = scanner.Text() - } - utils.Check(scanner.Err()) - - return line -} - -func getPassword() (string, error) { - stdin := int(syscall.Stdin) - initialTermState, err := terminal.GetState(stdin) - if err != nil { - return "", err - } - - c := make(chan os.Signal) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - go func() { - s := <-c - terminal.Restore(stdin, initialTermState) - switch sig := s.(type) { - case syscall.Signal: - if int(sig) == 2 { - fmt.Println("^C") - } - } - os.Exit(1) - }() - - passBytes, err := terminal.ReadPassword(stdin) - if err != nil { - return "", err - } - - signal.Stop(c) - fmt.Print("\n") - return string(passBytes), nil -} - -func (c *Config) Find(host string) *Host { - for _, h := range c.Hosts { - if h.Host == host { - return h - } - } - - return nil -} - -func (c *Config) selectHost() *Host { - options := len(c.Hosts) - - if options == 1 { - return c.Hosts[0] - } - - prompt := "Select host:\n" - for idx, host := range c.Hosts { - prompt += fmt.Sprintf(" %d. %s\n", idx+1, host.Host) - } - prompt += fmt.Sprint("> ") - - ui.Printf(prompt) - index := c.scanLine() - i, err := strconv.Atoi(index) - if err != nil || i < 1 || i > options { - utils.Check(fmt.Errorf("Error: must enter a number [1-%d]", options)) - } - - return c.Hosts[i-1] -} - -var defaultConfigsFile string - -func configsFile() string { - if configFromEnv := os.Getenv("HUB_CONFIG"); configFromEnv != "" { - return configFromEnv - } - if defaultConfigsFile == "" { - var err error - defaultConfigsFile, err = determineConfigLocation() - utils.Check(err) - } - return defaultConfigsFile -} - -func homeConfig() (string, error) { - if home, err := homedir.Dir(); err != nil { - return "", err - } else { - return filepath.Join(home, ".config"), nil - } -} - -func determineConfigLocation() (string, error) { - var err error - - xdgHome := os.Getenv("XDG_CONFIG_HOME") - configDir := xdgHome - if configDir == "" { - if configDir, err = homeConfig(); err != nil { - return "", err - } - } - - xdgDirs := os.Getenv("XDG_CONFIG_DIRS") - if xdgDirs == "" { - xdgDirs = "/etc/xdg" - } - searchDirs := append([]string{configDir}, strings.Split(xdgDirs, ":")...) - - for _, dir := range searchDirs { - filename := filepath.Join(dir, "hub") - if _, err := os.Stat(filename); err == nil { - return filename, nil - } - } - - configFile := filepath.Join(configDir, "hub") - - if configDir == xdgHome { - if homeDir, _ := homeConfig(); homeDir != "" { - legacyConfig := filepath.Join(homeDir, "hub") - if _, err = os.Stat(legacyConfig); err == nil { - ui.Errorf("Notice: config file found but not respected at: %s\n", legacyConfig) - ui.Errorf("You might want to move it to `%s' to avoid re-authenticating.\n", configFile) - } - } - } - - return configFile, nil -} - -var currentConfig *Config -var configLoadedFrom = "" - -func CurrentConfig() *Config { - filename := configsFile() - if configLoadedFrom != filename { - currentConfig = &Config{} - newConfigService().Load(filename, currentConfig) - configLoadedFrom = filename - } - - return currentConfig -} - -func (c *Config) DefaultHost() (host *Host, err error) { - if GitHubHostEnv != "" { - host, err = c.PromptForHost(GitHubHostEnv) - } else if len(c.Hosts) > 0 { - host = c.selectHost() - // HACK: forces host to inherit GITHUB_TOKEN if applicable - host, err = c.PromptForHost(host.Host) - } else { - host, err = c.PromptForHost(DefaultGitHubHost()) - } - - return -} - -// CheckWriteable checks if config file is writeable. This should -// be called before asking for credentials and only if current -// operation needs to update the file. See issue #1314 for details. -func CheckWriteable(filename string) error { - // Check if file exists already. if it doesn't, we will delete it after - // checking for writeabilty - fileExistsAlready := false - - if _, err := os.Stat(filename); err == nil { - fileExistsAlready = true - } - - err := os.MkdirAll(filepath.Dir(filename), 0771) - if err != nil { - return err - } - - w, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) - if err != nil { - return err - } - w.Close() - - if !fileExistsAlready { - err := os.Remove(filename) - if err != nil { - return err - } - } - return nil -} - -// Public for testing purpose -func CreateTestConfigs(user, token string) *Config { - f, _ := ioutil.TempFile("", "test-config") - os.Setenv("HUB_CONFIG", f.Name()) - - host := &Host{ - User: "jingweno", - AccessToken: "123", - Host: GitHubHost, - } - - c := &Config{Hosts: []*Host{host}} - err := newConfigService().Save(f.Name(), c) - if err != nil { - panic(err) - } - - return c -} diff --git a/github/config_decoder.go b/github/config_decoder.go deleted file mode 100644 index 7e467b761..000000000 --- a/github/config_decoder.go +++ /dev/null @@ -1,61 +0,0 @@ -package github - -import ( - "io" - "io/ioutil" - - "github.com/BurntSushi/toml" - "gopkg.in/yaml.v2" -) - -type configDecoder interface { - Decode(r io.Reader, c *Config) error -} - -type tomlConfigDecoder struct { -} - -func (t *tomlConfigDecoder) Decode(r io.Reader, c *Config) error { - _, err := toml.DecodeReader(r, c) - return err -} - -type yamlConfigDecoder struct { -} - -func (y *yamlConfigDecoder) Decode(r io.Reader, c *Config) error { - d, err := ioutil.ReadAll(r) - if err != nil { - return err - } - - yc := yaml.MapSlice{} - err = yaml.Unmarshal(d, &yc) - - if err != nil { - return err - } - - for _, hostEntry := range yc { - v := hostEntry.Value.([]interface{}) - if len(v) < 1 { - continue - } - host := &Host{Host: hostEntry.Key.(string)} - for _, prop := range v[0].(yaml.MapSlice) { - switch prop.Key.(string) { - case "user": - host.User = prop.Value.(string) - case "oauth_token": - host.AccessToken = prop.Value.(string) - case "protocol": - host.Protocol = prop.Value.(string) - case "unix_socket": - host.UnixSocket = prop.Value.(string) - } - } - c.Hosts = append(c.Hosts, host) - } - - return nil -} diff --git a/github/config_encoder.go b/github/config_encoder.go deleted file mode 100644 index 87609d38d..000000000 --- a/github/config_encoder.go +++ /dev/null @@ -1,52 +0,0 @@ -package github - -import ( - "io" - - "github.com/BurntSushi/toml" - "gopkg.in/yaml.v2" -) - -type configEncoder interface { - Encode(w io.Writer, c *Config) error -} - -type tomlConfigEncoder struct { -} - -func (t *tomlConfigEncoder) Encode(w io.Writer, c *Config) error { - enc := toml.NewEncoder(w) - return enc.Encode(c) -} - -type yamlConfigEncoder struct { -} - -func (y *yamlConfigEncoder) Encode(w io.Writer, c *Config) error { - yc := yaml.MapSlice{} - for _, h := range c.Hosts { - yc = append(yc, yaml.MapItem{ - Key: h.Host, - Value: []yamlHost{ - { - User: h.User, - OAuthToken: h.AccessToken, - Protocol: h.Protocol, - UnixSocket: h.UnixSocket, - }, - }, - }) - } - - d, err := yaml.Marshal(yc) - if err != nil { - return err - } - - n, err := w.Write(d) - if err == nil && n < len(d) { - err = io.ErrShortWrite - } - - return err -} diff --git a/github/config_service.go b/github/config_service.go deleted file mode 100644 index 3aaa6915f..000000000 --- a/github/config_service.go +++ /dev/null @@ -1,43 +0,0 @@ -package github - -import ( - "os" - "path/filepath" -) - -func newConfigService() *configService { - return &configService{ - Encoder: &yamlConfigEncoder{}, - Decoder: &yamlConfigDecoder{}, - } -} - -type configService struct { - Encoder configEncoder - Decoder configDecoder -} - -func (s *configService) Save(filename string, c *Config) error { - err := os.MkdirAll(filepath.Dir(filename), 0771) - if err != nil { - return err - } - - w, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return err - } - defer w.Close() - - return s.Encoder.Encode(w, c) -} - -func (s *configService) Load(filename string, c *Config) error { - r, err := os.Open(filename) - if err != nil { - return err - } - defer r.Close() - - return s.Decoder.Decode(r, c) -} diff --git a/github/hosts.go b/github/hosts.go deleted file mode 100644 index a74d9810c..000000000 --- a/github/hosts.go +++ /dev/null @@ -1,64 +0,0 @@ -package github - -import ( - "fmt" - "net/url" - "os" - "strings" - - "github.com/github/gh-cli/git" -) - -var ( - GitHubHostEnv = os.Getenv("GITHUB_HOST") - cachedHosts []string -) - -type GithubHostError struct { - url *url.URL -} - -func (e *GithubHostError) Error() string { - return fmt.Sprintf("Invalid GitHub URL: %s", e.url) -} - -func KnownGitHubHostsInclude(host string) bool { - for _, hh := range knownGitHubHosts() { - if hh == host { - return true - } - } - - return false -} - -func knownGitHubHosts() []string { - if cachedHosts != nil { - return cachedHosts - } - - hosts := []string{} - defaultHost := DefaultGitHubHost() - hosts = append(hosts, defaultHost) - hosts = append(hosts, "ssh.github.com") - - ghHosts, _ := git.ConfigAll("hub.host") - for _, ghHost := range ghHosts { - ghHost = strings.TrimSpace(ghHost) - if ghHost != "" { - hosts = append(hosts, ghHost) - } - } - - cachedHosts = hosts - return hosts -} - -func DefaultGitHubHost() string { - defaultHost := GitHubHostEnv - if defaultHost == "" { - defaultHost = GitHubHost - } - - return defaultHost -} diff --git a/github/http.go b/github/http.go deleted file mode 100644 index 25abcb351..000000000 --- a/github/http.go +++ /dev/null @@ -1,490 +0,0 @@ -package github - -import ( - "bytes" - "context" - "crypto/md5" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net" - "net/http" - "net/url" - "os" - "path" - "path/filepath" - "regexp" - "sort" - "strconv" - "strings" - "time" - - "github.com/github/gh-cli/ui" - "github.com/github/gh-cli/utils" -) - -const apiPayloadVersion = "application/vnd.github.v3+json;charset=utf-8" -const patchMediaType = "application/vnd.github.v3.patch;charset=utf-8" -const textMediaType = "text/plain;charset=utf-8" -const checksType = "application/vnd.github.antiope-preview+json;charset=utf-8" -const draftsType = "application/vnd.github.shadow-cat-preview+json;charset=utf-8" -const cacheVersion = 2 - -var inspectHeaders = []string{ - "Authorization", - "X-GitHub-OTP", - "Location", - "Link", - "Accept", -} - -type verboseTransport struct { - Transport *http.Transport - Verbose bool - OverrideURL *url.URL - Out io.Writer - Colorized bool -} - -func (t *verboseTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - if t.Verbose { - t.dumpRequest(req) - } - - if t.OverrideURL != nil { - port := "80" - if s := strings.Split(req.URL.Host, ":"); len(s) > 1 { - port = s[1] - } - - req = cloneRequest(req) - req.Header.Set("X-Original-Scheme", req.URL.Scheme) - req.Header.Set("X-Original-Port", port) - req.Host = req.URL.Host - req.URL.Scheme = t.OverrideURL.Scheme - req.URL.Host = t.OverrideURL.Host - } - - resp, err = t.Transport.RoundTrip(req) - - if err == nil && t.Verbose { - t.dumpResponse(resp) - } - - return -} - -func (t *verboseTransport) dumpRequest(req *http.Request) { - info := fmt.Sprintf("> %s %s://%s%s", req.Method, req.URL.Scheme, req.URL.Host, req.URL.RequestURI()) - t.verbosePrintln(info) - t.dumpHeaders(req.Header, ">") - body := t.dumpBody(req.Body) - if body != nil { - // reset body since it's been read - req.Body = body - } -} - -func (t *verboseTransport) dumpResponse(resp *http.Response) { - info := fmt.Sprintf("< HTTP %d", resp.StatusCode) - t.verbosePrintln(info) - t.dumpHeaders(resp.Header, "<") - body := t.dumpBody(resp.Body) - if body != nil { - // reset body since it's been read - resp.Body = body - } -} - -func (t *verboseTransport) dumpHeaders(header http.Header, indent string) { - for _, listed := range inspectHeaders { - for name, vv := range header { - if !strings.EqualFold(name, listed) { - continue - } - for _, v := range vv { - if v != "" { - r := regexp.MustCompile("(?i)^(basic|token) (.+)") - if r.MatchString(v) { - v = r.ReplaceAllString(v, "$1 [REDACTED]") - } - - info := fmt.Sprintf("%s %s: %s", indent, name, v) - t.verbosePrintln(info) - } - } - } - } -} - -func (t *verboseTransport) dumpBody(body io.ReadCloser) io.ReadCloser { - if body == nil { - return nil - } - - defer body.Close() - buf := new(bytes.Buffer) - _, err := io.Copy(buf, body) - utils.Check(err) - - if buf.Len() > 0 { - t.verbosePrintln(buf.String()) - } - - return ioutil.NopCloser(buf) -} - -func (t *verboseTransport) verbosePrintln(msg string) { - if t.Colorized { - msg = fmt.Sprintf("\033[36m%s\033[0m", msg) - } - - fmt.Fprintln(t.Out, msg) -} - -func newHttpClient(testHost string, verbose bool, unixSocket string) *http.Client { - var testURL *url.URL - if testHost != "" { - testURL, _ = url.Parse(testHost) - } - var httpTransport *http.Transport - if unixSocket != "" { - dialFunc := func(network, addr string) (net.Conn, error) { - return net.Dial("unix", unixSocket) - } - dialContext := func(_ context.Context, _, _ string) (net.Conn, error) { - return net.Dial("unix", unixSocket) - } - httpTransport = &http.Transport{ - DialContext: dialContext, - DialTLS: dialFunc, - ResponseHeaderTimeout: 30 * time.Second, - ExpectContinueTimeout: 10 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - } - } else { - httpTransport = &http.Transport{ - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).DialContext, - TLSHandshakeTimeout: 10 * time.Second, - } - } - tr := &verboseTransport{ - Transport: httpTransport, - Verbose: verbose, - OverrideURL: testURL, - Out: ui.Stderr, - Colorized: ui.IsTerminal(os.Stderr), - } - - return &http.Client{ - Transport: tr, - } -} - -func cloneRequest(req *http.Request) *http.Request { - dup := new(http.Request) - *dup = *req - dup.URL, _ = url.Parse(req.URL.String()) - dup.Header = make(http.Header) - for k, s := range req.Header { - dup.Header[k] = s - } - return dup -} - -type simpleClient struct { - httpClient *http.Client - rootUrl *url.URL - PrepareRequest func(*http.Request) - CacheTTL int -} - -func (c *simpleClient) performRequest(method, path string, body io.Reader, configure func(*http.Request)) (*simpleResponse, error) { - url, err := url.Parse(path) - if err == nil { - url = c.rootUrl.ResolveReference(url) - return c.performRequestUrl(method, url, body, configure) - } else { - return nil, err - } -} - -func (c *simpleClient) performRequestUrl(method string, url *url.URL, body io.Reader, configure func(*http.Request)) (res *simpleResponse, err error) { - req, err := http.NewRequest(method, url.String(), body) - if err != nil { - return - } - if c.PrepareRequest != nil { - c.PrepareRequest(req) - } - req.Header.Set("User-Agent", userAgent) - req.Header.Set("Accept", apiPayloadVersion) - - if configure != nil { - configure(req) - } - - key := cacheKey(req) - if cachedResponse := c.cacheRead(key, req); cachedResponse != nil { - res = &simpleResponse{cachedResponse} - return - } - - httpResponse, err := c.httpClient.Do(req) - if err != nil { - return - } - - c.cacheWrite(key, httpResponse) - res = &simpleResponse{httpResponse} - - return -} - -func isGraphQL(req *http.Request) bool { - return req.URL.Path == "/graphql" -} - -func canCache(req *http.Request) bool { - return strings.EqualFold(req.Method, "GET") || isGraphQL(req) -} - -func (c *simpleClient) cacheRead(key string, req *http.Request) (res *http.Response) { - if c.CacheTTL > 0 && canCache(req) { - f := cacheFile(key) - cacheInfo, err := os.Stat(f) - if err != nil { - return - } - if time.Since(cacheInfo.ModTime()).Seconds() > float64(c.CacheTTL) { - return - } - cf, err := os.Open(f) - if err != nil { - return - } - defer cf.Close() - - cb, err := ioutil.ReadAll(cf) - if err != nil { - return - } - parts := strings.SplitN(string(cb), "\r\n\r\n", 2) - if len(parts) < 2 { - return - } - - res = &http.Response{ - Body: ioutil.NopCloser(bytes.NewBufferString(parts[1])), - Header: http.Header{}, - } - headerLines := strings.Split(parts[0], "\r\n") - if len(headerLines) < 1 { - return - } - if proto := strings.SplitN(headerLines[0], " ", 3); len(proto) >= 3 { - res.Proto = proto[0] - res.Status = fmt.Sprintf("%s %s", proto[1], proto[2]) - if code, _ := strconv.Atoi(proto[1]); code > 0 { - res.StatusCode = code - } - } - for _, line := range headerLines[1:] { - kv := strings.SplitN(line, ":", 2) - if len(kv) >= 2 { - res.Header.Add(kv[0], strings.TrimLeft(kv[1], " ")) - } - } - } - return -} - -func (c *simpleClient) cacheWrite(key string, res *http.Response) { - if c.CacheTTL > 0 && canCache(res.Request) && res.StatusCode < 500 && res.StatusCode != 403 { - bodyCopy := &bytes.Buffer{} - bodyReplacement := readCloserCallback{ - Reader: io.TeeReader(res.Body, bodyCopy), - Closer: res.Body, - Callback: func() { - f := cacheFile(key) - err := os.MkdirAll(filepath.Dir(f), 0771) - if err != nil { - return - } - cf, err := os.OpenFile(f, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return - } - defer cf.Close() - fmt.Fprintf(cf, "%s %s\r\n", res.Proto, res.Status) - res.Header.Write(cf) - fmt.Fprintf(cf, "\r\n") - io.Copy(cf, bodyCopy) - }, - } - res.Body = &bodyReplacement - } -} - -type readCloserCallback struct { - Callback func() - Closer io.Closer - io.Reader -} - -func (rc *readCloserCallback) Close() error { - err := rc.Closer.Close() - if err == nil { - rc.Callback() - } - return err -} - -func cacheKey(req *http.Request) string { - path := strings.Replace(req.URL.EscapedPath(), "/", "-", -1) - if len(path) > 1 { - path = strings.TrimPrefix(path, "-") - } - host := req.Host - if host == "" { - host = req.URL.Host - } - hash := md5.New() - fmt.Fprintf(hash, "%d:", cacheVersion) - io.WriteString(hash, req.Header.Get("Accept")) - io.WriteString(hash, req.Header.Get("Authorization")) - queryParts := strings.Split(req.URL.RawQuery, "&") - sort.Strings(queryParts) - for _, q := range queryParts { - fmt.Fprintf(hash, "%s&", q) - } - if isGraphQL(req) && req.Body != nil { - if b, err := ioutil.ReadAll(req.Body); err == nil { - req.Body = ioutil.NopCloser(bytes.NewBuffer(b)) - hash.Write(b) - } - } - return fmt.Sprintf("%s/%s_%x", host, path, hash.Sum(nil)) -} - -func cacheFile(key string) string { - return path.Join(os.TempDir(), "hub", "api", key) -} - -func (c *simpleClient) jsonRequest(method, path string, body interface{}, configure func(*http.Request)) (*simpleResponse, error) { - json, err := json.Marshal(body) - if err != nil { - return nil, err - } - buf := bytes.NewBuffer(json) - - return c.performRequest(method, path, buf, func(req *http.Request) { - req.Header.Set("Content-Type", "application/json; charset=utf-8") - if configure != nil { - configure(req) - } - }) -} - -func (c *simpleClient) Get(path string) (*simpleResponse, error) { - return c.performRequest("GET", path, nil, nil) -} - -func (c *simpleClient) GetFile(path string, mimeType string) (*simpleResponse, error) { - return c.performRequest("GET", path, nil, func(req *http.Request) { - req.Header.Set("Accept", mimeType) - }) -} - -func (c *simpleClient) PostJSON(path string, payload interface{}) (*simpleResponse, error) { - return c.jsonRequest("POST", path, payload, nil) -} - -func (c *simpleClient) PostJSONPreview(path string, payload interface{}, mimeType string) (*simpleResponse, error) { - return c.jsonRequest("POST", path, payload, func(req *http.Request) { - req.Header.Set("Accept", mimeType) - }) -} - -func (c *simpleClient) PatchJSON(path string, payload interface{}) (*simpleResponse, error) { - return c.jsonRequest("PATCH", path, payload, nil) -} - -type simpleResponse struct { - *http.Response -} - -type errorInfo struct { - Message string `json:"message"` - Errors []fieldError `json:"errors"` - Response *http.Response -} -type errorInfoSimple struct { - Message string `json:"message"` - Errors []string `json:"errors"` -} -type fieldError struct { - Resource string `json:"resource"` - Message string `json:"message"` - Code string `json:"code"` - Field string `json:"field"` -} - -func (e *errorInfo) Error() string { - return e.Message -} - -func (res *simpleResponse) Unmarshal(dest interface{}) (err error) { - defer res.Body.Close() - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return - } - - return json.Unmarshal(body, dest) -} - -func (res *simpleResponse) ErrorInfo() (msg *errorInfo, err error) { - defer res.Body.Close() - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return - } - - msg = &errorInfo{} - err = json.Unmarshal(body, msg) - if err != nil { - msgSimple := &errorInfoSimple{} - if err = json.Unmarshal(body, msgSimple); err == nil { - msg.Message = msgSimple.Message - for _, errMsg := range msgSimple.Errors { - msg.Errors = append(msg.Errors, fieldError{ - Code: "custom", - Message: errMsg, - }) - } - } - } - if err == nil { - msg.Response = res.Response - } - - return -} - -func (res *simpleResponse) Link(name string) string { - linkVal := res.Header.Get("Link") - re := regexp.MustCompile(`<([^>]+)>; rel="([^"]+)"`) - for _, match := range re.FindAllStringSubmatch(linkVal, -1) { - if match[2] == name { - return match[1] - } - } - return "" -} diff --git a/test/helpers.go b/test/helpers.go index ce2de88df..90b16778a 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/github/gh-cli/api" - "github.com/github/gh-cli/github" "github.com/spf13/cobra" ) @@ -20,8 +19,6 @@ type TempGitRepo struct { } func UseTempGitRepo() *TempGitRepo { - github.CreateTestConfigs("mario", "i-love-peach") - pwd, _ := os.Getwd() oldEnv := make(map[string]string) overrideEnv := func(name, value string) {