oscar_bridge.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. package handlers
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/hex"
  6. "encoding/xml"
  7. "fmt"
  8. "log/slog"
  9. "net/http"
  10. "strings"
  11. "github.com/mk6i/open-oscar-server/server/webapi/middleware"
  12. "github.com/mk6i/open-oscar-server/state"
  13. "github.com/mk6i/open-oscar-server/wire"
  14. )
  15. // OSCARBridgeHandler handles Web API to OSCAR protocol bridging endpoints.
  16. // This handler is responsible for creating a bridge between web-based clients
  17. // and the native OSCAR protocol, allowing web clients to connect to OSCAR services.
  18. type OSCARBridgeHandler struct {
  19. SessionManager *state.WebAPISessionManager
  20. OSCARAuthService OSCARAuthService
  21. CookieBaker CookieBaker
  22. BridgeStore OSCARBridgeStore
  23. Config OSCARConfig
  24. Logger *slog.Logger
  25. }
  26. // OSCARAuthService defines methods needed for OSCAR authentication and session management.
  27. type OSCARAuthService interface {
  28. // RegisterBOSSession creates a new BOS (Basic OSCAR Service) session
  29. RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie, conf func(sess *state.Session)) (*state.SessionInstance, error)
  30. // RetrieveBOSSession retrieves an existing BOS session
  31. RetrieveBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
  32. // Signout ends an OSCAR session
  33. Signout(ctx context.Context, session *state.Session)
  34. }
  35. // CookieBaker issues and validates authentication cookies for OSCAR services.
  36. type CookieBaker interface {
  37. // Issue creates a new authentication cookie from the given payload
  38. Issue(data []byte) ([]byte, error)
  39. // Crack verifies and decodes an authentication cookie
  40. Crack(data []byte) ([]byte, error)
  41. }
  42. // OSCARBridgeStore manages the persistence of OSCAR bridge sessions.
  43. type OSCARBridgeStore interface {
  44. // SaveBridgeSession stores the mapping between WebAPI and OSCAR sessions
  45. SaveBridgeSession(ctx context.Context, webSessionID string, oscarCookie []byte, bosHost string, bosPort int) error
  46. // GetBridgeSession retrieves bridge session details
  47. GetBridgeSession(ctx context.Context, webSessionID string) (*state.OSCARBridgeSession, error)
  48. // DeleteBridgeSession removes a bridge session
  49. DeleteBridgeSession(ctx context.Context, webSessionID string) error
  50. }
  51. // OSCARConfig provides configuration for OSCAR services.
  52. type OSCARConfig interface {
  53. // GetBOSAddress returns the BOS server address for client connections
  54. GetBOSAddress() (host string, port int)
  55. // GetSSLBOSAddress returns the SSL-enabled BOS server address
  56. GetSSLBOSAddress() (host string, port int)
  57. // IsSSLAvailable checks if SSL is configured for BOS connections
  58. IsSSLAvailable() bool
  59. // IsAuthDisabled returns whether authentication is disabled
  60. IsAuthDisabled() bool
  61. }
  62. // StartOSCARSessionRequest represents the request parameters for startOSCARSession.
  63. type StartOSCARSessionRequest struct {
  64. AimSID string // WebAPI session ID
  65. UseSSL bool // Whether to use SSL for the OSCAR connection
  66. Compress bool // Whether to use compression (not implemented)
  67. }
  68. // StartOSCARSessionResponse represents the response for startOSCARSession endpoint.
  69. type StartOSCARSessionResponse struct {
  70. XMLName xml.Name `xml:"response" json:"-"`
  71. Response struct {
  72. StatusCode int `json:"statusCode" xml:"statusCode"`
  73. StatusText string `json:"statusText" xml:"statusText"`
  74. Data struct {
  75. Host string `json:"host" xml:"host"`
  76. Port int `json:"port" xml:"port"`
  77. Cookie string `json:"cookie" xml:"cookie"`
  78. UseSSL bool `json:"useSSL" xml:"useSSL"`
  79. Encryption string `json:"encryption,omitempty" xml:"encryption,omitempty"`
  80. Compression string `json:"compression,omitempty" xml:"compression,omitempty"`
  81. } `json:"data" xml:"data"`
  82. } `json:"response" xml:"-"`
  83. // For XML responses, flatten the structure
  84. StatusCode int `json:"-" xml:"statusCode"`
  85. StatusText string `json:"-" xml:"statusText"`
  86. Data struct {
  87. Host string `json:"-" xml:"host"`
  88. Port int `json:"-" xml:"port"`
  89. Cookie string `json:"-" xml:"cookie"`
  90. UseSSL bool `json:"-" xml:"useSSL"`
  91. Encryption string `json:"-" xml:"encryption,omitempty"`
  92. Compression string `json:"-" xml:"compression,omitempty"`
  93. } `json:"-" xml:"data"`
  94. }
  95. // StartOSCARSession handles GET /aim/startOSCARSession requests.
  96. // This endpoint creates a bridge between a WebAPI session and the native OSCAR protocol,
  97. // returning connection details that allow a web client to establish a direct OSCAR connection.
  98. //
  99. // The endpoint performs the following operations:
  100. // 1. Validates the WebAPI session
  101. // 2. Creates an OSCAR authentication cookie
  102. // 3. Optionally pre-registers a BOS session
  103. // 4. Returns connection details (host, port, cookie)
  104. //
  105. // Parameters:
  106. // - aimsid: The WebAPI session ID (required)
  107. // - useSSL: Whether to use SSL connection (optional, default: false)
  108. // - compress: Whether to use compression (optional, not implemented)
  109. // - f: Response format - "json" or "xml" (optional, default: "json")
  110. //
  111. // Returns:
  112. // - 200 OK: Successfully created OSCAR session bridge
  113. // - 400 Bad Request: Missing or invalid parameters
  114. // - 401 Unauthorized: Invalid or expired WebAPI session
  115. // - 500 Internal Server Error: Failed to create OSCAR session
  116. func (h *OSCARBridgeHandler) StartOSCARSession(w http.ResponseWriter, r *http.Request) {
  117. ctx := r.Context()
  118. // Log the request
  119. h.Logger.InfoContext(ctx, "startOSCARSession requested",
  120. "method", r.Method,
  121. "remote_addr", r.RemoteAddr,
  122. "user_agent", r.UserAgent())
  123. // Get API key info from context (set by auth middleware)
  124. apiKey, ok := ctx.Value(middleware.ContextKeyAPIKey).(*state.WebAPIKey)
  125. if !ok {
  126. h.Logger.Error("API key not found in context")
  127. h.sendError(w, r, http.StatusInternalServerError, "internal server error")
  128. return
  129. }
  130. // Verify that this API key has permission to create OSCAR sessions
  131. if !h.hasOSCARBridgeCapability(apiKey) {
  132. h.Logger.Warn("API key lacks OSCAR bridge capability",
  133. "dev_id", apiKey.DevID)
  134. h.sendError(w, r, http.StatusForbidden, "OSCAR bridge not enabled for this application")
  135. return
  136. }
  137. // Parse request parameters
  138. params := r.URL.Query()
  139. aimsid := params.Get("aimsid")
  140. if aimsid == "" {
  141. h.Logger.Warn("missing aimsid parameter")
  142. h.sendError(w, r, http.StatusBadRequest, "missing aimsid parameter")
  143. return
  144. }
  145. // Validate WebAPI session
  146. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  147. if err != nil {
  148. if err == state.ErrNoWebAPISession {
  149. h.Logger.Warn("session not found", "aimsid", aimsid)
  150. h.sendError(w, r, http.StatusNotFound, "session not found")
  151. } else if err == state.ErrWebAPISessionExpired {
  152. h.Logger.Warn("session expired", "aimsid", aimsid)
  153. h.sendError(w, r, http.StatusGone, "session expired")
  154. } else {
  155. h.Logger.Error("failed to get session", "error", err)
  156. h.sendError(w, r, http.StatusInternalServerError, "internal server error")
  157. }
  158. return
  159. }
  160. // Touch the session to update last access time
  161. h.SessionManager.TouchSession(r.Context(), aimsid)
  162. // Check if session already has an OSCAR bridge
  163. if session.OSCARSession != nil {
  164. h.Logger.Info("session already has OSCAR bridge",
  165. "aimsid", aimsid,
  166. "screen_name", session.ScreenName)
  167. // Return existing connection details
  168. h.returnExistingBridge(w, r, session)
  169. return
  170. }
  171. // Parse optional parameters
  172. useSSL := h.parseBoolParam(params.Get("useSSL"))
  173. compress := h.parseBoolParam(params.Get("compress"))
  174. // Check SSL availability if requested
  175. if useSSL && !h.Config.IsSSLAvailable() {
  176. h.Logger.Warn("SSL requested but not available")
  177. h.sendError(w, r, http.StatusBadRequest, "SSL not available")
  178. return
  179. }
  180. // Create OSCAR authentication cookie
  181. cookie, err := h.createOSCARCookie(session)
  182. if err != nil {
  183. h.Logger.Error("failed to create OSCAR cookie",
  184. "error", err,
  185. "screen_name", session.ScreenName)
  186. h.sendError(w, r, http.StatusInternalServerError, "failed to create authentication cookie")
  187. return
  188. }
  189. // Get BOS server address
  190. var host string
  191. var port int
  192. if useSSL {
  193. host, port = h.Config.GetSSLBOSAddress()
  194. } else {
  195. host, port = h.Config.GetBOSAddress()
  196. }
  197. // Store bridge session in database
  198. if h.BridgeStore != nil {
  199. if err := h.BridgeStore.SaveBridgeSession(ctx, aimsid, cookie, host, port); err != nil {
  200. h.Logger.Error("failed to save bridge session",
  201. "error", err,
  202. "aimsid", aimsid)
  203. // Continue anyway - the bridge will work without persistence
  204. }
  205. }
  206. // Prepare response
  207. resp := h.buildResponse(host, port, cookie, useSSL, compress)
  208. // Send response in requested format
  209. h.sendResponse(w, r, resp)
  210. h.Logger.InfoContext(ctx, "OSCAR session bridge created",
  211. "aimsid", aimsid,
  212. "screen_name", session.ScreenName,
  213. "bos_host", host,
  214. "bos_port", port,
  215. "use_ssl", useSSL,
  216. "compress", compress)
  217. }
  218. // createOSCARCookie generates an OSCAR authentication cookie for the session.
  219. func (h *OSCARBridgeHandler) createOSCARCookie(session *state.WebAPISession) ([]byte, error) {
  220. // Create server cookie with session details
  221. serverCookie := state.ServerCookie{
  222. Service: wire.BOS, // Basic OSCAR Service
  223. ScreenName: session.ScreenName,
  224. ClientID: fmt.Sprintf("WebAPI-%s", session.ClientName),
  225. MultiConnFlag: 0, // Single connection
  226. }
  227. // Marshal the cookie to bytes
  228. buf := &bytes.Buffer{}
  229. if err := wire.MarshalBE(serverCookie, buf); err != nil {
  230. return nil, fmt.Errorf("failed to marshal server cookie: %w", err)
  231. }
  232. // Issue the cookie with HMAC signature
  233. cookie, err := h.CookieBaker.Issue(buf.Bytes())
  234. if err != nil {
  235. return nil, fmt.Errorf("failed to issue cookie: %w", err)
  236. }
  237. return cookie, nil
  238. }
  239. // hasOSCARBridgeCapability checks if the API key has permission to create OSCAR bridges.
  240. func (h *OSCARBridgeHandler) hasOSCARBridgeCapability(apiKey *state.WebAPIKey) bool {
  241. if len(apiKey.Capabilities) == 0 {
  242. return true // No restrictions if capabilities not specified
  243. }
  244. // Check if OSCAR bridge is explicitly enabled
  245. for _, cap := range apiKey.Capabilities {
  246. if cap == "oscar_bridge" || cap == "*" {
  247. return true
  248. }
  249. }
  250. return false
  251. }
  252. // parseBoolParam parses a boolean parameter from query string.
  253. func (h *OSCARBridgeHandler) parseBoolParam(value string) bool {
  254. value = strings.ToLower(value)
  255. return value == "true" || value == "1" || value == "yes"
  256. }
  257. // returnExistingBridge returns details for an existing OSCAR bridge.
  258. func (h *OSCARBridgeHandler) returnExistingBridge(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  259. // Retrieve existing bridge details from store
  260. if h.BridgeStore != nil {
  261. bridge, err := h.BridgeStore.GetBridgeSession(r.Context(), session.AimSID)
  262. if err == nil && bridge != nil {
  263. resp := h.buildResponse(bridge.BOSHost, bridge.BOSPort, bridge.OSCARCookie, bridge.UseSSL, false)
  264. h.sendResponse(w, r, resp)
  265. return
  266. }
  267. }
  268. // If we can't retrieve the bridge, return an error
  269. h.sendError(w, r, http.StatusInternalServerError, "failed to retrieve existing bridge")
  270. }
  271. // buildResponse constructs the response object.
  272. func (h *OSCARBridgeHandler) buildResponse(host string, port int, cookie []byte, useSSL, compress bool) *StartOSCARSessionResponse {
  273. resp := &StartOSCARSessionResponse{}
  274. resp.Response.StatusCode = 200
  275. resp.Response.StatusText = "OK"
  276. resp.Response.Data.Host = host
  277. resp.Response.Data.Port = port
  278. resp.Response.Data.Cookie = hex.EncodeToString(cookie) // Hex encode the cookie
  279. resp.Response.Data.UseSSL = useSSL
  280. // Add encryption info if SSL is used
  281. if useSSL {
  282. resp.Response.Data.Encryption = "TLS"
  283. }
  284. // Add compression info if requested (not implemented)
  285. if compress {
  286. resp.Response.Data.Compression = "none" // Compression not implemented
  287. }
  288. // Duplicate data for XML format
  289. resp.StatusCode = resp.Response.StatusCode
  290. resp.StatusText = resp.Response.StatusText
  291. resp.Data.Host = resp.Response.Data.Host
  292. resp.Data.Port = resp.Response.Data.Port
  293. resp.Data.Cookie = resp.Response.Data.Cookie
  294. resp.Data.UseSSL = resp.Response.Data.UseSSL
  295. resp.Data.Encryption = resp.Response.Data.Encryption
  296. resp.Data.Compression = resp.Response.Data.Compression
  297. return resp
  298. }
  299. // sendResponse sends the response in the requested format.
  300. func (h *OSCARBridgeHandler) sendResponse(w http.ResponseWriter, r *http.Request, resp *StartOSCARSessionResponse) {
  301. // Use the centralized SendResponse function which handles all formats
  302. SendResponse(w, r, resp, h.Logger)
  303. }
  304. // sendError sends an error response in the appropriate format.
  305. func (h *OSCARBridgeHandler) sendError(w http.ResponseWriter, r *http.Request, statusCode int, message string) {
  306. // SendError already detects format from Content-Type header
  307. SendError(w, statusCode, message)
  308. }