oscar_bridge.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. switch err {
  149. case state.ErrNoWebAPISession:
  150. h.Logger.Warn("session not found", "aimsid", aimsid)
  151. h.sendError(w, r, http.StatusNotFound, "session not found")
  152. case state.ErrWebAPISessionExpired:
  153. h.Logger.Warn("session expired", "aimsid", aimsid)
  154. h.sendError(w, r, http.StatusGone, "session expired")
  155. default:
  156. h.Logger.Error("failed to get session", "error", err)
  157. h.sendError(w, r, http.StatusInternalServerError, "internal server error")
  158. }
  159. return
  160. }
  161. // Touch the session to update last access time
  162. _ = h.SessionManager.TouchSession(r.Context(), aimsid)
  163. // Check if session already has an OSCAR bridge
  164. if session.OSCARSession != nil {
  165. h.Logger.Info("session already has OSCAR bridge",
  166. "aimsid", aimsid,
  167. "screen_name", session.ScreenName)
  168. // Return existing connection details
  169. h.returnExistingBridge(w, r, session)
  170. return
  171. }
  172. // Parse optional parameters
  173. useSSL := h.parseBoolParam(params.Get("useSSL"))
  174. compress := h.parseBoolParam(params.Get("compress"))
  175. // Check SSL availability if requested
  176. if useSSL && !h.Config.IsSSLAvailable() {
  177. h.Logger.Warn("SSL requested but not available")
  178. h.sendError(w, r, http.StatusBadRequest, "SSL not available")
  179. return
  180. }
  181. // Create OSCAR authentication cookie
  182. cookie, err := h.createOSCARCookie(session)
  183. if err != nil {
  184. h.Logger.Error("failed to create OSCAR cookie",
  185. "error", err,
  186. "screen_name", session.ScreenName)
  187. h.sendError(w, r, http.StatusInternalServerError, "failed to create authentication cookie")
  188. return
  189. }
  190. // Get BOS server address
  191. var host string
  192. var port int
  193. if useSSL {
  194. host, port = h.Config.GetSSLBOSAddress()
  195. } else {
  196. host, port = h.Config.GetBOSAddress()
  197. }
  198. // Store bridge session in database
  199. if h.BridgeStore != nil {
  200. if err := h.BridgeStore.SaveBridgeSession(ctx, aimsid, cookie, host, port); err != nil {
  201. h.Logger.Error("failed to save bridge session",
  202. "error", err,
  203. "aimsid", aimsid)
  204. // Continue anyway - the bridge will work without persistence
  205. }
  206. }
  207. // Prepare response
  208. resp := h.buildResponse(host, port, cookie, useSSL, compress)
  209. // Send response in requested format
  210. h.sendResponse(w, r, resp)
  211. h.Logger.InfoContext(ctx, "OSCAR session bridge created",
  212. "aimsid", aimsid,
  213. "screen_name", session.ScreenName,
  214. "bos_host", host,
  215. "bos_port", port,
  216. "use_ssl", useSSL,
  217. "compress", compress)
  218. }
  219. // createOSCARCookie generates an OSCAR authentication cookie for the session.
  220. func (h *OSCARBridgeHandler) createOSCARCookie(session *state.WebAPISession) ([]byte, error) {
  221. // Create server cookie with session details
  222. serverCookie := state.ServerCookie{
  223. Service: wire.BOS, // Basic OSCAR Service
  224. ScreenName: session.ScreenName,
  225. ClientID: fmt.Sprintf("WebAPI-%s", session.ClientName),
  226. MultiConnFlag: 0, // Single connection
  227. }
  228. // Marshal the cookie to bytes
  229. buf := &bytes.Buffer{}
  230. if err := wire.MarshalBE(serverCookie, buf); err != nil {
  231. return nil, fmt.Errorf("failed to marshal server cookie: %w", err)
  232. }
  233. // Issue the cookie with HMAC signature
  234. cookie, err := h.CookieBaker.Issue(buf.Bytes())
  235. if err != nil {
  236. return nil, fmt.Errorf("failed to issue cookie: %w", err)
  237. }
  238. return cookie, nil
  239. }
  240. // hasOSCARBridgeCapability checks if the API key has permission to create OSCAR bridges.
  241. func (h *OSCARBridgeHandler) hasOSCARBridgeCapability(apiKey *state.WebAPIKey) bool {
  242. if len(apiKey.Capabilities) == 0 {
  243. return true // No restrictions if capabilities not specified
  244. }
  245. // Check if OSCAR bridge is explicitly enabled
  246. for _, cap := range apiKey.Capabilities {
  247. if cap == "oscar_bridge" || cap == "*" {
  248. return true
  249. }
  250. }
  251. return false
  252. }
  253. // parseBoolParam parses a boolean parameter from query string.
  254. func (h *OSCARBridgeHandler) parseBoolParam(value string) bool {
  255. value = strings.ToLower(value)
  256. return value == "true" || value == "1" || value == "yes"
  257. }
  258. // returnExistingBridge returns details for an existing OSCAR bridge.
  259. func (h *OSCARBridgeHandler) returnExistingBridge(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  260. // Retrieve existing bridge details from store
  261. if h.BridgeStore != nil {
  262. bridge, err := h.BridgeStore.GetBridgeSession(r.Context(), session.AimSID)
  263. if err == nil && bridge != nil {
  264. resp := h.buildResponse(bridge.BOSHost, bridge.BOSPort, bridge.OSCARCookie, bridge.UseSSL, false)
  265. h.sendResponse(w, r, resp)
  266. return
  267. }
  268. }
  269. // If we can't retrieve the bridge, return an error
  270. h.sendError(w, r, http.StatusInternalServerError, "failed to retrieve existing bridge")
  271. }
  272. // buildResponse constructs the response object.
  273. func (h *OSCARBridgeHandler) buildResponse(host string, port int, cookie []byte, useSSL, compress bool) *StartOSCARSessionResponse {
  274. resp := &StartOSCARSessionResponse{}
  275. resp.Response.StatusCode = 200
  276. resp.Response.StatusText = "OK"
  277. resp.Response.Data.Host = host
  278. resp.Response.Data.Port = port
  279. resp.Response.Data.Cookie = hex.EncodeToString(cookie) // Hex encode the cookie
  280. resp.Response.Data.UseSSL = useSSL
  281. // Add encryption info if SSL is used
  282. if useSSL {
  283. resp.Response.Data.Encryption = "TLS"
  284. }
  285. // Add compression info if requested (not implemented)
  286. if compress {
  287. resp.Response.Data.Compression = "none" // Compression not implemented
  288. }
  289. // Duplicate data for XML format
  290. resp.StatusCode = resp.Response.StatusCode
  291. resp.StatusText = resp.Response.StatusText
  292. resp.Data.Host = resp.Response.Data.Host
  293. resp.Data.Port = resp.Response.Data.Port
  294. resp.Data.Cookie = resp.Response.Data.Cookie
  295. resp.Data.UseSSL = resp.Response.Data.UseSSL
  296. resp.Data.Encryption = resp.Response.Data.Encryption
  297. resp.Data.Compression = resp.Response.Data.Compression
  298. return resp
  299. }
  300. // sendResponse sends the response in the requested format.
  301. func (h *OSCARBridgeHandler) sendResponse(w http.ResponseWriter, r *http.Request, resp *StartOSCARSessionResponse) {
  302. // Use the centralized SendResponse function which handles all formats
  303. SendResponse(w, r, resp, h.Logger)
  304. }
  305. // sendError sends an error response in the appropriate format.
  306. func (h *OSCARBridgeHandler) sendError(w http.ResponseWriter, r *http.Request, statusCode int, message string) {
  307. // SendError already detects format from Content-Type header
  308. SendError(w, statusCode, message)
  309. }