Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions internal/flashduty/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
Expand Down Expand Up @@ -327,18 +328,37 @@ func httpContextFunc(ctx context.Context, r *http.Request, defaultBaseURL string
// requestOrigin returns the scheme and host the client used to reach this
// server, honoring X-Forwarded-Proto/X-Forwarded-Host set by a reverse proxy.
func requestOrigin(r *http.Request) string {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
if proto := firstHeaderValue(r, "X-Forwarded-Proto"); proto != "" {
scheme = proto
}
host := r.Host
if fwdHost := firstHeaderValue(r, "X-Forwarded-Host"); fwdHost != "" {
host = fwdHost
}
return scheme + "://" + host
return requestScheme(r, host) + "://" + host
}

// requestScheme trusts X-Forwarded-Proto when it names http or https. Without
// it (a TLS-terminating load balancer that does not set the header), only a
// loopback host is plain http: MCP clients require https for any other
// authorization target, so the public resource is https.
func requestScheme(r *http.Request, host string) string {
switch proto := strings.ToLower(firstHeaderValue(r, "X-Forwarded-Proto")); proto {
case "http", "https":
return proto
}
if r.TLS != nil {
return "https"
}
hostname := host
if h, _, err := net.SplitHostPort(host); err == nil {
hostname = h
}
hostname = strings.Trim(hostname, "[]")
if hostname == "localhost" {
return "http"
}
if ip := net.ParseIP(hostname); ip != nil && ip.IsLoopback() {
return "http"
}
return "https"
}

// firstHeaderValue returns the first entry of a possibly comma-separated
Expand Down
31 changes: 25 additions & 6 deletions internal/flashduty/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,28 @@ func TestHTTPMux_MissingCredentialChallenges(t *testing.T) {
want string
}{
{
name: "direct",
// A TLS-terminating load balancer that sets no X-Forwarded-Proto.
name: "no forwarded proto",
path: "/mcp",
want: `Bearer resource_metadata="http://mcp.example.com/.well-known/oauth-protected-resource/mcp"`,
want: `Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"`,
},
{
name: "loopback",
path: "/mcp",
headers: map[string]string{"X-Forwarded-Host": "127.0.0.1:11310"},
want: `Bearer resource_metadata="http://127.0.0.1:11310/.well-known/oauth-protected-resource/mcp"`,
},
{
name: "unknown forwarded proto",
path: "/mcp",
headers: map[string]string{"X-Forwarded-Proto": "gopher"},
want: `Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"`,
},
{
name: "forwarded http",
path: "/mcp",
headers: map[string]string{"X-Forwarded-Proto": "http", "X-Forwarded-Host": "mcp.internal"},
want: `Bearer resource_metadata="http://mcp.internal/.well-known/oauth-protected-resource/mcp"`,
},
{
name: "behind proxy",
Expand All @@ -93,7 +112,7 @@ func TestHTTPMux_MissingCredentialChallenges(t *testing.T) {
{
name: "legacy path",
path: "/flashduty",
want: `Bearer resource_metadata="http://mcp.example.com/.well-known/oauth-protected-resource/flashduty"`,
want: `Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/flashduty"`,
},
}
for _, tt := range tests {
Expand Down Expand Up @@ -161,17 +180,17 @@ func TestHTTPMux_ProtectedResourceMetadata(t *testing.T) {
{
name: "path inserted",
path: "/.well-known/oauth-protected-resource/mcp",
wantResource: "http://mcp.example.com/mcp",
wantResource: "https://mcp.example.com/mcp",
},
{
name: "root describes /mcp",
path: "/.well-known/oauth-protected-resource",
wantResource: "http://mcp.example.com/mcp",
wantResource: "https://mcp.example.com/mcp",
},
{
name: "legacy path",
path: "/.well-known/oauth-protected-resource/flashduty",
wantResource: "http://mcp.example.com/flashduty",
wantResource: "https://mcp.example.com/flashduty",
},
{
name: "behind proxy",
Expand Down
Loading