Skip to content

Commit 0ea1f77

Browse files
fix: preserve authority for loopback GHES hosts
Address review: the loopback exception accepted http://localhost:3000 and http://[::1], but newGHESHost built URLs from u.Hostname(), which drops the port (silently retargeting the dev server to port 80) and strips IPv6 brackets (producing an unusable URL such as http://::1/api/v3/). Derive the base-host REST/GraphQL/upload/raw/authorization URLs from u.Host so the port and IPv6 brackets are preserved. Subdomain-isolation URLs keep using the bare hostname, since a label cannot be prepended to a host:port or an IP literal. Add tests for the ::1 case and for port preservation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 0c825b4 commit 0ea1f77

2 files changed

Lines changed: 28 additions & 5 deletions

File tree

pkg/utils/api.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,20 @@ func newGHESHost(hostname string) (APIHost, error) {
145145
return APIHost{}, fmt.Errorf("failed to parse GHES URL: %w", err)
146146
}
147147

148-
restURL, err := url.Parse(fmt.Sprintf("%s://%s/api/v3/", u.Scheme, u.Hostname()))
148+
// Preserve the full authority (host, port, and IPv6 brackets) for the
149+
// base-host URLs. u.Hostname() drops the port and strips IPv6 brackets,
150+
// which would silently retarget a loopback dev server to port 80 and produce
151+
// an unusable URL for [::1]. The subdomain-isolation URLs below still derive
152+
// from the bare hostname, since a label cannot be prepended to a host:port or
153+
// an IP literal.
154+
authority := u.Host
155+
156+
restURL, err := url.Parse(fmt.Sprintf("%s://%s/api/v3/", u.Scheme, authority))
149157
if err != nil {
150158
return APIHost{}, fmt.Errorf("failed to parse GHES REST URL: %w", err)
151159
}
152160

153-
gqlURL, err := url.Parse(fmt.Sprintf("%s://%s/api/graphql", u.Scheme, u.Hostname()))
161+
gqlURL, err := url.Parse(fmt.Sprintf("%s://%s/api/graphql", u.Scheme, authority))
154162
if err != nil {
155163
return APIHost{}, fmt.Errorf("failed to parse GHES GraphQL URL: %w", err)
156164
}
@@ -165,7 +173,7 @@ func newGHESHost(hostname string) (APIHost, error) {
165173
uploadURL, err = url.Parse(fmt.Sprintf("%s://uploads.%s/", u.Scheme, u.Hostname()))
166174
} else {
167175
// Without subdomain isolation: https://hostname/api/uploads/
168-
uploadURL, err = url.Parse(fmt.Sprintf("%s://%s/api/uploads/", u.Scheme, u.Hostname()))
176+
uploadURL, err = url.Parse(fmt.Sprintf("%s://%s/api/uploads/", u.Scheme, authority))
169177
}
170178
if err != nil {
171179
return APIHost{}, fmt.Errorf("failed to parse GHES Upload URL: %w", err)
@@ -177,13 +185,13 @@ func newGHESHost(hostname string) (APIHost, error) {
177185
rawURL, err = url.Parse(fmt.Sprintf("%s://raw.%s/", u.Scheme, u.Hostname()))
178186
} else {
179187
// Without subdomain isolation: https://hostname/raw/
180-
rawURL, err = url.Parse(fmt.Sprintf("%s://%s/raw/", u.Scheme, u.Hostname()))
188+
rawURL, err = url.Parse(fmt.Sprintf("%s://%s/raw/", u.Scheme, authority))
181189
}
182190
if err != nil {
183191
return APIHost{}, fmt.Errorf("failed to parse GHES Raw URL: %w", err)
184192
}
185193

186-
authorizationServerURL, err := url.Parse(fmt.Sprintf("%s://%s/login/oauth", u.Scheme, u.Hostname()))
194+
authorizationServerURL, err := url.Parse(fmt.Sprintf("%s://%s/login/oauth", u.Scheme, authority))
187195
if err != nil {
188196
return APIHost{}, fmt.Errorf("failed to parse GHES Authorization Server URL: %w", err)
189197
}

pkg/utils/api_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,21 @@ func TestParseAPIHost(t *testing.T) {
7676
input: "http://127.0.0.1",
7777
wantRestURL: "http://127.0.0.1/api/v3/",
7878
},
79+
{
80+
name: "http loopback preserves port for local development",
81+
input: "http://localhost:3000",
82+
wantRestURL: "http://localhost:3000/api/v3/",
83+
},
84+
{
85+
name: "http ipv6 loopback preserves brackets",
86+
input: "http://[::1]",
87+
wantRestURL: "http://[::1]/api/v3/",
88+
},
89+
{
90+
name: "http ipv6 loopback preserves brackets and port",
91+
input: "http://[::1]:8080",
92+
wantRestURL: "http://[::1]:8080/api/v3/",
93+
},
7994
{
8095
name: "http remote host rejected",
8196
input: "http://notgithub.com",

0 commit comments

Comments
 (0)