package network
import (
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"html/template"
"io"
"net/http"
"net/url"
"slices"
"strings"
"sync"
"time"
bolt "go.etcd.io/bbolt"
)
type GatewayMCP struct {
gateway *Gateway
db *bolt.DB
handler http.Handler
mu sync.Mutex
requests int
reset time.Time
}
func (m *GatewayMCP) Close() error { return m.db.Close() }
func (m *GatewayMCP) ServeHTTP(w http.ResponseWriter, r *http.Request) { m.handler.ServeHTTP(w, r) }
func isMCPPath(path string) bool {
return path == "/mcp" || strings.HasPrefix(path, "/oauth/") || strings.HasPrefix(path, "/.well-known/oauth-")
}
func (m *GatewayMCP) baseURL() string { return m.gateway.origin.Scheme + "://" + m.gateway.origin.Host }
func oauthError(w http.ResponseWriter, status int, code string) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(struct {
Error string `json:"error"`
}{code})
}
func (m *GatewayMCP) routes(mux *http.ServeMux) {
for _, path := range []string{"/.well-known/oauth-authorization-server", "/.well-known/oauth-authorization-server/mcp"} {
mux.HandleFunc("GET "+path, func(w http.ResponseWriter, r *http.Request) {
b := m.baseURL()
writeJSON(w, map[string]any{"issuer": b, "authorization_endpoint": b + "/oauth/authorize", "token_endpoint": b + "/oauth/token", "registration_endpoint": b + "/oauth/register", "revocation_endpoint": b + "/oauth/revoke", "response_types_supported": []string{"code"}, "grant_types_supported": []string{"authorization_code", "refresh_token"}, "token_endpoint_auth_methods_supported": []string{"none"}, "code_challenge_methods_supported": []string{"S256"}, "scopes_supported": []string{"agentpack"}})
})
}
for _, path := range []string{"/.well-known/oauth-protected-resource", "/.well-known/oauth-protected-resource/mcp"} {
mux.HandleFunc("GET "+path, func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]any{"resource": m.baseURL() + "/mcp", "authorization_servers": []string{m.baseURL()}, "scopes_supported": []string{"agentpack"}, "bearer_methods_supported": []string{"header"}, "resource_name": "Agentpack"})
})
}
mux.HandleFunc("POST /oauth/register", m.register)
mux.HandleFunc("GET /oauth/authorize", m.authorize)
mux.HandleFunc("POST /oauth/authorize", m.authorize)
mux.HandleFunc("POST /oauth/token", m.token)
mux.HandleFunc("POST /oauth/revoke", m.revoke)
}
func (m *GatewayMCP) limited(w http.ResponseWriter) bool {
m.mu.Lock()
defer m.mu.Unlock()
if time.Now().After(m.reset) {
m.requests = 0
m.reset = time.Now().Add(time.Minute)
}
m.requests++
if m.requests <= 120 {
return false
}
w.Header().Set("Retry-After", "60")
oauthError(w, 429, "rate_limited")
return true
}
func validOAuthRedirect(raw string) bool {
u, err := url.Parse(raw)
return err == nil && len(raw) <= 2048 && u.User == nil && u.Fragment == "" && u.Host != "" && (u.Scheme == "https" || u.Scheme == "http" && (u.Hostname() == "localhost" || u.Hostname() == "127.0.0.1" || u.Hostname() == "::1"))
}
func (m *GatewayMCP) register(w http.ResponseWriter, r *http.Request) {
if m.limited(w) {
return
}
r.Body = http.MaxBytesReader(w, r.Body, 16384)
var client oauthClient
decoder := json.NewDecoder(r.Body)
if decoder.Decode(&client) != nil || decoder.Decode(new(any)) != io.EOF || len(client.RedirectURIs) == 0 || len(client.RedirectURIs) > 10 || len(client.ClientName) > 100 || client.TokenAuth != "" && client.TokenAuth != "none" {
oauthError(w, 400, "invalid_client_metadata")
return
}
for _, uri := range client.RedirectURIs {
if !validOAuthRedirect(uri) {
oauthError(w, 400, "invalid_redirect_uri")
return
}
}
for _, grant := range client.GrantTypes {
if grant != "authorization_code" && grant != "refresh_token" {
oauthError(w, 400, "invalid_client_metadata")
return
}
}
for _, response := range client.ResponseTypes {
if response != "code" {
oauthError(w, 400, "invalid_client_metadata")
return
}
}
client.TokenAuth, client.GrantTypes, client.ResponseTypes = "none", []string{"authorization_code", "refresh_token"}, []string{"code"}
id := randomToken()
err := m.db.Update(func(tx *bolt.Tx) error {
if err := oauthPrune(tx); err != nil {
return err
}
return oauthPut(tx, id, oauthRecord{Kind: "client", Client: &client})
})
if err != nil {
oauthError(w, 503, "registration_unavailable")
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(struct {
ClientID string `json:"client_id"`
oauthClient
}{id, client})
}
var oauthConsent = template.Must(template.New("consent").Parse(`
Connect to AgentpackAgentpack connection
Connect {{.Name}}
This client will be able to read terminal output, type into sessions when control is released, start and stop agents, and install packages on your connected computers.
Return address: {{.Redirect}}
`))
func (m *GatewayMCP) authorize(w http.ResponseWriter, r *http.Request) {
if m.limited(w) {
return
}
if len(r.URL.RawQuery) > 8192 {
oauthError(w, 400, "invalid_request")
return
}
query := r.URL.Query()
var client oauthRecord
err := m.db.View(func(tx *bolt.Tx) error { var err error; client, err = oauthGet(tx, query.Get("client_id")); return err })
if err != nil || client.Kind != "client" || client.Client == nil {
oauthError(w, 400, "invalid_client")
return
}
redirect := query.Get("redirect_uri")
if !slices.Contains(client.Client.RedirectURIs, redirect) {
oauthError(w, 400, "invalid_redirect_uri")
return
}
challenge, err := base64.RawURLEncoding.DecodeString(query.Get("code_challenge"))
if err != nil || len(challenge) != 32 || query.Get("response_type") != "code" || query.Get("code_challenge_method") != "S256" || query.Get("scope") != "" && query.Get("scope") != "agentpack" || query.Get("resource") != "" && query.Get("resource") != m.baseURL()+"/mcp" {
oauthError(w, 400, "invalid_request")
return
}
if !m.gateway.auth.authenticated(r) {
http.Redirect(w, r, "/login?return_to="+url.QueryEscape(r.URL.RequestURI()), http.StatusSeeOther)
return
}
cookie, _ := r.Cookie("agentpack_session")
session := string(oauthKey(cookie.Value))
if r.Method == http.MethodGet {
token := randomToken()
err := m.db.Update(func(tx *bolt.Tx) error {
if err := oauthPrune(tx); err != nil {
return err
}
return oauthPut(tx, token, oauthRecord{Kind: "consent", Session: session, Query: r.URL.RawQuery, Expires: time.Now().Add(10 * time.Minute)})
})
if err != nil {
oauthError(w, 503, "authorization_unavailable")
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'self'; frame-ancestors 'none'; base-uri 'none'")
name := client.Client.ClientName
if name == "" {
name = "Your AI assistant"
}
_ = oauthConsent.Execute(w, struct{ Name, Redirect, Action, Token string }{name, redirect, r.URL.RequestURI(), token})
return
}
if r.Header.Get("Origin") != m.baseURL() {
oauthError(w, 403, "invalid_origin")
return
}
r.Body = http.MaxBytesReader(w, r.Body, 16384)
if r.ParseForm() != nil {
oauthError(w, 400, "invalid_request")
return
}
code := randomToken()
err = m.db.Update(func(tx *bolt.Tx) error {
consent, err := oauthGet(tx, r.PostForm.Get("consent"))
if err != nil || consent.Kind != "consent" || consent.Session != session || consent.Query != r.URL.RawQuery {
return errOAuthGrant
}
if err := tx.Bucket(oauthBucket).Delete(oauthKey(r.PostForm.Get("consent"))); err != nil {
return err
}
if r.PostForm.Get("decision") != "allow" {
return nil
}
return oauthPut(tx, code, oauthRecord{Kind: "code", ClientID: query.Get("client_id"), Redirect: redirect, Challenge: query.Get("code_challenge"), Resource: m.baseURL() + "/mcp", Expires: time.Now().Add(5 * time.Minute)})
})
if err != nil {
oauthError(w, 400, "invalid_consent")
return
}
u, _ := url.Parse(redirect)
params := u.Query()
params.Set("state", query.Get("state"))
if r.PostForm.Get("decision") == "allow" {
params.Set("code", code)
} else {
params.Set("error", "access_denied")
}
u.RawQuery = params.Encode()
http.Redirect(w, r, u.String(), http.StatusSeeOther)
}
func validVerifier(verifier string) bool {
if len(verifier) < 43 || len(verifier) > 128 {
return false
}
for _, char := range verifier {
if !(char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' || strings.ContainsRune("-._~", char)) {
return false
}
}
return true
}
func (m *GatewayMCP) token(w http.ResponseWriter, r *http.Request) {
if m.limited(w) {
return
}
r.Body = http.MaxBytesReader(w, r.Body, 16384)
if r.ParseForm() != nil {
oauthError(w, 400, "invalid_request")
return
}
form := r.PostForm
if form.Get("resource") != "" && form.Get("resource") != m.baseURL()+"/mcp" {
oauthError(w, 400, "invalid_target")
return
}
if form.Get("scope") != "" && form.Get("scope") != "agentpack" {
oauthError(w, 400, "invalid_scope")
return
}
if form.Get("client_secret") != "" || r.Header.Get("Authorization") != "" {
oauthError(w, 400, "invalid_client")
return
}
grant := form.Get("grant_type")
if grant != "authorization_code" && grant != "refresh_token" {
oauthError(w, 400, "unsupported_grant_type")
return
}
access, refresh := randomToken(), randomToken()
replayed := false
expires := 3600
err := m.db.Update(func(tx *bolt.Tx) error {
client, err := oauthGet(tx, form.Get("client_id"))
if err != nil || client.Kind != "client" {
return errOAuthGrant
}
var record oauthRecord
if grant == "authorization_code" {
record, err = oauthGet(tx, form.Get("code"))
if err != nil || record.Kind != "code" || record.ClientID != form.Get("client_id") || record.Redirect != form.Get("redirect_uri") || !validVerifier(form.Get("code_verifier")) {
return errOAuthGrant
}
hash := sha256.Sum256([]byte(form.Get("code_verifier")))
if subtle.ConstantTimeCompare([]byte(base64.RawURLEncoding.EncodeToString(hash[:])), []byte(record.Challenge)) != 1 {
return errOAuthGrant
}
if err := tx.Bucket(oauthBucket).Delete(oauthKey(form.Get("code"))); err != nil {
return err
}
record.Family, record.GrantExpires = randomToken(), time.Now().Add(30*24*time.Hour)
} else {
record, err = oauthGet(tx, form.Get("refresh_token"))
if err != nil || record.ClientID != form.Get("client_id") || record.Kind != "refresh" && record.Kind != "spent" {
return errOAuthGrant
}
if record.Kind == "spent" {
replayed = true
return oauthRevokeFamily(tx, record.Family)
}
record.Kind = "spent"
if err := oauthPut(tx, form.Get("refresh_token"), record); err != nil {
return err
}
}
if record.Resource != m.baseURL()+"/mcp" {
return errOAuthGrant
}
if err := oauthPrune(tx); err != nil {
return err
}
record.Kind, record.Expires = "access", time.Now().Add(time.Hour)
record.Challenge = ""
if record.Expires.After(record.GrantExpires) {
record.Expires = record.GrantExpires
}
expires = max(0, int(time.Until(record.Expires).Seconds()))
if err := oauthPut(tx, access, record); err != nil {
return err
}
record.Kind, record.Expires = "refresh", record.GrantExpires
return oauthPut(tx, refresh, record)
})
if err != nil || replayed {
oauthError(w, 400, "invalid_grant")
return
}
writeJSON(w, struct {
Access string `json:"access_token"`
Refresh string `json:"refresh_token"`
Type string `json:"token_type"`
Expires int `json:"expires_in"`
Scope string `json:"scope"`
}{access, refresh, "Bearer", expires, "agentpack"})
}
func (m *GatewayMCP) revoke(w http.ResponseWriter, r *http.Request) {
if m.limited(w) {
return
}
r.Body = http.MaxBytesReader(w, r.Body, 16384)
if r.ParseForm() != nil {
oauthError(w, 400, "invalid_request")
return
}
err := m.db.Update(func(tx *bolt.Tx) error {
record, err := oauthGet(tx, r.PostForm.Get("token"))
if err != nil {
return nil
}
if client := r.PostForm.Get("client_id"); client != "" && client != record.ClientID {
return nil
}
return oauthRevokeFamily(tx, record.Family)
})
if err != nil {
oauthError(w, 503, "revocation_unavailable")
return
}
w.WriteHeader(http.StatusOK)
}
func (m *GatewayMCP) authenticated(r *http.Request) bool {
value := r.Header.Get("Authorization")
if !strings.HasPrefix(value, "Bearer ") {
return false
}
valid := false
_ = m.db.View(func(tx *bolt.Tx) error {
record, err := oauthGet(tx, strings.TrimPrefix(value, "Bearer "))
valid = err == nil && record.Kind == "access" && record.Resource == m.baseURL()+"/mcp"
return nil
})
return valid
}