package handlers import ( "context" "encoding/hex" "encoding/json" "encoding/xml" "fmt" "log/slog" "net/http" "strconv" "strings" ) // BaseResponse is the standard response envelope for all Web API responses. // It supports both JSON and XML marshaling. type BaseResponse struct { Response ResponseBody `json:"response"` } // MarshalXML renders the envelope as the Web API's flat root, where // JSON nests the same body under a "response" key. Reconciling the two shapes // here is what lets one struct describe a response in both formats. func (b BaseResponse) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { return e.EncodeElement(b.Response, xml.StartElement{Name: xml.Name{Local: "response"}}) } // ResponseBody contains the status and data for API responses. type ResponseBody struct { StatusCode int `json:"statusCode" xml:"statusCode"` StatusText string `json:"statusText" xml:"statusText"` RequestID string `json:"requestId,omitempty" xml:"requestId,omitempty"` // Data is never omitted. Every Web API method sends a data element even when // it carries no payload, and the client dereferences response.data on any // success; SendResponse substitutes an empty object when a handler sets none. Data interface{} `json:"data" xml:"data"` } // ErrorResponse represents an error response with proper XML/JSON support. type ErrorResponse struct { Response struct { StatusCode int `json:"statusCode" xml:"statusCode"` // StatusDetailCode names which failure of a status code this is, e.g. 3011 // (bad password) under 330. Omitted when unset, which a client would // otherwise read as a detail code of its own. StatusDetailCode int `json:"statusDetailCode,omitempty" xml:"statusDetailCode,omitempty"` StatusText string `json:"statusText" xml:"statusText"` // Data carries an empty object for the same reason the JSONP error path // sends one: a client callback that reaches response.data on a failure // throws a TypeError when it is absent. Data interface{} `json:"data" xml:"data"` } `json:"response"` } // MarshalXML renders the error envelope with the same flat root as BaseResponse. func (e ErrorResponse) MarshalXML(enc *xml.Encoder, _ xml.StartElement) error { return enc.EncodeElement(e.Response, xml.StartElement{Name: xml.Name{Local: "response"}}) } // newErrorResponse builds the error envelope every format shares. func newErrorResponse(statusCode int, message string) ErrorResponse { return newErrorResponseDetail(statusCode, 0, message) } // newErrorResponseDetail builds the error envelope with a statusDetailCode. func newErrorResponseDetail(statusCode, detailCode int, message string) ErrorResponse { resp := ErrorResponse{} resp.Response.StatusCode = statusCode resp.Response.StatusDetailCode = detailCode resp.Response.StatusText = message resp.Response.Data = struct{}{} return resp } // requestFormat returns the format the client asked for. A POST sends "f" in // its body, as clientLogin does. func requestFormat(r *http.Request) string { format := strings.ToLower(r.URL.Query().Get("f")) if format == "" && r.Method == http.MethodPost { _ = r.ParseForm() format = strings.ToLower(r.FormValue("f")) } return format } // requestIDFromRequest returns the Web AIM client request correlation id from the // "r" query parameter. JSONP callbacks require this echoed in response.requestId. func requestIDFromRequest(r *http.Request) string { if r == nil { return "" } return r.URL.Query().Get("r") } // normalizeEnvelope fills in the envelope fields a handler does not set itself: // the request correlation id, and an empty data object for a response that // carries no payload. Both are things every encoder needs and none can infer — // and encoding/xml has no way to render a nil data at all. func normalizeEnvelope(r *http.Request, data interface{}) interface{} { br, ok := data.(BaseResponse) if !ok { return data } if br.Response.RequestID == "" { br.Response.RequestID = requestIDFromRequest(r) } if br.Response.Data == nil { br.Response.Data = struct{}{} } return br } // SendResponse sends a response in the requested format (JSON, JSONP, XML, or AMF). // This is the centralized function that all handlers should use for responses. func SendResponse(w http.ResponseWriter, r *http.Request, data interface{}, logger *slog.Logger) { data = normalizeEnvelope(r, data) format := requestFormat(r) callback := jsonpCallback(r) // Check for AMF format first if format == "amf" || format == "amf3" { sendAMF(w, r, data, logger) return } // Check Accept header for AMF accept := strings.ToLower(r.Header.Get("Accept")) if strings.Contains(accept, "application/x-amf") || strings.Contains(accept, "application/amf") { sendAMF(w, r, data, logger) return } // If callback is provided, it's JSONP if callback != "" { sendJSONP(w, r, callback, data, logger) return } // Check for XML format if format == "xml" { sendXML(w, data, logger) return } // Default to JSON sendJSON(w, data, logger) } // SendError sends an error response in the format the client asked for. // // When the client requested JSONP, the error must be delivered as an executable // callback: a bare JSON body inside a