Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
4.6 kB · 152 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153package spxrpc
import ( "context" "fmt" "io" "net/http" "net/url" "strings" "time"
"github.com/labstack/echo/v4" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" "stream.place/streamplace/pkg/aqhttp" "stream.place/streamplace/pkg/log")
const serviceAuthHeader = "X-Streamplace-Service-Auth"const serviceTokenLifetime = 5 * time.Minute
// ServiceIdentity represents an authenticated peer node in the same station.type ServiceIdentity struct { DID string}
type serviceAuthContextKeyType struct{}
var serviceAuthContextKey = serviceAuthContextKeyType{}
// GetServiceAuth returns the authenticated service identity from the context,// or nil if the request did not come from an authenticated peer node.func GetServiceAuth(ctx context.Context) *ServiceIdentity { v := ctx.Value(serviceAuthContextKey) if v == nil { return nil } identity, ok := v.(*ServiceIdentity) if !ok { return nil } return identity}
// ServiceAuthMiddleware checks for a service-to-service JWT in the// X-Streamplace-Service-Auth header. If present and valid, it populates// the context with the caller's ServiceIdentity. If absent or invalid,// the request passes through unchanged for the normal OAuth flow.func (s *Server) ServiceAuthMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { tokenStr := c.Request().Header.Get(serviceAuthHeader) if tokenStr == "" { return next(c) }
ctx := c.Request().Context() key := s.cli.ServiceAuthKey if key == nil { log.Warn(ctx, "service auth token present but no service auth key configured") return next(c) }
token, err := jwt.Parse([]byte(tokenStr), jwt.WithKey(jwa.HS256, key), jwt.WithValidate(true)) if err != nil { log.Warn(ctx, "invalid service auth token", "error", err) return next(c) }
issuer := token.Issuer() if issuer == "" { log.Warn(ctx, "service auth token missing issuer claim") return next(c) }
identity := &ServiceIdentity{DID: issuer} ctx = context.WithValue(ctx, serviceAuthContextKey, identity) c.SetRequest(c.Request().WithContext(ctx)) log.Log(ctx, "got authenticated service request", "service_did", issuer) return next(c) } }}
// CreateServiceToken generates a signed JWT for authenticating this node// to another node in the same station.func CreateServiceToken(key jwk.Key, serverDID string) (string, error) { now := time.Now() token, err := jwt.NewBuilder(). Issuer(serverDID). IssuedAt(now). Expiration(now.Add(serviceTokenLifetime)). Build() if err != nil { return "", fmt.Errorf("failed to build service token: %w", err) }
signed, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, key)) if err != nil { return "", fmt.Errorf("failed to sign service token: %w", err) }
return string(signed), nil}
// DIDWebToHost extracts the hostname from a did:web DID.func DIDWebToHost(did string) string { return strings.TrimPrefix(did, "did:web:")}
// ProxyServiceRequest proxies an XRPC request to a peer node, authenticating// with a service token. Returns the response body bytes or an echo.HTTPError.func (s *Server) ProxyServiceRequest(ctx context.Context, targetDID, httpMethod, xrpcMethod string, query url.Values, body io.Reader, contentType string) ([]byte, error) { token, err := CreateServiceToken(s.cli.ServiceAuthKey, s.cli.ServerDID()) if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, "error creating service token: "+err.Error()) }
u := url.URL{ Scheme: "https", Host: DIDWebToHost(targetDID), Path: "/xrpc/" + xrpcMethod, RawQuery: query.Encode(), }
log.Log(ctx, "proxying service request", "target", targetDID, "method", xrpcMethod)
req, err := http.NewRequestWithContext(ctx, httpMethod, u.String(), body) if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to construct proxy request: "+err.Error()) } if contentType != "" { req.Header.Set("Content-Type", contentType) } req.Header.Set(serviceAuthHeader, token)
resp, err := aqhttp.Client.Do(req) if err != nil { return nil, echo.NewHTTPError(http.StatusBadGateway, "error proxying to peer: "+err.Error()) } defer resp.Body.Close()
data, err := io.ReadAll(resp.Body) if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, "error reading peer response: "+err.Error()) } if resp.StatusCode != http.StatusOK { return nil, echo.NewHTTPError(resp.StatusCode, "peer error: "+string(data)) } return data, nil}