amf_encoder.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  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. "github.com/mk6i/open-oscar-server/server/webapi/types"
  11. )
  12. // AMFVersion represents the AMF encoding version
  13. type AMFVersion int
  14. const (
  15. AMF3 AMFVersion = 3
  16. )
  17. // AMFEncoder handles AMF encoding operations for WebAPI responses
  18. type AMFEncoder struct {
  19. logger *slog.Logger
  20. }
  21. // NewAMFEncoder creates a new AMF encoder instance
  22. func NewAMFEncoder(logger *slog.Logger) *AMFEncoder {
  23. return &AMFEncoder{logger: logger}
  24. }
  25. // EncodeAMF encodes data to AMF3 format (only supported version)
  26. func (e *AMFEncoder) EncodeAMF(data interface{}, version AMFVersion) ([]byte, error) {
  27. // For AMF3, use goAMF3 which properly supports it
  28. // Convert to a regular map structure (no ECMAArray needed)
  29. amfData := e.toAMF3Compatible(data)
  30. // goAMF3 panics on nil values, ensure we sanitize
  31. sanitized := e.sanitizeForAMF3(amfData)
  32. encoded := goAMF3.EncodeAMF3(sanitized)
  33. return encoded, nil
  34. }
  35. // toAMF3Compatible converts Go types to AMF3-compatible format for goAMF3
  36. func (e *AMFEncoder) toAMF3Compatible(data interface{}) interface{} {
  37. if data == nil {
  38. return map[string]interface{}{}
  39. }
  40. // goAMF3 handles regular Go types well, just need to ensure maps are used
  41. // Don't use ECMAArray for AMF3 - just regular maps
  42. switch d := data.(type) {
  43. case BaseResponse:
  44. return e.baseResponseToMap(d)
  45. case ResponseBody:
  46. return e.responseBodyToMap(d)
  47. case ErrorResponse:
  48. return e.errorResponseToMap(d)
  49. case StartSessionResponse:
  50. // Special handling for StartSessionResponse
  51. return map[string]interface{}{
  52. "response": map[string]interface{}{
  53. "statusCode": d.Response.StatusCode,
  54. "statusText": d.Response.StatusText,
  55. "data": map[string]interface{}{
  56. "aimsid": d.Response.Data.AimSID,
  57. "fetchTimeout": d.Response.Data.FetchTimeout,
  58. "timeToNextFetch": d.Response.Data.TimeToNextFetch,
  59. "fetchBaseURL": d.Response.Data.FetchBaseURL, // Required for Gromit
  60. "events": d.Response.Data.Events,
  61. "wellKnownUrls": d.Response.Data.WellKnownUrls,
  62. },
  63. },
  64. }
  65. case FetchEventsResponse:
  66. // Special handling for FetchEventsResponse
  67. // goAMF3 can't handle uint64, must convert to int
  68. return map[string]interface{}{
  69. "response": map[string]interface{}{
  70. "statusCode": d.Response.StatusCode,
  71. "statusText": d.Response.StatusText,
  72. "data": map[string]interface{}{
  73. "events": d.Response.Data.Events,
  74. "lastSeqNum": int(d.Response.Data.LastSeqNum), // Convert uint64 to int
  75. "timeToNextFetch": d.Response.Data.TimeToNextFetch,
  76. "fetchBaseURL": d.Response.Data.FetchBaseURL,
  77. },
  78. },
  79. }
  80. case EndSessionResponse:
  81. // Special handling for EndSessionResponse - Gromit expects flat structure
  82. // Based on Gromit's MockServer, it expects:
  83. // { "data": {}, "statusCode": 200, "statusText": "OK" }
  84. return map[string]interface{}{
  85. "data": map[string]interface{}{}, // Empty data object
  86. "statusCode": d.Response.StatusCode,
  87. "statusText": d.Response.StatusText,
  88. }
  89. default:
  90. // For other types, convert structs to maps
  91. return e.convertToMap(data)
  92. }
  93. }
  94. // sanitizeForAMF3 recursively removes nil values from the data structure
  95. // because goAMF3 panics when encountering nil values in maps
  96. func (e *AMFEncoder) sanitizeForAMF3(data interface{}) interface{} {
  97. if data == nil {
  98. return map[string]interface{}{}
  99. }
  100. switch v := data.(type) {
  101. case uint64:
  102. // goAMF3 can't handle uint64, convert to int
  103. return int(v)
  104. case uint32:
  105. // Convert all unsigned to signed for safety
  106. return int(v)
  107. case uint16:
  108. return int(v)
  109. case uint8:
  110. return int(v)
  111. case uint:
  112. return int(v)
  113. case map[string]interface{}:
  114. result := make(map[string]interface{})
  115. for key, val := range v {
  116. if val == nil {
  117. // For fields like 'data', replace with empty map
  118. // For other fields, skip them
  119. if key == "data" {
  120. result[key] = map[string]interface{}{}
  121. }
  122. continue
  123. }
  124. result[key] = e.sanitizeForAMF3(val)
  125. }
  126. return result
  127. case []interface{}:
  128. result := make([]interface{}, len(v))
  129. for i, item := range v {
  130. result[i] = e.sanitizeForAMF3(item)
  131. }
  132. return result
  133. case []types.Event:
  134. // Handle WebAPIEvent arrays specially
  135. result := make([]interface{}, len(v))
  136. for i, event := range v {
  137. // AMF3 has a 29-bit limit for integers
  138. // Keep seqNum small by using modulo
  139. seqNum := int(event.SeqNum % (1 << 29))
  140. // Convert timestamp to seconds ago to keep it small
  141. timestampSec := int(time.Now().Unix() - event.Timestamp)
  142. if timestampSec < 0 {
  143. timestampSec = 0
  144. }
  145. result[i] = map[string]interface{}{
  146. "type": event.Type,
  147. "seqNum": seqNum,
  148. "timestamp": timestampSec,
  149. "data": e.sanitizeForAMF3(event.Data),
  150. }
  151. }
  152. return result
  153. case types.Event:
  154. // Handle single WebAPIEvent
  155. // AMF3 has a 29-bit limit for integers
  156. seqNum := int(v.SeqNum % (1 << 29))
  157. // Convert timestamp to seconds ago to keep it small
  158. timestampSec := int(time.Now().Unix() - v.Timestamp)
  159. if timestampSec < 0 {
  160. timestampSec = 0
  161. }
  162. return map[string]interface{}{
  163. "type": v.Type,
  164. "seqNum": seqNum,
  165. "timestamp": timestampSec,
  166. "data": e.sanitizeForAMF3(v.Data),
  167. }
  168. default:
  169. // For other types, use reflection to check if it's a struct
  170. // and convert to map
  171. rv := reflect.ValueOf(data)
  172. if rv.Kind() == reflect.Struct {
  173. return e.structToMap(rv)
  174. }
  175. return data
  176. }
  177. }
  178. // baseResponseToMap converts BaseResponse to AMF3-compatible map
  179. func (e *AMFEncoder) baseResponseToMap(resp BaseResponse) map[string]interface{} {
  180. return map[string]interface{}{
  181. "response": e.responseBodyToMap(resp.Response),
  182. }
  183. }
  184. // responseBodyToMap converts ResponseBody to AMF3-compatible map
  185. func (e *AMFEncoder) responseBodyToMap(body ResponseBody) map[string]interface{} {
  186. m := map[string]interface{}{
  187. "statusCode": body.StatusCode,
  188. "statusText": body.StatusText,
  189. }
  190. if body.RequestID != "" {
  191. m["requestId"] = body.RequestID
  192. }
  193. if body.Data != nil {
  194. m["data"] = e.toAMF3Compatible(body.Data)
  195. } else {
  196. // For AMF3, always include data field even if empty to prevent truncation
  197. m["data"] = map[string]interface{}{}
  198. }
  199. return m
  200. }
  201. // errorResponseToMap converts ErrorResponse to AMF3-compatible map
  202. func (e *AMFEncoder) errorResponseToMap(err ErrorResponse) map[string]interface{} {
  203. return map[string]interface{}{
  204. "response": map[string]interface{}{
  205. "statusCode": err.Response.StatusCode,
  206. "statusText": err.Response.StatusText,
  207. },
  208. }
  209. }
  210. // structToMap converts a struct to a map using JSON tags for AMF3
  211. func (e *AMFEncoder) structToMap(v reflect.Value) map[string]interface{} {
  212. result := make(map[string]interface{})
  213. t := v.Type()
  214. for i := 0; i < v.NumField(); i++ {
  215. field := t.Field(i)
  216. fieldValue := v.Field(i)
  217. // Skip unexported fields
  218. if !fieldValue.CanInterface() {
  219. continue
  220. }
  221. // Get JSON tag
  222. jsonTag := field.Tag.Get("json")
  223. if jsonTag == "-" {
  224. continue
  225. }
  226. // Parse JSON tag
  227. tagParts := strings.Split(jsonTag, ",")
  228. fieldName := tagParts[0]
  229. if fieldName == "" {
  230. fieldName = field.Name
  231. }
  232. // Check for omitempty
  233. omitEmpty := false
  234. for _, part := range tagParts[1:] {
  235. if part == "omitempty" {
  236. omitEmpty = true
  237. break
  238. }
  239. }
  240. // Skip if omitempty and value is zero
  241. if omitEmpty && e.isZeroValue(fieldValue) {
  242. continue
  243. }
  244. // Get field value and convert recursively
  245. fieldData := fieldValue.Interface()
  246. result[fieldName] = e.toAMF3Compatible(fieldData)
  247. }
  248. return result
  249. }
  250. // mapToAMFMap converts a Go map to an AMF3-compatible map
  251. func (e *AMFEncoder) mapToAMFMap(v reflect.Value) map[string]interface{} {
  252. result := make(map[string]interface{})
  253. for _, key := range v.MapKeys() {
  254. // Convert key to string (AMF only supports string keys)
  255. keyStr := fmt.Sprintf("%v", key.Interface())
  256. value := v.MapIndex(key)
  257. if value.CanInterface() {
  258. result[keyStr] = e.toAMF3Compatible(value.Interface())
  259. }
  260. }
  261. return result
  262. }
  263. // convertToMap converts any data to a map structure for AMF3
  264. func (e *AMFEncoder) convertToMap(data interface{}) interface{} {
  265. if data == nil {
  266. // For AMF3, return empty map instead of nil to avoid truncation
  267. return map[string]interface{}{}
  268. }
  269. // If already a map, return as-is (even if empty)
  270. if m, ok := data.(map[string]interface{}); ok {
  271. if m == nil {
  272. return map[string]interface{}{}
  273. }
  274. return m
  275. }
  276. v := reflect.ValueOf(data)
  277. // Handle pointers
  278. if v.Kind() == reflect.Pointer {
  279. if v.IsNil() {
  280. return nil
  281. }
  282. v = v.Elem()
  283. data = v.Interface()
  284. }
  285. // Handle different types
  286. switch v.Kind() {
  287. case reflect.Struct:
  288. return e.structToMap(v)
  289. case reflect.Map:
  290. return e.mapToAMFMap(v)
  291. case reflect.Slice, reflect.Array:
  292. result := make([]interface{}, v.Len())
  293. for i := 0; i < v.Len(); i++ {
  294. elem := v.Index(i)
  295. if elem.CanInterface() {
  296. result[i] = e.convertToMap(elem.Interface())
  297. }
  298. }
  299. return result
  300. default:
  301. // For basic types, return as-is
  302. return data
  303. }
  304. }
  305. // isZeroValue checks if a reflect.Value is a zero value
  306. func (e *AMFEncoder) isZeroValue(v reflect.Value) bool {
  307. switch v.Kind() {
  308. case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
  309. return v.Len() == 0
  310. case reflect.Bool:
  311. return !v.Bool()
  312. case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
  313. return v.Int() == 0
  314. case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
  315. return v.Uint() == 0
  316. case reflect.Float32, reflect.Float64:
  317. return v.Float() == 0
  318. case reflect.Interface, reflect.Pointer:
  319. return v.IsNil()
  320. case reflect.Struct:
  321. // For time.Time, check if it's zero
  322. if t, ok := v.Interface().(time.Time); ok {
  323. return t.IsZero()
  324. }
  325. // For other structs, we can't easily determine zero value
  326. return false
  327. }
  328. return false
  329. }
  330. // DetectAMFVersion determines which AMF version to use based on the request
  331. func DetectAMFVersion(r *http.Request) AMFVersion {
  332. if r == nil {
  333. return AMF3
  334. }
  335. // Check query parameter first (highest priority)
  336. format := strings.ToLower(r.URL.Query().Get("f"))
  337. switch format {
  338. case "amf3":
  339. return AMF3
  340. case "amf":
  341. // Default to AMF3 for modern clients (Gromit expects AMF3)
  342. return AMF3
  343. }
  344. // Check Accept header for version hint
  345. accept := r.Header.Get("Accept")
  346. if strings.Contains(accept, "amf3") || strings.Contains(accept, "AMF3") {
  347. return AMF3
  348. }
  349. if strings.Contains(accept, "amf") || strings.Contains(accept, "AMF") {
  350. return AMF3 // Default to AMF3 for AMF requests
  351. }
  352. // Check Content-Type header (for POST requests)
  353. contentType := r.Header.Get("Content-Type")
  354. if strings.Contains(contentType, "amf3") || strings.Contains(contentType, "AMF3") {
  355. return AMF3
  356. }
  357. if strings.Contains(contentType, "amf") || strings.Contains(contentType, "AMF") {
  358. return AMF3 // Default to AMF3 for AMF requests
  359. }
  360. // Default to AMF3 for modern clients
  361. return AMF3
  362. }