oscar_bridge.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. package handlers
  2. import (
  3. "encoding/base64"
  4. "encoding/xml"
  5. "log/slog"
  6. "net"
  7. "net/http"
  8. "strconv"
  9. "strings"
  10. "github.com/mk6i/open-oscar-server/config"
  11. "github.com/mk6i/open-oscar-server/server/webapi/middleware"
  12. "github.com/mk6i/open-oscar-server/state"
  13. )
  14. // OSCARBridgeHandler handles the handoff from the Web API's HTTP login to the
  15. // native OSCAR protocol, telling a client where to connect and what credential
  16. // to present.
  17. type OSCARBridgeHandler struct {
  18. OSCARAuthService OSCARAuthService
  19. Listener config.ListenerGroup
  20. Logger *slog.Logger
  21. }
  22. // OSCARAuthService verifies the credential a client presents to the bridge.
  23. type OSCARAuthService interface {
  24. CrackCookie(authCookie []byte) (state.ServerCookie, error)
  25. }
  26. // StartOSCARSessionResponse represents the response for startOSCARSession endpoint.
  27. type StartOSCARSessionResponse struct {
  28. Response struct {
  29. StatusCode int `json:"statusCode" xml:"statusCode"`
  30. StatusText string `json:"statusText" xml:"statusText"`
  31. Data struct {
  32. Host string `json:"host" xml:"host"`
  33. Port int `json:"port" xml:"port"`
  34. Cookie string `json:"cookie" xml:"cookie"`
  35. // TLSCertName is the certificate name the client verifies BOS against.
  36. // Omitted rather than sent empty: its absence means connect in the clear.
  37. TLSCertName string `json:"tlsCertName,omitempty" xml:"tlsCertName,omitempty"`
  38. } `json:"data" xml:"data"`
  39. } `json:"response"`
  40. }
  41. // MarshalXML renders the envelope with the same flat root as BaseResponse.
  42. func (s StartOSCARSessionResponse) MarshalXML(e *xml.Encoder, _ xml.StartElement) error {
  43. return e.EncodeElement(s.Response, xml.StartElement{Name: xml.Name{Local: "response"}})
  44. }
  45. // StartOSCARSession handles GET /aim/startOSCARSession requests, which hand a
  46. // client that authenticated over HTTP the address of a BOS server and the
  47. // cookie to sign on with. The token in "a" is the auth cookie clientLogin
  48. // minted, already what BOS expects, so it is handed straight back.
  49. //
  50. // The sig_sha256 the client computes over the query string is not checked: that
  51. // signature is keyed by HMAC(password, sessionSecret), and clientLogin keeps
  52. // neither past the response.
  53. func (h *OSCARBridgeHandler) StartOSCARSession(w http.ResponseWriter, r *http.Request) {
  54. ctx := r.Context()
  55. h.Logger.InfoContext(ctx, "startOSCARSession requested",
  56. "method", r.Method,
  57. "remote_addr", r.RemoteAddr,
  58. "user_agent", r.UserAgent())
  59. // Get API key info from context (set by auth middleware)
  60. apiKey, ok := ctx.Value(middleware.ContextKeyAPIKey).(*state.WebAPIKey)
  61. if !ok {
  62. h.Logger.Error("API key not found in context")
  63. SendError(w, r, http.StatusInternalServerError, "internal server error")
  64. return
  65. }
  66. // Verify that this API key has permission to create OSCAR sessions
  67. if !h.hasOSCARBridgeCapability(apiKey) {
  68. h.Logger.Warn("API key lacks OSCAR bridge capability",
  69. "dev_id", apiKey.DevID)
  70. SendError(w, r, http.StatusForbidden, "OSCAR bridge not enabled for this application")
  71. return
  72. }
  73. params := r.URL.Query()
  74. token := params.Get("a")
  75. if token == "" {
  76. h.Logger.Warn("missing authentication token")
  77. SendError(w, r, http.StatusUnauthorized, "authentication token required")
  78. return
  79. }
  80. rawCookie, err := base64.URLEncoding.DecodeString(strings.TrimSpace(token))
  81. if err != nil {
  82. h.Logger.WarnContext(ctx, "invalid authentication token (base64)", "err", err.Error())
  83. SendError(w, r, http.StatusUnauthorized, "invalid or expired token")
  84. return
  85. }
  86. cookie, err := h.OSCARAuthService.CrackCookie(rawCookie)
  87. if err != nil {
  88. h.Logger.WarnContext(ctx, "invalid authentication token", "err", err.Error())
  89. SendError(w, r, http.StatusUnauthorized, "invalid or expired token")
  90. return
  91. }
  92. // Encryption the server cannot provide degrades to a plaintext host, which a
  93. // client doing opportunistic encryption expects when no certificate is named.
  94. // The sign-on cookie then crosses the wire in the clear, so the downgrade is
  95. // logged rather than left to be inferred from the absent tlsCertName.
  96. useTLS := h.parseBoolParam(params.Get("useTLS"))
  97. endpoint := h.Listener.PlainEndpoint()
  98. if useTLS {
  99. ssl, ok := h.Listener.SSLEndpoint()
  100. if !ok {
  101. h.Logger.WarnContext(ctx, "TLS requested but no SSL listener is configured, advertising a plaintext BOS host",
  102. "screen_name", cookie.ScreenName)
  103. useTLS = false
  104. } else {
  105. endpoint = ssl
  106. }
  107. }
  108. host, portStr, err := net.SplitHostPort(endpoint.AdvertisedHost())
  109. if err != nil {
  110. h.Logger.ErrorContext(ctx, "unable to split advertised BOS host", "err", err.Error())
  111. SendError(w, r, http.StatusInternalServerError, "internal server error")
  112. return
  113. }
  114. port, _ := strconv.Atoi(portStr)
  115. resp := &StartOSCARSessionResponse{}
  116. resp.Response.StatusCode = 200
  117. resp.Response.StatusText = "OK"
  118. resp.Response.Data.Host = host
  119. resp.Response.Data.Port = port
  120. // Base64, the encoding the client decodes the cookie with.
  121. resp.Response.Data.Cookie = base64.StdEncoding.EncodeToString(rawCookie)
  122. if useTLS {
  123. // The advertised SSL host is the name the certificate is issued to.
  124. resp.Response.Data.TLSCertName = host
  125. }
  126. SendResponse(w, r, resp, h.Logger)
  127. h.Logger.InfoContext(ctx, "OSCAR session bridge created",
  128. "screen_name", cookie.ScreenName,
  129. "bos_host", host,
  130. "bos_port", port,
  131. "use_tls", useTLS)
  132. }
  133. // hasOSCARBridgeCapability checks if the API key has permission to create OSCAR bridges.
  134. func (h *OSCARBridgeHandler) hasOSCARBridgeCapability(apiKey *state.WebAPIKey) bool {
  135. if len(apiKey.Capabilities) == 0 {
  136. return true // No restrictions if capabilities not specified
  137. }
  138. // Check if OSCAR bridge is explicitly enabled
  139. for _, cap := range apiKey.Capabilities {
  140. if cap == "oscar_bridge" || cap == "*" {
  141. return true
  142. }
  143. }
  144. return false
  145. }
  146. // parseBoolParam parses a boolean parameter from query string.
  147. func (h *OSCARBridgeHandler) parseBoolParam(value string) bool {
  148. value = strings.ToLower(value)
  149. return value == "true" || value == "1" || value == "yes"
  150. }