Skip to content

Commit c4d9e26

Browse files
authored
fix: Fix base url regression to ensure trailing / (#2990)
Signed-off-by: Steve Hipwell <steve.hipwell@gmail.com>
1 parent 2838f0f commit c4d9e26

12 files changed

+250
-222
lines changed

github/apps.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"io"
1010
"net/http"
1111
"net/url"
12-
"path"
1312
"time"
1413

1514
"github.com/go-jose/go-jose/v3"
@@ -18,29 +17,22 @@ import (
1817

1918
// GenerateOAuthTokenFromApp generates a GitHub OAuth access token from a set of valid GitHub App credentials.
2019
// The returned token can be used to interact with both GitHub's REST and GraphQL APIs.
21-
func GenerateOAuthTokenFromApp(baseURL *url.URL, appID, appInstallationID, pemData string) (string, error) {
20+
func GenerateOAuthTokenFromApp(apiURL *url.URL, appID, appInstallationID, pemData string) (string, error) {
2221
appJWT, err := generateAppJWT(appID, time.Now(), []byte(pemData))
2322
if err != nil {
2423
return "", err
2524
}
2625

27-
token, err := getInstallationAccessToken(baseURL, appJWT, appInstallationID)
26+
token, err := getInstallationAccessToken(apiURL, appJWT, appInstallationID)
2827
if err != nil {
2928
return "", err
3029
}
3130

3231
return token, nil
3332
}
3433

35-
func getInstallationAccessToken(baseURL *url.URL, jwt, installationID string) (string, error) {
36-
hostname := baseURL.Hostname()
37-
if hostname != DotComHost && !GHECDataResidencyHostMatch.MatchString(hostname) {
38-
baseURL.Path = path.Join(baseURL.Path, "api/v3/")
39-
}
40-
41-
baseURL.Path = path.Join(baseURL.Path, "app/installations/", installationID, "access_tokens")
42-
43-
req, err := http.NewRequest(http.MethodPost, baseURL.String(), nil)
34+
func getInstallationAccessToken(apiURL *url.URL, jwt, installationID string) (string, error) {
35+
req, err := http.NewRequest(http.MethodPost, apiURL.JoinPath("app/installations", installationID, "access_tokens").String(), nil)
4436
if err != nil {
4537
return "", err
4638
}

github/apps_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ func TestGetInstallationAccessToken(t *testing.T) {
146146

147147
ts := githubApiMock([]*mockResponse{
148148
{
149-
ExpectedUri: fmt.Sprintf("/api/v3/app/installations/%s/access_tokens", testGitHubAppInstallationID),
149+
ExpectedUri: fmt.Sprintf("/app/installations/%s/access_tokens", testGitHubAppInstallationID),
150150
ExpectedHeaders: map[string]string{
151151
"Accept": "application/vnd.github.v3+json",
152152
"Authorization": fmt.Sprintf("Bearer %s", fakeJWT),

github/config.go

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"fmt"
66
"net/http"
77
"net/url"
8-
"path"
98
"regexp"
109
"strings"
1110
"time"
@@ -19,7 +18,8 @@ import (
1918
type Config struct {
2019
Token string
2120
Owner string
22-
BaseURL string
21+
BaseURL *url.URL
22+
IsGHES bool
2323
Insecure bool
2424
WriteDelay time.Duration
2525
ReadDelay time.Duration
@@ -38,12 +38,23 @@ type Owner struct {
3838
IsOrganization bool
3939
}
4040

41-
// DotComHost is the hostname for GitHub.com API.
42-
const DotComHost = "api.github.com"
41+
const (
42+
// DotComAPIURL is the base API URL for github.com.
43+
DotComAPIURL = "https://api.github.com/"
44+
// DotComHost is the hostname for github.com.
45+
DotComHost = "github.com"
46+
// DotComAPIHost is the API hostname for github.com.
47+
DotComAPIHost = "api.github.com"
48+
// GHESRESTAPISuffix is the rest api suffix for GitHub Enterprise Server.
49+
GHESRESTAPIPath = "api/v3/"
50+
)
4351

44-
// GHECDataResidencyHostMatch is a regex to match a GitHub Enterprise Cloud data residency host:
45-
// https://[hostname].ghe.com/ instances expect paths that behave similar to GitHub.com, not GitHub Enterprise Server.
46-
var GHECDataResidencyHostMatch = regexp.MustCompile(`^[a-zA-Z0-9.\-]+\.ghe\.com\/?$`)
52+
var (
53+
// GHECHostMatch is a regex to match GitHub Enterprise Cloud hosts.
54+
GHECHostMatch = regexp.MustCompile(`\.ghe\.com$`)
55+
// GHECAPIHostMatch is a regex to match GitHub Enterprise Cloud API hosts.
56+
GHECAPIHostMatch = regexp.MustCompile(`^api\.[a-zA-Z0-9-]+\.ghe\.com$`)
57+
)
4758

4859
func RateLimitedHTTPClient(client *http.Client, writeDelay, readDelay, retryDelay time.Duration, parallelRequests bool, retryableErrors map[int]bool, maxRetries int) *http.Client {
4960
client.Transport = NewEtagTransport(client.Transport)
@@ -81,38 +92,24 @@ func (c *Config) AnonymousHTTPClient() *http.Client {
8192
}
8293

8394
func (c *Config) NewGraphQLClient(client *http.Client) (*githubv4.Client, error) {
84-
uv4, err := url.Parse(c.BaseURL)
85-
if err != nil {
86-
return nil, err
87-
}
88-
89-
hostname := uv4.Hostname()
90-
if hostname != DotComHost && !GHECDataResidencyHostMatch.MatchString(hostname) {
91-
uv4.Path = path.Join(uv4.Path, "api/graphql/")
95+
var path string
96+
if c.IsGHES {
97+
path = "api/graphql"
9298
} else {
93-
uv4.Path = path.Join(uv4.Path, "graphql")
99+
path = "graphql"
94100
}
95101

96-
return githubv4.NewEnterpriseClient(uv4.String(), client), nil
102+
return githubv4.NewEnterpriseClient(c.BaseURL.JoinPath(path).String(), client), nil
97103
}
98104

99105
func (c *Config) NewRESTClient(client *http.Client) (*github.Client, error) {
100-
uv3, err := url.Parse(c.BaseURL)
101-
if err != nil {
102-
return nil, err
106+
path := ""
107+
if c.IsGHES {
108+
path = GHESRESTAPIPath
103109
}
104110

105-
hostname := uv3.Hostname()
106-
if hostname != DotComHost && !GHECDataResidencyHostMatch.MatchString(hostname) {
107-
uv3.Path = fmt.Sprintf("%s/", path.Join(uv3.Path, "api/v3"))
108-
}
109-
110-
v3client, err := github.NewClient(client).WithEnterpriseURLs(uv3.String(), "")
111-
if err != nil {
112-
return nil, err
113-
}
114-
115-
v3client.BaseURL = uv3
111+
v3client := github.NewClient(client)
112+
v3client.BaseURL = c.BaseURL.JoinPath(path)
116113

117114
return v3client, nil
118115
}
@@ -199,3 +196,45 @@ func (injector *previewHeaderInjectorTransport) RoundTrip(req *http.Request) (*h
199196
}
200197
return injector.rt.RoundTrip(req)
201198
}
199+
200+
// getBaseURL returns a correctly configured base URL and a bool as to if this is GitHub Enterprise Server.
201+
func getBaseURL(s string) (*url.URL, bool, error) {
202+
if len(s) == 0 {
203+
s = DotComAPIURL
204+
}
205+
206+
u, err := url.Parse(s)
207+
if err != nil {
208+
return nil, false, err
209+
}
210+
211+
if !u.IsAbs() {
212+
return nil, false, fmt.Errorf("base url must be absolute")
213+
}
214+
215+
u = u.JoinPath("/")
216+
217+
switch {
218+
case u.Host == DotComAPIHost:
219+
case u.Host == DotComHost:
220+
u.Host = DotComAPIHost
221+
case GHECAPIHostMatch.MatchString(u.Host):
222+
case GHECHostMatch.MatchString(u.Host):
223+
u.Host = fmt.Sprintf("api.%s", u.Host)
224+
default:
225+
u.Path = strings.TrimSuffix(u.Path, GHESRESTAPIPath)
226+
return u, true, nil
227+
}
228+
229+
if u.Scheme != "https" {
230+
return nil, false, fmt.Errorf("base url for github.com or ghe.com must use the https scheme")
231+
}
232+
233+
if len(u.Path) > 1 {
234+
return nil, false, fmt.Errorf("base url for github.com or ghe.com must not contain a path, got %s", u.Path)
235+
}
236+
237+
u.Path = "/"
238+
239+
return u, false, nil
240+
}

0 commit comments

Comments
 (0)