package factory import ( "fmt" "net/http" "regexp" "strings" "time" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/httpunix" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/utils" ) var timezoneNames = map[int]string{ -39600: "Pacific/Niue", -36000: "Pacific/Honolulu", -34200: "Pacific/Marquesas", -32400: "America/Anchorage", -28800: "America/Los_Angeles", -25200: "America/Chihuahua", -21600: "America/Chicago", -18000: "America/Bogota", -14400: "America/Caracas", -12600: "America/St_Johns", -10800: "America/Argentina/Buenos_Aires", -7200: "Atlantic/South_Georgia", -3600: "Atlantic/Cape_Verde", 0: "Europe/London", 3600: "Europe/Amsterdam", 7200: "Europe/Athens", 10800: "Europe/Istanbul", 12600: "Asia/Tehran", 14400: "Asia/Dubai", 16200: "Asia/Kabul", 18000: "Asia/Tashkent", 19800: "Asia/Kolkata", 20700: "Asia/Kathmandu", 21600: "Asia/Dhaka", 23400: "Asia/Rangoon", 25200: "Asia/Bangkok", 28800: "Asia/Manila", 31500: "Australia/Eucla", 32400: "Asia/Tokyo", 34200: "Australia/Darwin", 36000: "Australia/Brisbane", 37800: "Australia/Adelaide", 39600: "Pacific/Guadalcanal", 43200: "Pacific/Nauru", 46800: "Pacific/Auckland", 49500: "Pacific/Chatham", 50400: "Pacific/Kiritimati", } type configGetter interface { Get(string, string) (string, error) } // generic authenticated HTTP client for commands func NewHTTPClient(io *iostreams.IOStreams, cfg configGetter, appVersion string, setAccept bool) (*http.Client, error) { var opts []api.ClientOption // We need to check and potentially add the unix socket roundtripper option // before adding any other options, since if we are going to use the unix // socket transport, it needs to form the base of the transport chain // represented by invocations of opts... // // Another approach might be to change the signature of api.NewHTTPClient to // take an explicit base http.RoundTripper as its first parameter (it // currently defaults internally to http.DefaultTransport), or add another // variant like api.NewHTTPClientWithBaseRoundTripper. But, the only caller // which would use that non-default behavior is right here, and it doesn't // seem worth the cognitive overhead everywhere else just to serve this one // use case. unixSocket, err := cfg.Get("", "http_unix_socket") if err != nil { return nil, err } if unixSocket != "" { opts = append(opts, api.ClientOption(func(http.RoundTripper) http.RoundTripper { return httpunix.NewRoundTripper(unixSocket) })) } if isVerbose, debugValue := utils.IsDebugEnabled(); isVerbose { logTraffic := strings.Contains(debugValue, "api") opts = append(opts, api.VerboseLog(io.ErrOut, logTraffic, io.IsStderrTTY())) } opts = append(opts, api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", appVersion)), api.AddHeaderFunc("Authorization", func(req *http.Request) (string, error) { hostname := ghinstance.NormalizeHostname(getHost(req)) if token, err := cfg.Get(hostname, "oauth_token"); err == nil && token != "" { return fmt.Sprintf("token %s", token), nil } return "", nil }), api.AddHeaderFunc("Time-Zone", func(req *http.Request) (string, error) { if req.Method != "GET" && req.Method != "HEAD" { if time.Local.String() != "Local" { return time.Local.String(), nil } _, offset := time.Now().Zone() return timezoneNames[offset], nil } return "", nil }), api.ExtractHeader("X-GitHub-SSO", &ssoHeader), ) if setAccept { opts = append(opts, api.AddHeaderFunc("Accept", func(req *http.Request) (string, error) { accept := "application/vnd.github.merge-info-preview+json" // PullRequest.mergeStateStatus accept += ", application/vnd.github.nebula-preview" // visibility when RESTing repos into an org if ghinstance.IsEnterprise(getHost(req)) { accept += ", application/vnd.github.antiope-preview" // Commit.statusCheckRollup accept += ", application/vnd.github.shadow-cat-preview" // PullRequest.isDraft } return accept, nil }), ) } return api.NewHTTPClient(opts...), nil } var ssoHeader string var ssoURLRE = regexp.MustCompile(`\burl=([^;]+)`) // SSOURL returns the URL of a SAML SSO challenge received by the server for clients that use ExtractHeader // to extract the value of the "X-GitHub-SSO" response header. func SSOURL() string { if ssoHeader == "" { return "" } m := ssoURLRE.FindStringSubmatch(ssoHeader) if m == nil { return "" } return m[1] } func getHost(r *http.Request) string { if r.Host != "" { return r.Host } return r.URL.Hostname() }