common.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. package handlers
  2. import (
  3. "context"
  4. "encoding/hex"
  5. "encoding/json"
  6. "encoding/xml"
  7. "fmt"
  8. "log/slog"
  9. "net/http"
  10. "strconv"
  11. "strings"
  12. "github.com/mk6i/open-oscar-server/state"
  13. "github.com/mk6i/open-oscar-server/wire"
  14. )
  15. // SessionRetriever provides methods to retrieve OSCAR sessions.
  16. type SessionRetriever interface {
  17. AllSessions() []*state.Session
  18. RetrieveSession(screenName state.IdentScreenName) *state.Session
  19. }
  20. // FeedbagRetriever provides methods to retrieve feedbag data.
  21. type FeedbagRetriever interface {
  22. RetrieveFeedbag(ctx context.Context, screenName state.IdentScreenName) ([]wire.FeedbagItem, error)
  23. RelationshipsByUser(ctx context.Context, screenName state.IdentScreenName) ([]state.IdentScreenName, error)
  24. }
  25. // CommonHandler provides shared utilities for all Web API handlers.
  26. type CommonHandler struct {
  27. Logger *slog.Logger
  28. }
  29. // BaseResponse is the standard response envelope for all Web API responses.
  30. // It supports both JSON and XML marshaling.
  31. type BaseResponse struct {
  32. XMLName xml.Name `xml:"response" json:"-"`
  33. Response ResponseBody `json:"response"`
  34. }
  35. // ResponseBody contains the status and data for API responses.
  36. type ResponseBody struct {
  37. StatusCode int `json:"statusCode" xml:"statusCode"`
  38. StatusText string `json:"statusText" xml:"statusText"`
  39. RequestID string `json:"requestId,omitempty" xml:"requestId,omitempty"`
  40. Data interface{} `json:"data,omitempty" xml:"data,omitempty"`
  41. }
  42. // ErrorResponse represents an error response with proper XML/JSON support.
  43. type ErrorResponse struct {
  44. XMLName xml.Name `xml:"response" json:"-"`
  45. Response struct {
  46. StatusCode int `json:"statusCode" xml:"statusCode"`
  47. StatusText string `json:"statusText" xml:"statusText"`
  48. } `json:"response" xml:"-"`
  49. // For XML responses, flatten the structure
  50. StatusCode int `json:"-" xml:"statusCode"`
  51. StatusText string `json:"-" xml:"statusText"`
  52. }
  53. // XMLMapResponse is a helper struct for converting map-based responses to XML
  54. type XMLMapResponse struct {
  55. XMLName xml.Name `xml:"response"`
  56. StatusCode int `xml:"statusCode"`
  57. StatusText string `xml:"statusText"`
  58. Data XMLData `xml:"data,omitempty"`
  59. }
  60. // XMLData wraps the data for XML responses
  61. type XMLData struct {
  62. // Auth response fields
  63. Token *XMLToken `xml:"token,omitempty"`
  64. LoginID string `xml:"loginId,omitempty"`
  65. ScreenName string `xml:"screenName,omitempty"`
  66. SessionSecret string `xml:"sessionSecret,omitempty"`
  67. HostTime int64 `xml:"hostTime,omitempty"`
  68. TokenExpiresIn int `xml:"tokenExpiresIn,omitempty"`
  69. // Generic fields for other responses
  70. AimSID string `xml:"aimsid,omitempty"`
  71. FetchURL string `xml:"fetchUrl,omitempty"`
  72. MsgID string `xml:"msgId,omitempty"`
  73. State string `xml:"state,omitempty"`
  74. // For any other data, we'll encode as string
  75. Raw string `xml:",chardata"`
  76. }
  77. // XMLToken represents the token structure in XML
  78. type XMLToken struct {
  79. A string `xml:"a"`
  80. ExpiresIn int `xml:"expiresIn"`
  81. }
  82. // requestIDFromRequest returns the Web AIM client request correlation id from the
  83. // "r" query parameter. JSONP callbacks require this echoed in response.requestId.
  84. func requestIDFromRequest(r *http.Request) string {
  85. if r == nil {
  86. return ""
  87. }
  88. return r.URL.Query().Get("r")
  89. }
  90. // attachRequestID copies the request's "r" parameter into BaseResponse.requestId
  91. // when the handler did not set one explicitly.
  92. func attachRequestID(r *http.Request, data interface{}) interface{} {
  93. id := requestIDFromRequest(r)
  94. if id == "" {
  95. return data
  96. }
  97. br, ok := data.(BaseResponse)
  98. if !ok || br.Response.RequestID != "" {
  99. return data
  100. }
  101. br.Response.RequestID = id
  102. return br
  103. }
  104. // SendResponse sends a response in the requested format (JSON, JSONP, XML, or AMF).
  105. // This is the centralized function that all handlers should use for responses.
  106. func SendResponse(w http.ResponseWriter, r *http.Request, data interface{}, logger *slog.Logger) {
  107. data = attachRequestID(r, data)
  108. // Check for format parameter (f for format or callback for JSONP)
  109. // First check URL query parameters
  110. format := strings.ToLower(r.URL.Query().Get("f"))
  111. callback := JSONPCallback(r)
  112. // If format not in URL query, check form values (for POST requests)
  113. if format == "" && r.Method == "POST" {
  114. _ = r.ParseForm()
  115. format = strings.ToLower(r.FormValue("f"))
  116. if callback == "" {
  117. callback = JSONPCallback(r)
  118. }
  119. }
  120. // Check for AMF format first
  121. if format == "amf" || format == "amf3" {
  122. SendAMF(w, r, data, logger)
  123. return
  124. }
  125. // Check Accept header for AMF
  126. accept := strings.ToLower(r.Header.Get("Accept"))
  127. if strings.Contains(accept, "application/x-amf") ||
  128. strings.Contains(accept, "application/amf") {
  129. SendAMF(w, r, data, logger)
  130. return
  131. }
  132. // If callback is provided, it's JSONP
  133. if callback != "" {
  134. SendJSONP(w, callback, data, logger)
  135. return
  136. }
  137. // Check for XML format
  138. if format == "xml" {
  139. SendXML(w, data, logger)
  140. return
  141. }
  142. // Default to JSON
  143. SendJSON(w, data, logger)
  144. }
  145. // SendError sends an error response in the appropriate format.
  146. func SendError(w http.ResponseWriter, statusCode int, message string) {
  147. // Try to detect format from Content-Type header if already set
  148. contentType := w.Header().Get("Content-Type")
  149. if strings.Contains(contentType, "amf") {
  150. SendAMFError(w, nil, statusCode, message, nil)
  151. } else if strings.Contains(contentType, "xml") {
  152. SendXMLError(w, statusCode, message)
  153. } else {
  154. SendJSONError(w, statusCode, message)
  155. }
  156. }
  157. // SendJSONError sends a JSON error response.
  158. func SendJSONError(w http.ResponseWriter, statusCode int, message string) {
  159. resp := ErrorResponse{}
  160. resp.Response.StatusCode = statusCode
  161. resp.Response.StatusText = message
  162. w.Header().Set("Content-Type", "application/json")
  163. w.WriteHeader(statusCode)
  164. _ = json.NewEncoder(w).Encode(resp)
  165. }
  166. // SendXMLError sends an XML error response.
  167. func SendXMLError(w http.ResponseWriter, statusCode int, message string) {
  168. resp := ErrorResponse{}
  169. resp.StatusCode = statusCode
  170. resp.StatusText = message
  171. w.Header().Set("Content-Type", "text/xml; charset=utf-8")
  172. w.WriteHeader(statusCode)
  173. // Write XML declaration and marshal the response
  174. xmlData, err := xml.Marshal(resp)
  175. if err != nil {
  176. // Fall back to simple text response
  177. http.Error(w, message, statusCode)
  178. return
  179. }
  180. xmlOutput := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>%s`, xmlData)
  181. _, _ = w.Write([]byte(xmlOutput))
  182. }
  183. // SendJSON sends a JSON response.
  184. func SendJSON(w http.ResponseWriter, data interface{}, logger *slog.Logger) {
  185. w.Header().Set("Content-Type", "application/json")
  186. body, err := json.Marshal(data)
  187. if err != nil {
  188. if logger != nil {
  189. logger.Error("failed to encode JSON response", "err", err.Error())
  190. }
  191. return
  192. }
  193. if logger != nil {
  194. logger.Debug("JSON response", "body", string(body))
  195. }
  196. if _, err := w.Write(body); err != nil && logger != nil {
  197. logger.Error("failed to write JSON response", "err", err.Error())
  198. }
  199. }
  200. // SendXML sends an XML response.
  201. func SendXML(w http.ResponseWriter, data interface{}, logger *slog.Logger) {
  202. w.Header().Set("Content-Type", "text/xml; charset=utf-8")
  203. // Convert BaseResponse with map data to a format XML can handle
  204. if baseResp, ok := data.(BaseResponse); ok {
  205. data = convertBaseResponseForXML(baseResp)
  206. }
  207. // Marshal the data
  208. xmlData, err := xml.Marshal(data)
  209. if err != nil {
  210. if logger != nil {
  211. logger.Error("failed to marshal XML response", "err", err.Error())
  212. }
  213. SendXMLError(w, http.StatusInternalServerError, "internal server error")
  214. return
  215. }
  216. // Write XML declaration and data
  217. xmlOutput := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>%s`, xmlData)
  218. // Set content length for proper response handling
  219. w.Header().Set("Content-Length", strconv.Itoa(len(xmlOutput)))
  220. _, _ = w.Write([]byte(xmlOutput))
  221. }
  222. // JSONPCallback returns the JSONP callback name from the request.
  223. // Web AIM clients use the "c" query parameter; other callers may use "callback".
  224. func JSONPCallback(r *http.Request) string {
  225. if callback := r.URL.Query().Get("c"); callback != "" {
  226. return callback
  227. }
  228. return r.URL.Query().Get("callback")
  229. }
  230. // SendJSONP sends a JSONP response with the specified callback.
  231. func SendJSONP(w http.ResponseWriter, callback string, data interface{}, logger *slog.Logger) {
  232. // Validate callback to prevent XSS
  233. if !IsValidCallback(callback) {
  234. SendJSONError(w, http.StatusBadRequest, "invalid callback parameter")
  235. return
  236. }
  237. jsonData, err := json.Marshal(data)
  238. if err != nil {
  239. if logger != nil {
  240. logger.Error("failed to marshal response", "err", err.Error())
  241. }
  242. SendJSONError(w, http.StatusInternalServerError, "internal server error")
  243. return
  244. }
  245. w.Header().Set("Content-Type", "application/javascript")
  246. _, _ = w.Write([]byte(callback))
  247. _, _ = w.Write([]byte("("))
  248. _, _ = w.Write(jsonData)
  249. _, _ = w.Write([]byte(");"))
  250. }
  251. // IsValidCallback validates a JSONP callback name to prevent XSS.
  252. func IsValidCallback(callback string) bool {
  253. if len(callback) == 0 || len(callback) > 100 {
  254. return false
  255. }
  256. // Allow alphanumeric, underscore, dollar sign, and dot (for namespace)
  257. for _, r := range callback {
  258. if (r < 'a' || r > 'z') &&
  259. (r < 'A' || r > 'Z') &&
  260. (r < '0' || r > '9') &&
  261. r != '_' && r != '$' && r != '.' {
  262. return false
  263. }
  264. }
  265. return true
  266. }
  267. // SendAMF sends an AMF response
  268. func SendAMF(w http.ResponseWriter, r *http.Request, data interface{}, logger *slog.Logger) {
  269. encoder := NewAMFEncoder(logger)
  270. version := DetectAMFVersion(r)
  271. amfData, err := encoder.EncodeAMF(data, version)
  272. if err != nil {
  273. if logger != nil {
  274. logger.Error("failed to encode AMF response",
  275. "err", err.Error(),
  276. "version", version,
  277. "dataType", fmt.Sprintf("%T", data))
  278. }
  279. // Fall back to JSON error
  280. SendJSONError(w, http.StatusInternalServerError, "AMF encoding failed")
  281. return
  282. }
  283. w.Header().Set("Content-Type", "application/x-amf")
  284. w.Header().Set("Content-Length", strconv.Itoa(len(amfData)))
  285. // Debug logging if enabled
  286. if logger != nil && logger.Enabled(context.TODO(), slog.LevelDebug) {
  287. hexPreview := ""
  288. if len(amfData) > 0 {
  289. previewLen := len(amfData)
  290. if previewLen > 64 {
  291. previewLen = 64
  292. }
  293. hexPreview = hex.EncodeToString(amfData[:previewLen])
  294. }
  295. logger.Debug("sending AMF response",
  296. "version", version,
  297. "size", len(amfData),
  298. "path", r.URL.Path,
  299. "hexPreview", hexPreview)
  300. }
  301. if _, err := w.Write(amfData); err != nil {
  302. if logger != nil {
  303. logger.Error("failed to write AMF response",
  304. "err", err.Error())
  305. }
  306. }
  307. }
  308. // convertBaseResponseForXML converts a BaseResponse with map data to XMLMapResponse
  309. func convertBaseResponseForXML(resp BaseResponse) XMLMapResponse {
  310. xmlResp := XMLMapResponse{
  311. StatusCode: resp.Response.StatusCode,
  312. StatusText: resp.Response.StatusText,
  313. }
  314. // Convert map data to XMLData struct
  315. if dataMap, ok := resp.Response.Data.(map[string]interface{}); ok {
  316. xmlData := XMLData{}
  317. // Handle auth response fields
  318. if tokenData, ok := dataMap["token"].(map[string]interface{}); ok {
  319. xmlData.Token = &XMLToken{}
  320. if a, ok := tokenData["a"].(string); ok {
  321. xmlData.Token.A = a
  322. }
  323. if expiresIn, ok := tokenData["expiresIn"].(int); ok {
  324. xmlData.Token.ExpiresIn = expiresIn
  325. }
  326. }
  327. if loginId, ok := dataMap["loginId"].(string); ok {
  328. xmlData.LoginID = loginId
  329. }
  330. if screenName, ok := dataMap["screenName"].(string); ok {
  331. xmlData.ScreenName = screenName
  332. }
  333. if sessionSecret, ok := dataMap["sessionSecret"].(string); ok {
  334. xmlData.SessionSecret = sessionSecret
  335. }
  336. if hostTime, ok := dataMap["hostTime"].(int64); ok {
  337. xmlData.HostTime = hostTime
  338. }
  339. if tokenExpiresIn, ok := dataMap["tokenExpiresIn"].(int); ok {
  340. xmlData.TokenExpiresIn = tokenExpiresIn
  341. }
  342. // Handle session response fields
  343. if aimsid, ok := dataMap["aimsid"].(string); ok {
  344. xmlData.AimSID = aimsid
  345. }
  346. if fetchUrl, ok := dataMap["fetchUrl"].(string); ok {
  347. xmlData.FetchURL = fetchUrl
  348. }
  349. // Handle message response fields
  350. if msgId, ok := dataMap["msgId"].(string); ok {
  351. xmlData.MsgID = msgId
  352. }
  353. if state, ok := dataMap["state"].(string); ok {
  354. xmlData.State = state
  355. }
  356. xmlResp.Data = xmlData
  357. }
  358. return xmlResp
  359. }
  360. // SendAMFError sends an AMF error response
  361. func SendAMFError(w http.ResponseWriter, r *http.Request, statusCode int, message string, logger *slog.Logger) {
  362. errorResp := ErrorResponse{}
  363. errorResp.Response.StatusCode = statusCode
  364. errorResp.Response.StatusText = message
  365. encoder := NewAMFEncoder(logger)
  366. version := DetectAMFVersion(r)
  367. amfData, err := encoder.EncodeAMF(errorResp, version)
  368. if err != nil {
  369. // If AMF encoding fails, fall back to JSON error
  370. SendJSONError(w, statusCode, message)
  371. return
  372. }
  373. w.Header().Set("Content-Type", "application/x-amf")
  374. w.Header().Set("Content-Length", strconv.Itoa(len(amfData)))
  375. w.WriteHeader(statusCode)
  376. _, _ = w.Write(amfData)
  377. }