4
0

amf_encoder.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  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. // goAMF3 writes nothing for a value it cannot encode, which truncates the
  102. // object mid-key, so pointers and structs are reduced to maps first.
  103. rv := reflect.ValueOf(data)
  104. if rv.Kind() == reflect.Pointer {
  105. if rv.IsNil() {
  106. return map[string]interface{}{}
  107. }
  108. rv = rv.Elem()
  109. }
  110. if rv.Kind() == reflect.Struct {
  111. return e.sanitizeForAMF3(e.structToMap(rv))
  112. }
  113. return rv.Interface()
  114. }
  115. }
  116. // baseResponseToMap converts BaseResponse to AMF3-compatible map
  117. func (e *AMFEncoder) baseResponseToMap(resp BaseResponse) map[string]interface{} {
  118. return map[string]interface{}{
  119. "response": e.responseBodyToMap(resp.Response),
  120. }
  121. }
  122. // responseBodyToMap converts ResponseBody to AMF3-compatible map
  123. func (e *AMFEncoder) responseBodyToMap(body ResponseBody) map[string]interface{} {
  124. m := map[string]interface{}{
  125. "statusCode": body.StatusCode,
  126. "statusText": body.StatusText,
  127. }
  128. if body.RequestID != "" {
  129. m["requestId"] = body.RequestID
  130. }
  131. if body.Data != nil {
  132. m["data"] = e.toAMF3Compatible(body.Data)
  133. } else {
  134. // For AMF3, always include data field even if empty to prevent truncation
  135. m["data"] = map[string]interface{}{}
  136. }
  137. return m
  138. }
  139. // errorResponseToMap converts ErrorResponse to AMF3-compatible map
  140. func (e *AMFEncoder) errorResponseToMap(err ErrorResponse) map[string]interface{} {
  141. m := map[string]interface{}{
  142. "statusCode": err.Response.StatusCode,
  143. "statusText": err.Response.StatusText,
  144. }
  145. // The client dereferences response.data on a failure too, so the error
  146. // envelope carries one in AMF as it does in every other format.
  147. if err.Response.Data != nil {
  148. m["data"] = e.toAMF3Compatible(err.Response.Data)
  149. } else {
  150. m["data"] = map[string]interface{}{}
  151. }
  152. return map[string]interface{}{"response": m}
  153. }
  154. // structToMap converts a struct to a map using JSON tags for AMF3
  155. func (e *AMFEncoder) structToMap(v reflect.Value) map[string]interface{} {
  156. result := make(map[string]interface{})
  157. t := v.Type()
  158. for i := 0; i < v.NumField(); i++ {
  159. field := t.Field(i)
  160. fieldValue := v.Field(i)
  161. // Skip unexported fields
  162. if !fieldValue.CanInterface() {
  163. continue
  164. }
  165. // Get JSON tag
  166. jsonTag := field.Tag.Get("json")
  167. if jsonTag == "-" {
  168. continue
  169. }
  170. // Parse JSON tag
  171. tagParts := strings.Split(jsonTag, ",")
  172. fieldName := tagParts[0]
  173. if fieldName == "" {
  174. fieldName = field.Name
  175. }
  176. // Check for omitempty
  177. omitEmpty := false
  178. for _, part := range tagParts[1:] {
  179. if part == "omitempty" {
  180. omitEmpty = true
  181. break
  182. }
  183. }
  184. // Skip if omitempty and value is zero
  185. if omitEmpty && e.isZeroValue(fieldValue) {
  186. continue
  187. }
  188. // Get field value and convert recursively
  189. fieldData := fieldValue.Interface()
  190. result[fieldName] = e.toAMF3Compatible(fieldData)
  191. }
  192. return result
  193. }
  194. // mapToAMFMap converts a Go map to an AMF3-compatible map
  195. func (e *AMFEncoder) mapToAMFMap(v reflect.Value) map[string]interface{} {
  196. result := make(map[string]interface{})
  197. for _, key := range v.MapKeys() {
  198. // Convert key to string (AMF only supports string keys)
  199. keyStr := fmt.Sprintf("%v", key.Interface())
  200. value := v.MapIndex(key)
  201. if value.CanInterface() {
  202. result[keyStr] = e.toAMF3Compatible(value.Interface())
  203. }
  204. }
  205. return result
  206. }
  207. // convertToMap converts any data to a map structure for AMF3
  208. func (e *AMFEncoder) convertToMap(data interface{}) interface{} {
  209. if data == nil {
  210. // For AMF3, return empty map instead of nil to avoid truncation
  211. return map[string]interface{}{}
  212. }
  213. // If already a map, return as-is (even if empty)
  214. if m, ok := data.(map[string]interface{}); ok {
  215. if m == nil {
  216. return map[string]interface{}{}
  217. }
  218. return m
  219. }
  220. v := reflect.ValueOf(data)
  221. // Handle pointers
  222. if v.Kind() == reflect.Pointer {
  223. if v.IsNil() {
  224. return nil
  225. }
  226. v = v.Elem()
  227. data = v.Interface()
  228. }
  229. // Handle different types
  230. switch v.Kind() {
  231. case reflect.Struct:
  232. return e.structToMap(v)
  233. case reflect.Map:
  234. return e.mapToAMFMap(v)
  235. case reflect.Slice, reflect.Array:
  236. result := make([]interface{}, v.Len())
  237. for i := 0; i < v.Len(); i++ {
  238. elem := v.Index(i)
  239. if elem.CanInterface() {
  240. result[i] = e.convertToMap(elem.Interface())
  241. }
  242. }
  243. return result
  244. default:
  245. // For basic types, return as-is
  246. return data
  247. }
  248. }
  249. // isZeroValue checks if a reflect.Value is a zero value
  250. func (e *AMFEncoder) isZeroValue(v reflect.Value) bool {
  251. switch v.Kind() {
  252. case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
  253. return v.Len() == 0
  254. case reflect.Bool:
  255. return !v.Bool()
  256. case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
  257. return v.Int() == 0
  258. case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
  259. return v.Uint() == 0
  260. case reflect.Float32, reflect.Float64:
  261. return v.Float() == 0
  262. case reflect.Interface, reflect.Pointer:
  263. return v.IsNil()
  264. case reflect.Struct:
  265. // For time.Time, check if it's zero
  266. if t, ok := v.Interface().(time.Time); ok {
  267. return t.IsZero()
  268. }
  269. // For other structs, we can't easily determine zero value
  270. return false
  271. }
  272. return false
  273. }
  274. // DetectAMFVersion determines which AMF version to use based on the request
  275. func DetectAMFVersion(r *http.Request) AMFVersion {
  276. if r == nil {
  277. return AMF3
  278. }
  279. // Check query parameter first (highest priority)
  280. format := strings.ToLower(r.URL.Query().Get("f"))
  281. switch format {
  282. case "amf3":
  283. return AMF3
  284. case "amf":
  285. // Default to AMF3 for modern clients (Gromit expects AMF3)
  286. return AMF3
  287. }
  288. // Check Accept header for version hint
  289. accept := r.Header.Get("Accept")
  290. if strings.Contains(accept, "amf3") || strings.Contains(accept, "AMF3") {
  291. return AMF3
  292. }
  293. if strings.Contains(accept, "amf") || strings.Contains(accept, "AMF") {
  294. return AMF3 // Default to AMF3 for AMF requests
  295. }
  296. // Check Content-Type header (for POST requests)
  297. contentType := r.Header.Get("Content-Type")
  298. if strings.Contains(contentType, "amf3") || strings.Contains(contentType, "AMF3") {
  299. return AMF3
  300. }
  301. if strings.Contains(contentType, "amf") || strings.Contains(contentType, "AMF") {
  302. return AMF3 // Default to AMF3 for AMF requests
  303. }
  304. // Default to AMF3 for modern clients
  305. return AMF3
  306. }