oscar_bridge.go 12 KB

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