-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Expand file tree
/
Copy pathapi.go
More file actions
293 lines (241 loc) · 9 KB
/
Copy pathapi.go
File metadata and controls
293 lines (241 loc) · 9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
package utils //nolint:revive //TODO: figure out a better name for this package
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
type APIHostResolver interface {
BaseRESTURL(ctx context.Context) (*url.URL, error)
GraphqlURL(ctx context.Context) (*url.URL, error)
UploadURL(ctx context.Context) (*url.URL, error)
RawURL(ctx context.Context) (*url.URL, error)
AuthorizationServerURL(ctx context.Context) (*url.URL, error)
}
type APIHost struct {
restURL *url.URL
gqlURL *url.URL
uploadURL *url.URL
rawURL *url.URL
authorizationServerURL *url.URL
}
var _ APIHostResolver = APIHost{}
func NewAPIHost(s string) (APIHostResolver, error) {
a, err := parseAPIHost(s)
if err != nil {
return nil, err
}
return a, nil
}
// APIHostResolver implementation
func (a APIHost) BaseRESTURL(_ context.Context) (*url.URL, error) {
return a.restURL, nil
}
func (a APIHost) GraphqlURL(_ context.Context) (*url.URL, error) {
return a.gqlURL, nil
}
func (a APIHost) UploadURL(_ context.Context) (*url.URL, error) {
return a.uploadURL, nil
}
func (a APIHost) RawURL(_ context.Context) (*url.URL, error) {
return a.rawURL, nil
}
func (a APIHost) AuthorizationServerURL(_ context.Context) (*url.URL, error) {
return a.authorizationServerURL, nil
}
func newDotcomHost() (APIHost, error) {
baseRestURL, err := url.Parse("https://api.github.com/")
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse dotcom REST URL: %w", err)
}
gqlURL, err := url.Parse("https://api.github.com/graphql")
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse dotcom GraphQL URL: %w", err)
}
uploadURL, err := url.Parse("https://uploads.github.com")
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse dotcom Upload URL: %w", err)
}
rawURL, err := url.Parse("https://raw.githubusercontent.com/")
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse dotcom Raw URL: %w", err)
}
// The authorization server for GitHub.com is at github.com/login/oauth, not api.github.com
authorizationServerURL, err := url.Parse("https://github.com/login/oauth")
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse dotcom Authorization Server URL: %w", err)
}
return APIHost{
restURL: baseRestURL,
gqlURL: gqlURL,
uploadURL: uploadURL,
rawURL: rawURL,
authorizationServerURL: authorizationServerURL,
}, nil
}
func newGHECHost(hostname string) (APIHost, error) {
u, err := url.Parse(hostname)
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse GHEC URL: %w", err)
}
// Unsecured GHEC would be an error
if u.Scheme == "http" {
return APIHost{}, fmt.Errorf("GHEC URL must be HTTPS")
}
restURL, err := url.Parse(fmt.Sprintf("https://api.%s/", u.Hostname()))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse GHEC REST URL: %w", err)
}
gqlURL, err := url.Parse(fmt.Sprintf("https://api.%s/graphql", u.Hostname()))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse GHEC GraphQL URL: %w", err)
}
uploadURL, err := url.Parse(fmt.Sprintf("https://uploads.%s/", u.Hostname()))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse GHEC Upload URL: %w", err)
}
rawURL, err := url.Parse(fmt.Sprintf("https://raw.%s/", u.Hostname()))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse GHEC Raw URL: %w", err)
}
authorizationServerURL, err := url.Parse(fmt.Sprintf("https://%s/login/oauth", u.Hostname()))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse GHEC Authorization Server URL: %w", err)
}
return APIHost{
restURL: restURL,
gqlURL: gqlURL,
uploadURL: uploadURL,
rawURL: rawURL,
authorizationServerURL: authorizationServerURL,
}, nil
}
func newGHESHost(hostname string) (APIHost, error) {
u, err := url.Parse(hostname)
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse GHES URL: %w", err)
}
restURL, err := url.Parse(fmt.Sprintf("%s://%s/api/v3/", u.Scheme, u.Hostname()))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse GHES REST URL: %w", err)
}
gqlURL, err := url.Parse(fmt.Sprintf("%s://%s/api/graphql", u.Scheme, u.Hostname()))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse GHES GraphQL URL: %w", err)
}
// Check if subdomain isolation is enabled
// See https://docs.github.com/en/enterprise-server@3.17/admin/configuring-settings/hardening-security-for-your-enterprise/enabling-subdomain-isolation#about-subdomain-isolation
hasSubdomainIsolation := checkSubdomainIsolation(u.Scheme, u.Hostname())
var uploadURL *url.URL
if hasSubdomainIsolation {
// With subdomain isolation: https://uploads.hostname/
uploadURL, err = url.Parse(fmt.Sprintf("%s://uploads.%s/", u.Scheme, u.Hostname()))
} else {
// Without subdomain isolation: https://hostname/api/uploads/
uploadURL, err = url.Parse(fmt.Sprintf("%s://%s/api/uploads/", u.Scheme, u.Hostname()))
}
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse GHES Upload URL: %w", err)
}
var rawURL *url.URL
if hasSubdomainIsolation {
// With subdomain isolation: https://raw.hostname/
rawURL, err = url.Parse(fmt.Sprintf("%s://raw.%s/", u.Scheme, u.Hostname()))
} else {
// Without subdomain isolation: https://hostname/raw/
rawURL, err = url.Parse(fmt.Sprintf("%s://%s/raw/", u.Scheme, u.Hostname()))
}
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse GHES Raw URL: %w", err)
}
authorizationServerURL, err := url.Parse(fmt.Sprintf("%s://%s/login/oauth", u.Scheme, u.Hostname()))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse GHES Authorization Server URL: %w", err)
}
return APIHost{
restURL: restURL,
gqlURL: gqlURL,
uploadURL: uploadURL,
rawURL: rawURL,
authorizationServerURL: authorizationServerURL,
}, nil
}
// checkSubdomainIsolation detects if GitHub Enterprise Server has subdomain isolation enabled
// by attempting to ping the raw.<host>/_ping endpoint on the subdomain. The raw subdomain must always exist for subdomain isolation.
func checkSubdomainIsolation(scheme, hostname string) bool {
subdomainURL := fmt.Sprintf("%s://raw.%s/_ping", scheme, hostname)
client := &http.Client{
Timeout: 5 * time.Second,
// Don't follow redirects - we just want to check if the endpoint exists
//nolint:revive // parameters are required by http.Client.CheckRedirect signature
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(subdomainURL)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
// Note that this does not handle ports yet, so development environments are out.
func parseAPIHost(s string) (APIHost, error) {
if s == "" {
return newDotcomHost()
}
u, err := url.Parse(s)
if err != nil {
return APIHost{}, fmt.Errorf("could not parse host as URL: %s", s)
}
if u.Scheme == "" {
return APIHost{}, fmt.Errorf("host must have a scheme (http or https): %s", s)
}
if u.Hostname() == "github.com" || strings.HasSuffix(u.Hostname(), ".github.com") {
return newDotcomHost()
}
if u.Hostname() == "github.localhost" || strings.HasSuffix(u.Hostname(), ".github.localhost") {
return newLocalDevHost(u.Scheme)
}
if u.Hostname() == "ghe.com" || strings.HasSuffix(u.Hostname(), ".ghe.com") {
return newGHECHost(s)
}
return newGHESHost(s)
}
// newLocalDevHost handles a local dotcom checkout served from github.localhost.
// Local dev mirrors production's split-host layout (api., uploads., raw.) but
// uses a plain hostname that doesn't match the github.com / ghe.com branches,
// so without this it falls through to newGHESHost and tries /api/v3 paths that
// the local stack doesn't serve.
func newLocalDevHost(scheme string) (APIHost, error) {
const root = "github.localhost"
restURL, err := url.Parse(fmt.Sprintf("%s://api.%s/", scheme, root))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse local dev REST URL: %w", err)
}
gqlURL, err := url.Parse(fmt.Sprintf("%s://api.%s/graphql", scheme, root))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse local dev GraphQL URL: %w", err)
}
uploadURL, err := url.Parse(fmt.Sprintf("%s://uploads.%s/", scheme, root))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse local dev Upload URL: %w", err)
}
rawURL, err := url.Parse(fmt.Sprintf("%s://raw.%s/", scheme, root))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse local dev Raw URL: %w", err)
}
authorizationServerURL, err := url.Parse(fmt.Sprintf("%s://%s/login/oauth", scheme, root))
if err != nil {
return APIHost{}, fmt.Errorf("failed to parse local dev Authorization Server URL: %w", err)
}
return APIHost{
restURL: restURL,
gqlURL: gqlURL,
uploadURL: uploadURL,
rawURL: rawURL,
authorizationServerURL: authorizationServerURL,
}, nil
}