amf_encoder.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. package handlers
  2. import (
  3. "fmt"
  4. "log/slog"
  5. "net/http"
  6. "reflect"
  7. "strings"
  8. "time"
  9. goAMF3 "github.com/breign/goAMF3"
  10. )
  11. // AMFVersion represents the AMF encoding version
  12. type AMFVersion int
  13. const (
  14. AMF3 AMFVersion = 3
  15. )
  16. // AMFEncoder handles AMF encoding operations for WebAPI responses
  17. type AMFEncoder struct {
  18. logger *slog.Logger
  19. }
  20. // NewAMFEncoder creates a new AMF encoder instance
  21. func NewAMFEncoder(logger *slog.Logger) *AMFEncoder {
  22. return &AMFEncoder{logger: logger}
  23. }
  24. // EncodeAMF encodes data to AMF3 format (only supported version)
  25. func (e *AMFEncoder) EncodeAMF(data interface{}, version AMFVersion) ([]byte, error) {
  26. // For AMF3, use goAMF3 which properly supports it
  27. // Convert to a regular map structure (no ECMAArray needed)
  28. amfData := e.toAMF3Compatible(data)
  29. // goAMF3 panics on nil values, ensure we sanitize
  30. sanitized := e.sanitizeForAMF3(amfData)
  31. encoded := goAMF3.EncodeAMF3(sanitized)
  32. return encoded, nil
  33. }
  34. // toAMF3Compatible converts Go types to AMF3-compatible format for goAMF3
  35. func (e *AMFEncoder) toAMF3Compatible(data interface{}) interface{} {
  36. if data == nil {
  37. return map[string]interface{}{}
  38. }
  39. // goAMF3 handles regular Go types well, just need to ensure maps are used
  40. // Don't use ECMAArray for AMF3 - just regular maps
  41. // Every response is a struct whose json tags name its fields, and
  42. // structToMap reflects over exactly those tags, so no response needs a case
  43. // of its own here. sanitizeForAMF3 handles the types goAMF3 cannot take —
  44. // notably the uint64 sequence numbers — on the way out.
  45. switch d := data.(type) {
  46. case BaseResponse:
  47. return e.baseResponseToMap(d)
  48. case ResponseBody:
  49. return e.responseBodyToMap(d)
  50. case ErrorResponse:
  51. return e.errorResponseToMap(d)
  52. default:
  53. // For other types, convert structs to maps
  54. return e.convertToMap(data)
  55. }
  56. }
  57. // sanitizeForAMF3 recursively removes nil values from the data structure
  58. // because goAMF3 panics when encountering nil values in maps.
  59. //
  60. // It runs on the output of toAMF3Compatible, so every struct and slice has
  61. // already been reduced to maps and []interface{}; only the leaf types goAMF3
  62. // cannot take are left to convert.
  63. func (e *AMFEncoder) sanitizeForAMF3(data interface{}) interface{} {
  64. if data == nil {
  65. return map[string]interface{}{}
  66. }
  67. switch v := data.(type) {
  68. case uint64:
  69. // goAMF3 can't handle uint64, convert to int
  70. return int(v)
  71. case uint32:
  72. // Convert all unsigned to signed for safety
  73. return int(v)
  74. case uint16:
  75. return int(v)
  76. case uint8:
  77. return int(v)
  78. case uint:
  79. return int(v)
  80. case map[string]interface{}:
  81. result := make(map[string]interface{})
  82. for key, val := range v {
  83. if val == nil {
  84. // For fields like 'data', replace with empty map
  85. // For other fields, skip them
  86. if key == "data" {
  87. result[key] = map[string]interface{}{}
  88. }
  89. continue
  90. }
  91. result[key] = e.sanitizeForAMF3(val)
  92. }
  93. return result
  94. case []interface{}:
  95. result := make([]interface{}, len(v))
  96. for i, item := range v {
  97. result[i] = e.sanitizeForAMF3(item)
  98. }
  99. return result
  100. default:
  101. // For other types, use reflection to check if it's a struct
  102. // and convert to map
  103. rv := reflect.ValueOf(data)
  104. if rv.Kind() == reflect.Struct {
  105. return e.structToMap(rv)
  106. }
  107. return data
  108. }
  109. }
  110. // baseResponseToMap converts BaseResponse to AMF3-compatible map
  111. func (e *AMFEncoder) baseResponseToMap(resp BaseResponse) map[string]interface{} {
  112. return map[string]interface{}{
  113. "response": e.responseBodyToMap(resp.Response),
  114. }
  115. }
  116. // responseBodyToMap converts ResponseBody to AMF3-compatible map
  117. func (e *AMFEncoder) responseBodyToMap(body ResponseBody) map[string]interface{} {
  118. m := map[string]interface{}{
  119. "statusCode": body.StatusCode,
  120. "statusText": body.StatusText,
  121. }
  122. if body.RequestID != "" {
  123. m["requestId"] = body.RequestID
  124. }
  125. if body.Data != nil {
  126. m["data"] = e.toAMF3Compatible(body.Data)
  127. } else {
  128. // For AMF3, always include data field even if empty to prevent truncation
  129. m["data"] = map[string]interface{}{}
  130. }
  131. return m
  132. }
  133. // errorResponseToMap converts ErrorResponse to AMF3-compatible map
  134. func (e *AMFEncoder) errorResponseToMap(err ErrorResponse) map[string]interface{} {
  135. m := map[string]interface{}{
  136. "statusCode": err.Response.StatusCode,
  137. "statusText": err.Response.StatusText,
  138. }
  139. // The client dereferences response.data on a failure too, so the error
  140. // envelope carries one in AMF as it does in every other format.
  141. if err.Response.Data != nil {
  142. m["data"] = e.toAMF3Compatible(err.Response.Data)
  143. } else {
  144. m["data"] = map[string]interface{}{}
  145. }
  146. return map[string]interface{}{"response": m}
  147. }
  148. // structToMap converts a struct to a map using JSON tags for AMF3
  149. func (e *AMFEncoder) structToMap(v reflect.Value) map[string]interface{} {
  150. result := make(map[string]interface{})
  151. t := v.Type()
  152. for i := 0; i < v.NumField(); i++ {
  153. field := t.Field(i)
  154. fieldValue := v.Field(i)
  155. // Skip unexported fields
  156. if !fieldValue.CanInterface() {
  157. continue
  158. }
  159. // Get JSON tag
  160. jsonTag := field.Tag.Get("json")
  161. if jsonTag == "-" {
  162. continue
  163. }
  164. // Parse JSON tag
  165. tagParts := strings.Split(jsonTag, ",")
  166. fieldName := tagParts[0]
  167. if fieldName == "" {
  168. fieldName = field.Name
  169. }
  170. // Check for omitempty
  171. omitEmpty := false
  172. for _, part := range tagParts[1:] {
  173. if part == "omitempty" {
  174. omitEmpty = true
  175. break
  176. }
  177. }
  178. // Skip if omitempty and value is zero
  179. if omitEmpty && e.isZeroValue(fieldValue) {
  180. continue
  181. }
  182. // Get field value and convert recursively
  183. fieldData := fieldValue.Interface()
  184. result[fieldName] = e.toAMF3Compatible(fieldData)
  185. }
  186. return result
  187. }
  188. // mapToAMFMap converts a Go map to an AMF3-compatible map
  189. func (e *AMFEncoder) mapToAMFMap(v reflect.Value) map[string]interface{} {
  190. result := make(map[string]interface{})
  191. for _, key := range v.MapKeys() {
  192. // Convert key to string (AMF only supports string keys)
  193. keyStr := fmt.Sprintf("%v", key.Interface())
  194. value := v.MapIndex(key)
  195. if value.CanInterface() {
  196. result[keyStr] = e.toAMF3Compatible(value.Interface())
  197. }
  198. }
  199. return result
  200. }
  201. // convertToMap converts any data to a map structure for AMF3
  202. func (e *AMFEncoder) convertToMap(data interface{}) interface{} {
  203. if data == nil {
  204. // For AMF3, return empty map instead of nil to avoid truncation
  205. return map[string]interface{}{}
  206. }
  207. // If already a map, return as-is (even if empty)
  208. if m, ok := data.(map[string]interface{}); ok {
  209. if m == nil {
  210. return map[string]interface{}{}
  211. }
  212. return m
  213. }
  214. v := reflect.ValueOf(data)
  215. // Handle pointers
  216. if v.Kind() == reflect.Pointer {
  217. if v.IsNil() {
  218. return nil
  219. }
  220. v = v.Elem()
  221. data = v.Interface()
  222. }
  223. // Handle different types
  224. switch v.Kind() {
  225. case reflect.Struct:
  226. return e.structToMap(v)
  227. case reflect.Map:
  228. return e.mapToAMFMap(v)
  229. case reflect.Slice, reflect.Array:
  230. result := make([]interface{}, v.Len())
  231. for i := 0; i < v.Len(); i++ {
  232. elem := v.Index(i)
  233. if elem.CanInterface() {
  234. result[i] = e.convertToMap(elem.Interface())
  235. }
  236. }
  237. return result
  238. default:
  239. // For basic types, return as-is
  240. return data
  241. }
  242. }
  243. // isZeroValue checks if a reflect.Value is a zero value
  244. func (e *AMFEncoder) isZeroValue(v reflect.Value) bool {
  245. switch v.Kind() {
  246. case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
  247. return v.Len() == 0
  248. case reflect.Bool:
  249. return !v.Bool()
  250. case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
  251. return v.Int() == 0
  252. case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
  253. return v.Uint() == 0
  254. case reflect.Float32, reflect.Float64:
  255. return v.Float() == 0
  256. case reflect.Interface, reflect.Pointer:
  257. return v.IsNil()
  258. case reflect.Struct:
  259. // For time.Time, check if it's zero
  260. if t, ok := v.Interface().(time.Time); ok {
  261. return t.IsZero()
  262. }
  263. // For other structs, we can't easily determine zero value
  264. return false
  265. }
  266. return false
  267. }
  268. // DetectAMFVersion determines which AMF version to use based on the request
  269. func DetectAMFVersion(r *http.Request) AMFVersion {
  270. if r == nil {
  271. return AMF3
  272. }
  273. // Check query parameter first (highest priority)
  274. format := strings.ToLower(r.URL.Query().Get("f"))
  275. switch format {
  276. case "amf3":
  277. return AMF3
  278. case "amf":
  279. // Default to AMF3 for modern clients (Gromit expects AMF3)
  280. return AMF3
  281. }
  282. // Check Accept header for version hint
  283. accept := r.Header.Get("Accept")
  284. if strings.Contains(accept, "amf3") || strings.Contains(accept, "AMF3") {
  285. return AMF3
  286. }
  287. if strings.Contains(accept, "amf") || strings.Contains(accept, "AMF") {
  288. return AMF3 // Default to AMF3 for AMF requests
  289. }
  290. // Check Content-Type header (for POST requests)
  291. contentType := r.Header.Get("Content-Type")
  292. if strings.Contains(contentType, "amf3") || strings.Contains(contentType, "AMF3") {
  293. return AMF3
  294. }
  295. if strings.Contains(contentType, "amf") || strings.Contains(contentType, "AMF") {
  296. return AMF3 // Default to AMF3 for AMF requests
  297. }
  298. // Default to AMF3 for modern clients
  299. return AMF3
  300. }