common.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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. )
  13. // BaseResponse is the standard response envelope for all Web API responses.
  14. // It supports both JSON and XML marshaling.
  15. type BaseResponse struct {
  16. Response ResponseBody `json:"response"`
  17. }
  18. // MarshalXML renders the envelope as the Web API's flat <response> root, where
  19. // JSON nests the same body under a "response" key. Reconciling the two shapes
  20. // here is what lets one struct describe a response in both formats.
  21. func (b BaseResponse) MarshalXML(e *xml.Encoder, _ xml.StartElement) error {
  22. return e.EncodeElement(b.Response, xml.StartElement{Name: xml.Name{Local: "response"}})
  23. }
  24. // ResponseBody contains the status and data for API responses.
  25. type ResponseBody struct {
  26. StatusCode int `json:"statusCode" xml:"statusCode"`
  27. StatusText string `json:"statusText" xml:"statusText"`
  28. RequestID string `json:"requestId,omitempty" xml:"requestId,omitempty"`
  29. // Data is never omitted. Every Web API method sends a data element even when
  30. // it carries no payload, and the client dereferences response.data on any
  31. // success; SendResponse substitutes an empty object when a handler sets none.
  32. Data interface{} `json:"data" xml:"data"`
  33. }
  34. // ErrorResponse represents an error response with proper XML/JSON support.
  35. type ErrorResponse struct {
  36. Response struct {
  37. StatusCode int `json:"statusCode" xml:"statusCode"`
  38. StatusText string `json:"statusText" xml:"statusText"`
  39. // Data carries an empty object for the same reason the JSONP error path
  40. // sends one: a client callback that reaches response.data on a failure
  41. // throws a TypeError when it is absent.
  42. Data interface{} `json:"data" xml:"data"`
  43. } `json:"response"`
  44. }
  45. // MarshalXML renders the error envelope with the same flat root as BaseResponse.
  46. func (e ErrorResponse) MarshalXML(enc *xml.Encoder, _ xml.StartElement) error {
  47. return enc.EncodeElement(e.Response, xml.StartElement{Name: xml.Name{Local: "response"}})
  48. }
  49. // newErrorResponse builds the error envelope every format shares.
  50. func newErrorResponse(statusCode int, message string) ErrorResponse {
  51. resp := ErrorResponse{}
  52. resp.Response.StatusCode = statusCode
  53. resp.Response.StatusText = message
  54. resp.Response.Data = struct{}{}
  55. return resp
  56. }
  57. // requestIDFromRequest returns the Web AIM client request correlation id from the
  58. // "r" query parameter. JSONP callbacks require this echoed in response.requestId.
  59. func requestIDFromRequest(r *http.Request) string {
  60. if r == nil {
  61. return ""
  62. }
  63. return r.URL.Query().Get("r")
  64. }
  65. // normalizeEnvelope fills in the envelope fields a handler does not set itself:
  66. // the request correlation id, and an empty data object for a response that
  67. // carries no payload. Both are things every encoder needs and none can infer —
  68. // and encoding/xml has no way to render a nil data at all.
  69. func normalizeEnvelope(r *http.Request, data interface{}) interface{} {
  70. br, ok := data.(BaseResponse)
  71. if !ok {
  72. return data
  73. }
  74. if br.Response.RequestID == "" {
  75. br.Response.RequestID = requestIDFromRequest(r)
  76. }
  77. if br.Response.Data == nil {
  78. br.Response.Data = struct{}{}
  79. }
  80. return br
  81. }
  82. // SendResponse sends a response in the requested format (JSON, JSONP, XML, or AMF).
  83. // This is the centralized function that all handlers should use for responses.
  84. func SendResponse(w http.ResponseWriter, r *http.Request, data interface{}, logger *slog.Logger) {
  85. data = normalizeEnvelope(r, data)
  86. // Check for format parameter (f for format or callback for JSONP)
  87. // First check URL query parameters
  88. format := strings.ToLower(r.URL.Query().Get("f"))
  89. callback := jsonpCallback(r)
  90. // If format not in URL query, check form values (for POST requests)
  91. if format == "" && r.Method == "POST" {
  92. _ = r.ParseForm()
  93. format = strings.ToLower(r.FormValue("f"))
  94. if callback == "" {
  95. callback = jsonpCallback(r)
  96. }
  97. }
  98. // Check for AMF format first
  99. if format == "amf" || format == "amf3" {
  100. sendAMF(w, r, data, logger)
  101. return
  102. }
  103. // Check Accept header for AMF
  104. accept := strings.ToLower(r.Header.Get("Accept"))
  105. if strings.Contains(accept, "application/x-amf") ||
  106. strings.Contains(accept, "application/amf") {
  107. sendAMF(w, r, data, logger)
  108. return
  109. }
  110. // If callback is provided, it's JSONP
  111. if callback != "" {
  112. sendJSONP(w, r, callback, data, logger)
  113. return
  114. }
  115. // Check for XML format
  116. if format == "xml" {
  117. sendXML(w, data, logger)
  118. return
  119. }
  120. // Default to JSON
  121. sendJSON(w, data, logger)
  122. }
  123. // SendError sends an error response in the format the client asked for.
  124. //
  125. // When the client requested JSONP, the error must be delivered as an executable
  126. // callback: a bare JSON body inside a <script> tag is a syntax error, which the
  127. // Web AIM client reports as the generic "Failed to load script tag, probably
  128. // malformed JS at that url" instead of the real statusText.
  129. func SendError(w http.ResponseWriter, r *http.Request, statusCode int, message string) {
  130. if callback := jsonpCallback(r); callback != "" && isValidCallback(callback) {
  131. sendJSONPError(w, r, callback, statusCode, message)
  132. return
  133. }
  134. // Try to detect format from Content-Type header if already set
  135. contentType := w.Header().Get("Content-Type")
  136. if strings.Contains(contentType, "amf") {
  137. sendAMFError(w, r, statusCode, message, nil)
  138. } else if strings.Contains(contentType, "xml") {
  139. sendXMLError(w, statusCode, message)
  140. } else {
  141. sendJSONError(w, statusCode, message)
  142. }
  143. }
  144. // sendJSONPError writes an error envelope wrapped in the client's JSONP callback.
  145. //
  146. // The HTTP status is deliberately left at 200: browsers do not execute the body
  147. // of a <script> tag that came back with a 4xx or 5xx, so a status-carrying JSONP
  148. // error never reaches the callback at all. The real status travels in the
  149. // envelope, which is where the Web AIM client reads it from regardless.
  150. func sendJSONPError(w http.ResponseWriter, r *http.Request, callback string, statusCode int, message string) {
  151. envelope := map[string]any{
  152. "statusCode": statusCode,
  153. "statusText": message,
  154. // Callbacks that reach response.data on a failure throw a TypeError when
  155. // it is absent, which aborts whatever the client was doing mid-startup.
  156. // Its XHR path fabricates an empty data for transport failures; JSONP
  157. // delivers the envelope verbatim, so the empty data has to come from here.
  158. "data": map[string]any{},
  159. }
  160. // The client indexes JSONP replies by response.requestId and discards any
  161. // reply that lacks one ("Request id is missing from the server response"),
  162. // leaving the request pending until it times out.
  163. if id := requestIDFromRequest(r); id != "" {
  164. envelope["requestId"] = id
  165. }
  166. body, err := json.Marshal(map[string]any{"response": envelope})
  167. if err != nil {
  168. sendJSONError(w, http.StatusInternalServerError, "internal server error")
  169. return
  170. }
  171. w.Header().Set("Content-Type", "application/javascript")
  172. _, _ = w.Write([]byte(callback))
  173. _, _ = w.Write([]byte("("))
  174. _, _ = w.Write(body)
  175. _, _ = w.Write([]byte(");"))
  176. }
  177. // sendJSONError sends a JSON error response.
  178. func sendJSONError(w http.ResponseWriter, statusCode int, message string) {
  179. resp := newErrorResponse(statusCode, message)
  180. w.Header().Set("Content-Type", "application/json")
  181. w.WriteHeader(statusCode)
  182. _ = json.NewEncoder(w).Encode(resp)
  183. }
  184. // sendXMLError sends an XML error response.
  185. func sendXMLError(w http.ResponseWriter, statusCode int, message string) {
  186. resp := newErrorResponse(statusCode, message)
  187. w.Header().Set("Content-Type", "text/xml; charset=utf-8")
  188. w.WriteHeader(statusCode)
  189. // Write XML declaration and marshal the response
  190. xmlData, err := xml.Marshal(resp)
  191. if err != nil {
  192. // Fall back to simple text response
  193. http.Error(w, message, statusCode)
  194. return
  195. }
  196. xmlOutput := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>%s`, xmlData)
  197. _, _ = w.Write([]byte(xmlOutput))
  198. }
  199. // sendJSON sends a JSON response.
  200. func sendJSON(w http.ResponseWriter, data interface{}, logger *slog.Logger) {
  201. w.Header().Set("Content-Type", "application/json")
  202. body, err := json.Marshal(data)
  203. if err != nil {
  204. if logger != nil {
  205. logger.Error("failed to encode JSON response", "err", err.Error())
  206. }
  207. return
  208. }
  209. if logger != nil {
  210. logger.Debug("JSON response", "body", string(body))
  211. }
  212. if _, err := w.Write(body); err != nil && logger != nil {
  213. logger.Error("failed to write JSON response", "err", err.Error())
  214. }
  215. }
  216. // sendXML sends an XML response.
  217. func sendXML(w http.ResponseWriter, data interface{}, logger *slog.Logger) {
  218. w.Header().Set("Content-Type", "text/xml; charset=utf-8")
  219. // Every payload is a struct whose xml tags name its elements, and the
  220. // envelope's MarshalXML renders the flat <response> root the Web API uses.
  221. xmlData, err := xml.Marshal(data)
  222. if err != nil {
  223. if logger != nil {
  224. logger.Error("failed to marshal XML response", "err", err.Error())
  225. }
  226. sendXMLError(w, http.StatusInternalServerError, "internal server error")
  227. return
  228. }
  229. // Write XML declaration and data
  230. xmlOutput := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>%s`, xmlData)
  231. // Set content length for proper response handling
  232. w.Header().Set("Content-Length", strconv.Itoa(len(xmlOutput)))
  233. _, _ = w.Write([]byte(xmlOutput))
  234. }
  235. // jsonpCallback returns the JSONP callback name from the request.
  236. // Web AIM clients use the "c" query parameter; other callers may use "callback".
  237. func jsonpCallback(r *http.Request) string {
  238. if r == nil {
  239. return ""
  240. }
  241. if callback := r.URL.Query().Get("c"); callback != "" {
  242. return callback
  243. }
  244. return r.URL.Query().Get("callback")
  245. }
  246. // sendJSONP sends a JSONP response with the specified callback.
  247. func sendJSONP(w http.ResponseWriter, r *http.Request, callback string, data interface{}, logger *slog.Logger) {
  248. // Validate callback to prevent XSS. This is the one error here that cannot be
  249. // delivered as JSONP: there is no callback name safe to write.
  250. if !isValidCallback(callback) {
  251. sendJSONError(w, http.StatusBadRequest, "invalid callback parameter")
  252. return
  253. }
  254. jsonData, err := json.Marshal(data)
  255. if err != nil {
  256. if logger != nil {
  257. logger.Error("failed to marshal response", "err", err.Error())
  258. }
  259. // The client is on the <script> transport, so the error has to be
  260. // executable JS for it to see anything but a load failure.
  261. sendJSONPError(w, r, callback, http.StatusInternalServerError, "internal server error")
  262. return
  263. }
  264. w.Header().Set("Content-Type", "application/javascript")
  265. _, _ = w.Write([]byte(callback))
  266. _, _ = w.Write([]byte("("))
  267. _, _ = w.Write(jsonData)
  268. _, _ = w.Write([]byte(");"))
  269. }
  270. // isValidCallback validates a JSONP callback name to prevent XSS.
  271. func isValidCallback(callback string) bool {
  272. if len(callback) == 0 || len(callback) > 100 {
  273. return false
  274. }
  275. // Allow alphanumeric, underscore, dollar sign, and dot (for namespace)
  276. for _, r := range callback {
  277. if (r < 'a' || r > 'z') &&
  278. (r < 'A' || r > 'Z') &&
  279. (r < '0' || r > '9') &&
  280. r != '_' && r != '$' && r != '.' {
  281. return false
  282. }
  283. }
  284. return true
  285. }
  286. // sendAMF sends an AMF response
  287. func sendAMF(w http.ResponseWriter, r *http.Request, data interface{}, logger *slog.Logger) {
  288. encoder := NewAMFEncoder(logger)
  289. version := DetectAMFVersion(r)
  290. amfData, err := encoder.EncodeAMF(data, version)
  291. if err != nil {
  292. if logger != nil {
  293. logger.Error("failed to encode AMF response",
  294. "err", err.Error(),
  295. "version", version,
  296. "dataType", fmt.Sprintf("%T", data))
  297. }
  298. // Fall back to JSON error
  299. sendJSONError(w, http.StatusInternalServerError, "AMF encoding failed")
  300. return
  301. }
  302. w.Header().Set("Content-Type", "application/x-amf")
  303. w.Header().Set("Content-Length", strconv.Itoa(len(amfData)))
  304. // Debug logging if enabled
  305. if logger != nil && logger.Enabled(context.TODO(), slog.LevelDebug) {
  306. hexPreview := ""
  307. if len(amfData) > 0 {
  308. previewLen := len(amfData)
  309. if previewLen > 64 {
  310. previewLen = 64
  311. }
  312. hexPreview = hex.EncodeToString(amfData[:previewLen])
  313. }
  314. logger.Debug("sending AMF response",
  315. "version", version,
  316. "size", len(amfData),
  317. "path", r.URL.Path,
  318. "hexPreview", hexPreview)
  319. }
  320. if _, err := w.Write(amfData); err != nil {
  321. if logger != nil {
  322. logger.Error("failed to write AMF response",
  323. "err", err.Error())
  324. }
  325. }
  326. }
  327. // sendAMFError sends an AMF error response
  328. func sendAMFError(w http.ResponseWriter, r *http.Request, statusCode int, message string, logger *slog.Logger) {
  329. errorResp := newErrorResponse(statusCode, message)
  330. encoder := NewAMFEncoder(logger)
  331. version := DetectAMFVersion(r)
  332. amfData, err := encoder.EncodeAMF(errorResp, version)
  333. if err != nil {
  334. // If AMF encoding fails, fall back to JSON error
  335. sendJSONError(w, statusCode, message)
  336. return
  337. }
  338. w.Header().Set("Content-Type", "application/x-amf")
  339. w.Header().Set("Content-Length", strconv.Itoa(len(amfData)))
  340. w.WriteHeader(statusCode)
  341. _, _ = w.Write(amfData)
  342. }