Skip to content

Commit

Permalink
Merge pull request cli#3803 from cli/http-accept-header
Browse files Browse the repository at this point in the history
Update "Accept" header for github.com requests
  • Loading branch information
mislav authored Jun 11, 2021
2 parents 7dbaaf2 + 4debbb1 commit af90f72
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 10 deletions.
27 changes: 17 additions & 10 deletions pkg/cmd/factory/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"time"

"github.com/cli/cli/api"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/pkg/iostreams"
)
Expand Down Expand Up @@ -53,8 +52,12 @@ var timezoneNames = map[int]string{
50400: "Pacific/Kiritimati",
}

type configGetter interface {
Get(string, string) (string, error)
}

// generic authenticated HTTP client for commands
func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string, setAccept bool) *http.Client {
func NewHTTPClient(io *iostreams.IOStreams, cfg configGetter, appVersion string, setAccept bool) *http.Client {
var opts []api.ClientOption
if verbose := os.Getenv("DEBUG"); verbose != "" {
logTraffic := strings.Contains(verbose, "api")
Expand All @@ -64,7 +67,7 @@ func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string
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(req.URL.Hostname())
hostname := ghinstance.NormalizeHostname(getHost(req))
if token, err := cfg.Get(hostname, "oauth_token"); err == nil && token != "" {
return fmt.Sprintf("token %s", token), nil
}
Expand All @@ -85,13 +88,10 @@ func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string
if setAccept {
opts = append(opts,
api.AddHeaderFunc("Accept", func(req *http.Request) (string, error) {
// antiope-preview: Checks
accept := "application/vnd.github.antiope-preview+json"
// introduced for #2952: pr branch up to date status
accept += ", application/vnd.github.merge-info-preview+json"
if ghinstance.IsEnterprise(req.URL.Hostname()) {
// shadow-cat-preview: Draft pull requests
accept += ", application/vnd.github.shadow-cat-preview"
accept := "application/vnd.github.merge-info-preview+json" // PullRequest.mergeStateStatus
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
}),
Expand All @@ -100,3 +100,10 @@ func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string

return api.NewHTTPClient(opts...)
}

func getHost(r *http.Request) string {
if r.Host != "" {
return r.Host
}
return r.URL.Hostname()
}
174 changes: 174 additions & 0 deletions pkg/cmd/factory/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package factory

import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"regexp"
"testing"

"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/pkg/iostreams"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewHTTPClient(t *testing.T) {
type args struct {
config configGetter
appVersion string
setAccept bool
}
tests := []struct {
name string
args args
envDebug string
host string
wantHeader map[string]string
wantStderr string
}{
{
name: "github.com with Accept header",
args: args{
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: true,
},
host: "github.com",
wantHeader: map[string]string{
"authorization": "token MYTOKEN",
"user-agent": "GitHub CLI v1.2.3",
"accept": "application/vnd.github.merge-info-preview+json",
},
wantStderr: "",
},
{
name: "github.com no Accept header",
args: args{
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: false,
},
host: "github.com",
wantHeader: map[string]string{
"authorization": "token MYTOKEN",
"user-agent": "GitHub CLI v1.2.3",
"accept": "",
},
wantStderr: "",
},
{
name: "github.com no authentication token",
args: args{
config: tinyConfig{"example.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: true,
},
host: "github.com",
wantHeader: map[string]string{
"authorization": "",
"user-agent": "GitHub CLI v1.2.3",
"accept": "application/vnd.github.merge-info-preview+json",
},
wantStderr: "",
},
{
name: "github.com in verbose mode",
args: args{
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: true,
},
host: "github.com",
envDebug: "api",
wantHeader: map[string]string{
"authorization": "token MYTOKEN",
"user-agent": "GitHub CLI v1.2.3",
"accept": "application/vnd.github.merge-info-preview+json",
},
wantStderr: heredoc.Doc(`
* Request at <time>
* Request to http://<host>:<port>
> GET / HTTP/1.1
> Host: github.com
> Accept: application/vnd.github.merge-info-preview+json
> Authorization: token ████████████████████
> User-Agent: GitHub CLI v1.2.3
< HTTP/1.1 204 No Content
< Date: <time>
* Request took <duration>
`),
},
{
name: "GHES Accept header",
args: args{
config: tinyConfig{"example.com:oauth_token": "GHETOKEN"},
appVersion: "v1.2.3",
setAccept: true,
},
host: "example.com",
wantHeader: map[string]string{
"authorization": "token GHETOKEN",
"user-agent": "GitHub CLI v1.2.3",
"accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.antiope-preview, application/vnd.github.shadow-cat-preview",
},
wantStderr: "",
},
}

var gotReq *http.Request
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotReq = r
w.WriteHeader(http.StatusNoContent)
}))
defer ts.Close()

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
oldDebug := os.Getenv("DEBUG")
os.Setenv("DEBUG", tt.envDebug)
t.Cleanup(func() {
os.Setenv("DEBUG", oldDebug)
})

io, _, _, stderr := iostreams.Test()
client := NewHTTPClient(io, tt.args.config, tt.args.appVersion, tt.args.setAccept)

req, err := http.NewRequest("GET", ts.URL, nil)
req.Host = tt.host
require.NoError(t, err)

res, err := client.Do(req)
require.NoError(t, err)

for name, value := range tt.wantHeader {
assert.Equal(t, value, gotReq.Header.Get(name), name)
}

assert.Equal(t, 204, res.StatusCode)
assert.Equal(t, tt.wantStderr, normalizeVerboseLog(stderr.String()))
})
}
}

type tinyConfig map[string]string

func (c tinyConfig) Get(host, key string) (string, error) {
return c[fmt.Sprintf("%s:%s", host, key)], nil
}

var requestAtRE = regexp.MustCompile(`(?m)^\* Request at .+`)
var dateRE = regexp.MustCompile(`(?m)^< Date: .+`)
var hostWithPortRE = regexp.MustCompile(`127\.0\.0\.1:\d+`)
var durationRE = regexp.MustCompile(`(?m)^\* Request took .+`)

func normalizeVerboseLog(t string) string {
t = requestAtRE.ReplaceAllString(t, "* Request at <time>")
t = hostWithPortRE.ReplaceAllString(t, "<host>:<port>")
t = dateRE.ReplaceAllString(t, "< Date: <time>")
t = durationRE.ReplaceAllString(t, "* Request took <duration>")
return t
}

0 comments on commit af90f72

Please sign in to comment.