common.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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. Data interface{} `json:"data,omitempty" xml:"data,omitempty"`
  40. }
  41. // ErrorResponse represents an error response with proper XML/JSON support.
  42. type ErrorResponse struct {
  43. XMLName xml.Name `xml:"response" json:"-"`
  44. Response struct {
  45. StatusCode int `json:"statusCode" xml:"statusCode"`
  46. StatusText string `json:"statusText" xml:"statusText"`
  47. } `json:"response" xml:"-"`
  48. // For XML responses, flatten the structure
  49. StatusCode int `json:"-" xml:"statusCode"`
  50. StatusText string `json:"-" xml:"statusText"`
  51. }
  52. // XMLMapResponse is a helper struct for converting map-based responses to XML
  53. type XMLMapResponse struct {
  54. XMLName xml.Name `xml:"response"`
  55. StatusCode int `xml:"statusCode"`
  56. StatusText string `xml:"statusText"`
  57. Data XMLData `xml:"data,omitempty"`
  58. }
  59. // XMLData wraps the data for XML responses
  60. type XMLData struct {
  61. // Auth response fields
  62. Token *XMLToken `xml:"token,omitempty"`
  63. LoginID string `xml:"loginId,omitempty"`
  64. ScreenName string `xml:"screenName,omitempty"`
  65. SessionSecret string `xml:"sessionSecret,omitempty"`
  66. HostTime int64 `xml:"hostTime,omitempty"`
  67. TokenExpiresIn int `xml:"tokenExpiresIn,omitempty"`
  68. // Generic fields for other responses
  69. AimSID string `xml:"aimsid,omitempty"`
  70. FetchURL string `xml:"fetchUrl,omitempty"`
  71. MsgID string `xml:"msgId,omitempty"`
  72. State string `xml:"state,omitempty"`
  73. // For any other data, we'll encode as string
  74. Raw string `xml:",chardata"`
  75. }
  76. // XMLToken represents the token structure in XML
  77. type XMLToken struct {
  78. A string `xml:"a"`
  79. ExpiresIn int `xml:"expiresIn"`
  80. }
  81. // SendResponse sends a response in the requested format (JSON, JSONP, XML, or AMF).
  82. // This is the centralized function that all handlers should use for responses.
  83. func SendResponse(w http.ResponseWriter, r *http.Request, data interface{}, logger *slog.Logger) {
  84. // Check for format parameter (f for format or callback for JSONP)
  85. // First check URL query parameters
  86. format := strings.ToLower(r.URL.Query().Get("f"))
  87. callback := r.URL.Query().Get("callback")
  88. // If format not in URL query, check form values (for POST requests)
  89. if format == "" && r.Method == "POST" {
  90. r.ParseForm()
  91. format = strings.ToLower(r.FormValue("f"))
  92. if callback == "" {
  93. callback = r.FormValue("callback")
  94. }
  95. }
  96. // Check for AMF format first
  97. if format == "amf" || format == "amf3" {
  98. SendAMF(w, r, data, logger)
  99. return
  100. }
  101. // Check Accept header for AMF
  102. accept := strings.ToLower(r.Header.Get("Accept"))
  103. if strings.Contains(accept, "application/x-amf") ||
  104. strings.Contains(accept, "application/amf") {
  105. SendAMF(w, r, data, logger)
  106. return
  107. }
  108. // If callback is provided, it's JSONP
  109. if callback != "" {
  110. SendJSONP(w, callback, data, logger)
  111. return
  112. }
  113. // Check for XML format
  114. if format == "xml" {
  115. SendXML(w, data, logger)
  116. return
  117. }
  118. // Default to JSON
  119. SendJSON(w, data, logger)
  120. }
  121. // SendError sends an error response in the appropriate format.
  122. func SendError(w http.ResponseWriter, statusCode int, message string) {
  123. // Try to detect format from Content-Type header if already set
  124. contentType := w.Header().Get("Content-Type")
  125. if strings.Contains(contentType, "amf") {
  126. SendAMFError(w, nil, statusCode, message, nil)
  127. } else if strings.Contains(contentType, "xml") {
  128. SendXMLError(w, statusCode, message)
  129. } else {
  130. SendJSONError(w, statusCode, message)
  131. }
  132. }
  133. // SendJSONError sends a JSON error response.
  134. func SendJSONError(w http.ResponseWriter, statusCode int, message string) {
  135. resp := ErrorResponse{}
  136. resp.Response.StatusCode = statusCode
  137. resp.Response.StatusText = message
  138. w.Header().Set("Content-Type", "application/json")
  139. w.WriteHeader(statusCode)
  140. json.NewEncoder(w).Encode(resp)
  141. }
  142. // SendXMLError sends an XML error response.
  143. func SendXMLError(w http.ResponseWriter, statusCode int, message string) {
  144. resp := ErrorResponse{}
  145. resp.StatusCode = statusCode
  146. resp.StatusText = message
  147. w.Header().Set("Content-Type", "text/xml; charset=utf-8")
  148. w.WriteHeader(statusCode)
  149. // Write XML declaration and marshal the response
  150. xmlData, err := xml.Marshal(resp)
  151. if err != nil {
  152. // Fall back to simple text response
  153. http.Error(w, message, statusCode)
  154. return
  155. }
  156. xmlOutput := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>%s`, xmlData)
  157. w.Write([]byte(xmlOutput))
  158. }
  159. // SendJSON sends a JSON response.
  160. func SendJSON(w http.ResponseWriter, data interface{}, logger *slog.Logger) {
  161. w.Header().Set("Content-Type", "application/json")
  162. if err := json.NewEncoder(w).Encode(data); err != nil {
  163. if logger != nil {
  164. logger.Error("failed to encode JSON response", "err", err.Error())
  165. }
  166. }
  167. }
  168. // SendXML sends an XML response.
  169. func SendXML(w http.ResponseWriter, data interface{}, logger *slog.Logger) {
  170. w.Header().Set("Content-Type", "text/xml; charset=utf-8")
  171. // Convert BaseResponse with map data to a format XML can handle
  172. if baseResp, ok := data.(BaseResponse); ok {
  173. data = convertBaseResponseForXML(baseResp)
  174. }
  175. // Marshal the data
  176. xmlData, err := xml.Marshal(data)
  177. if err != nil {
  178. if logger != nil {
  179. logger.Error("failed to marshal XML response", "err", err.Error())
  180. }
  181. SendXMLError(w, http.StatusInternalServerError, "internal server error")
  182. return
  183. }
  184. // Write XML declaration and data
  185. xmlOutput := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>%s`, xmlData)
  186. // Set content length for proper response handling
  187. w.Header().Set("Content-Length", strconv.Itoa(len(xmlOutput)))
  188. w.Write([]byte(xmlOutput))
  189. }
  190. // SendJSONP sends a JSONP response with the specified callback.
  191. func SendJSONP(w http.ResponseWriter, callback string, data interface{}, logger *slog.Logger) {
  192. // Validate callback to prevent XSS
  193. if !IsValidCallback(callback) {
  194. SendJSONError(w, http.StatusBadRequest, "invalid callback parameter")
  195. return
  196. }
  197. jsonData, err := json.Marshal(data)
  198. if err != nil {
  199. if logger != nil {
  200. logger.Error("failed to marshal response", "err", err.Error())
  201. }
  202. SendJSONError(w, http.StatusInternalServerError, "internal server error")
  203. return
  204. }
  205. w.Header().Set("Content-Type", "application/javascript")
  206. w.Write([]byte(callback))
  207. w.Write([]byte("("))
  208. w.Write(jsonData)
  209. w.Write([]byte(");"))
  210. }
  211. // IsValidCallback validates a JSONP callback name to prevent XSS.
  212. func IsValidCallback(callback string) bool {
  213. if len(callback) == 0 || len(callback) > 100 {
  214. return false
  215. }
  216. // Allow alphanumeric, underscore, dollar sign, and dot (for namespace)
  217. for _, r := range callback {
  218. if !((r >= 'a' && r <= 'z') ||
  219. (r >= 'A' && r <= 'Z') ||
  220. (r >= '0' && r <= '9') ||
  221. r == '_' || r == '$' || r == '.') {
  222. return false
  223. }
  224. }
  225. return true
  226. }
  227. // SendAMF sends an AMF response
  228. func SendAMF(w http.ResponseWriter, r *http.Request, data interface{}, logger *slog.Logger) {
  229. encoder := NewAMFEncoder(logger)
  230. version := DetectAMFVersion(r)
  231. amfData, err := encoder.EncodeAMF(data, version)
  232. if err != nil {
  233. if logger != nil {
  234. logger.Error("failed to encode AMF response",
  235. "err", err.Error(),
  236. "version", version,
  237. "dataType", fmt.Sprintf("%T", data))
  238. }
  239. // Fall back to JSON error
  240. SendJSONError(w, http.StatusInternalServerError, "AMF encoding failed")
  241. return
  242. }
  243. w.Header().Set("Content-Type", "application/x-amf")
  244. w.Header().Set("Content-Length", strconv.Itoa(len(amfData)))
  245. // Debug logging if enabled
  246. if logger != nil && logger.Enabled(context.TODO(), slog.LevelDebug) {
  247. hexPreview := ""
  248. if len(amfData) > 0 {
  249. previewLen := len(amfData)
  250. if previewLen > 64 {
  251. previewLen = 64
  252. }
  253. hexPreview = hex.EncodeToString(amfData[:previewLen])
  254. }
  255. logger.Debug("sending AMF response",
  256. "version", version,
  257. "size", len(amfData),
  258. "path", r.URL.Path,
  259. "hexPreview", hexPreview)
  260. }
  261. if _, err := w.Write(amfData); err != nil {
  262. if logger != nil {
  263. logger.Error("failed to write AMF response",
  264. "err", err.Error())
  265. }
  266. }
  267. }
  268. // convertBaseResponseForXML converts a BaseResponse with map data to XMLMapResponse
  269. func convertBaseResponseForXML(resp BaseResponse) XMLMapResponse {
  270. xmlResp := XMLMapResponse{
  271. StatusCode: resp.Response.StatusCode,
  272. StatusText: resp.Response.StatusText,
  273. }
  274. // Convert map data to XMLData struct
  275. if dataMap, ok := resp.Response.Data.(map[string]interface{}); ok {
  276. xmlData := XMLData{}
  277. // Handle auth response fields
  278. if tokenData, ok := dataMap["token"].(map[string]interface{}); ok {
  279. xmlData.Token = &XMLToken{}
  280. if a, ok := tokenData["a"].(string); ok {
  281. xmlData.Token.A = a
  282. }
  283. if expiresIn, ok := tokenData["expiresIn"].(int); ok {
  284. xmlData.Token.ExpiresIn = expiresIn
  285. }
  286. }
  287. if loginId, ok := dataMap["loginId"].(string); ok {
  288. xmlData.LoginID = loginId
  289. }
  290. if screenName, ok := dataMap["screenName"].(string); ok {
  291. xmlData.ScreenName = screenName
  292. }
  293. if sessionSecret, ok := dataMap["sessionSecret"].(string); ok {
  294. xmlData.SessionSecret = sessionSecret
  295. }
  296. if hostTime, ok := dataMap["hostTime"].(int64); ok {
  297. xmlData.HostTime = hostTime
  298. }
  299. if tokenExpiresIn, ok := dataMap["tokenExpiresIn"].(int); ok {
  300. xmlData.TokenExpiresIn = tokenExpiresIn
  301. }
  302. // Handle session response fields
  303. if aimsid, ok := dataMap["aimsid"].(string); ok {
  304. xmlData.AimSID = aimsid
  305. }
  306. if fetchUrl, ok := dataMap["fetchUrl"].(string); ok {
  307. xmlData.FetchURL = fetchUrl
  308. }
  309. // Handle message response fields
  310. if msgId, ok := dataMap["msgId"].(string); ok {
  311. xmlData.MsgID = msgId
  312. }
  313. if state, ok := dataMap["state"].(string); ok {
  314. xmlData.State = state
  315. }
  316. xmlResp.Data = xmlData
  317. }
  318. return xmlResp
  319. }
  320. // SendAMFError sends an AMF error response
  321. func SendAMFError(w http.ResponseWriter, r *http.Request, statusCode int, message string, logger *slog.Logger) {
  322. errorResp := ErrorResponse{}
  323. errorResp.Response.StatusCode = statusCode
  324. errorResp.Response.StatusText = message
  325. encoder := NewAMFEncoder(logger)
  326. version := DetectAMFVersion(r)
  327. amfData, err := encoder.EncodeAMF(errorResp, version)
  328. if err != nil {
  329. // If AMF encoding fails, fall back to JSON error
  330. SendJSONError(w, statusCode, message)
  331. return
  332. }
  333. w.Header().Set("Content-Type", "application/x-amf")
  334. w.Header().Set("Content-Length", strconv.Itoa(len(amfData)))
  335. w.WriteHeader(statusCode)
  336. w.Write(amfData)
  337. }