Skip to content

Commit 9213bc6

Browse files
tonytrgmattdholloway
authored andcommitted
adding trailing slash to uploads url (#1947)
1 parent 2398ed0 commit 9213bc6

File tree

1 file changed

+192
-2
lines changed

1 file changed

+192
-2
lines changed

internal/ghmcp/server.go

Lines changed: 192 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,198 @@ func createFeatureChecker(enabledFeatures []string) inventory.FeatureFlagChecker
321321
for _, f := range enabledFeatures {
322322
featureSet[f] = true
323323
}
324-
return func(_ context.Context, flagName string) (bool, error) {
325-
return featureSet[flagName], nil
324+
325+
gqlURL, err := url.Parse("https://api.github.com/graphql")
326+
if err != nil {
327+
return apiHost{}, fmt.Errorf("failed to parse dotcom GraphQL URL: %w", err)
328+
}
329+
330+
uploadURL, err := url.Parse("https://uploads.github.com")
331+
if err != nil {
332+
return apiHost{}, fmt.Errorf("failed to parse dotcom Upload URL: %w", err)
333+
}
334+
335+
rawURL, err := url.Parse("https://raw.githubusercontent.com/")
336+
if err != nil {
337+
return apiHost{}, fmt.Errorf("failed to parse dotcom Raw URL: %w", err)
338+
}
339+
340+
return apiHost{
341+
baseRESTURL: baseRestURL,
342+
graphqlURL: gqlURL,
343+
uploadURL: uploadURL,
344+
rawURL: rawURL,
345+
}, nil
346+
}
347+
348+
func newGHECHost(hostname string) (apiHost, error) {
349+
u, err := url.Parse(hostname)
350+
if err != nil {
351+
return apiHost{}, fmt.Errorf("failed to parse GHEC URL: %w", err)
352+
}
353+
354+
// Unsecured GHEC would be an error
355+
if u.Scheme == "http" {
356+
return apiHost{}, fmt.Errorf("GHEC URL must be HTTPS")
357+
}
358+
359+
restURL, err := url.Parse(fmt.Sprintf("https://api.%s/", u.Hostname()))
360+
if err != nil {
361+
return apiHost{}, fmt.Errorf("failed to parse GHEC REST URL: %w", err)
362+
}
363+
364+
gqlURL, err := url.Parse(fmt.Sprintf("https://api.%s/graphql", u.Hostname()))
365+
if err != nil {
366+
return apiHost{}, fmt.Errorf("failed to parse GHEC GraphQL URL: %w", err)
367+
}
368+
369+
uploadURL, err := url.Parse(fmt.Sprintf("https://uploads.%s/", u.Hostname()))
370+
if err != nil {
371+
return apiHost{}, fmt.Errorf("failed to parse GHEC Upload URL: %w", err)
372+
}
373+
374+
rawURL, err := url.Parse(fmt.Sprintf("https://raw.%s/", u.Hostname()))
375+
if err != nil {
376+
return apiHost{}, fmt.Errorf("failed to parse GHEC Raw URL: %w", err)
377+
}
378+
379+
return apiHost{
380+
baseRESTURL: restURL,
381+
graphqlURL: gqlURL,
382+
uploadURL: uploadURL,
383+
rawURL: rawURL,
384+
}, nil
385+
}
386+
387+
func newGHESHost(hostname string) (apiHost, error) {
388+
u, err := url.Parse(hostname)
389+
if err != nil {
390+
return apiHost{}, fmt.Errorf("failed to parse GHES URL: %w", err)
391+
}
392+
393+
restURL, err := url.Parse(fmt.Sprintf("%s://%s/api/v3/", u.Scheme, u.Hostname()))
394+
if err != nil {
395+
return apiHost{}, fmt.Errorf("failed to parse GHES REST URL: %w", err)
396+
}
397+
398+
gqlURL, err := url.Parse(fmt.Sprintf("%s://%s/api/graphql", u.Scheme, u.Hostname()))
399+
if err != nil {
400+
return apiHost{}, fmt.Errorf("failed to parse GHES GraphQL URL: %w", err)
401+
}
402+
403+
// Check if subdomain isolation is enabled
404+
// See https://docs.github.com/en/enterprise-server@3.17/admin/configuring-settings/hardening-security-for-your-enterprise/enabling-subdomain-isolation#about-subdomain-isolation
405+
hasSubdomainIsolation := checkSubdomainIsolation(u.Scheme, u.Hostname())
406+
407+
var uploadURL *url.URL
408+
if hasSubdomainIsolation {
409+
// With subdomain isolation: https://uploads.hostname/
410+
uploadURL, err = url.Parse(fmt.Sprintf("%s://uploads.%s/", u.Scheme, u.Hostname()))
411+
} else {
412+
// Without subdomain isolation: https://hostname/api/uploads/
413+
uploadURL, err = url.Parse(fmt.Sprintf("%s://%s/api/uploads/", u.Scheme, u.Hostname()))
414+
}
415+
if err != nil {
416+
return apiHost{}, fmt.Errorf("failed to parse GHES Upload URL: %w", err)
417+
}
418+
419+
var rawURL *url.URL
420+
if hasSubdomainIsolation {
421+
// With subdomain isolation: https://raw.hostname/
422+
rawURL, err = url.Parse(fmt.Sprintf("%s://raw.%s/", u.Scheme, u.Hostname()))
423+
} else {
424+
// Without subdomain isolation: https://hostname/raw/
425+
rawURL, err = url.Parse(fmt.Sprintf("%s://%s/raw/", u.Scheme, u.Hostname()))
426+
}
427+
if err != nil {
428+
return apiHost{}, fmt.Errorf("failed to parse GHES Raw URL: %w", err)
429+
}
430+
431+
return apiHost{
432+
baseRESTURL: restURL,
433+
graphqlURL: gqlURL,
434+
uploadURL: uploadURL,
435+
rawURL: rawURL,
436+
}, nil
437+
}
438+
439+
// checkSubdomainIsolation detects if GitHub Enterprise Server has subdomain isolation enabled
440+
// by attempting to ping the raw.<host>/_ping endpoint on the subdomain. The raw subdomain must always exist for subdomain isolation.
441+
func checkSubdomainIsolation(scheme, hostname string) bool {
442+
subdomainURL := fmt.Sprintf("%s://raw.%s/_ping", scheme, hostname)
443+
444+
client := &http.Client{
445+
Timeout: 5 * time.Second,
446+
// Don't follow redirects - we just want to check if the endpoint exists
447+
//nolint:revive // parameters are required by http.Client.CheckRedirect signature
448+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
449+
return http.ErrUseLastResponse
450+
},
451+
}
452+
453+
resp, err := client.Get(subdomainURL)
454+
if err != nil {
455+
return false
456+
}
457+
defer resp.Body.Close()
458+
459+
return resp.StatusCode == http.StatusOK
460+
}
461+
462+
// Note that this does not handle ports yet, so development environments are out.
463+
func parseAPIHost(s string) (apiHost, error) {
464+
if s == "" {
465+
return newDotcomHost()
466+
}
467+
468+
u, err := url.Parse(s)
469+
if err != nil {
470+
return apiHost{}, fmt.Errorf("could not parse host as URL: %s", s)
471+
}
472+
473+
if u.Scheme == "" {
474+
return apiHost{}, fmt.Errorf("host must have a scheme (http or https): %s", s)
475+
}
476+
477+
if strings.HasSuffix(u.Hostname(), "github.com") {
478+
return newDotcomHost()
479+
}
480+
481+
if strings.HasSuffix(u.Hostname(), "ghe.com") {
482+
return newGHECHost(s)
483+
}
484+
485+
return newGHESHost(s)
486+
}
487+
488+
type userAgentTransport struct {
489+
transport http.RoundTripper
490+
agent string
491+
}
492+
493+
func (t *userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
494+
req = req.Clone(req.Context())
495+
req.Header.Set("User-Agent", t.agent)
496+
return t.transport.RoundTrip(req)
497+
}
498+
499+
type bearerAuthTransport struct {
500+
transport http.RoundTripper
501+
token string
502+
}
503+
504+
func (t *bearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
505+
req = req.Clone(req.Context())
506+
req.Header.Set("Authorization", "Bearer "+t.token)
507+
return t.transport.RoundTrip(req)
508+
}
509+
510+
func addGitHubAPIErrorToContext(next mcp.MethodHandler) mcp.MethodHandler {
511+
return func(ctx context.Context, method string, req mcp.Request) (result mcp.Result, err error) {
512+
// Ensure the context is cleared of any previous errors
513+
// as context isn't propagated through middleware
514+
ctx = errors.ContextWithGitHubErrors(ctx)
515+
return next(ctx, method, req)
326516
}
327517
}
328518

0 commit comments

Comments
 (0)