common.go 12 KB

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