amf_encoder.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  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. // toAMFCompatible converts Go types to AMF3-compatible types
  179. func (e *AMFEncoder) toAMFCompatible(data interface{}) interface{} {
  180. return e.toAMF3Compatible(data)
  181. }
  182. // baseResponseToMap converts BaseResponse to AMF3-compatible map
  183. func (e *AMFEncoder) baseResponseToMap(resp BaseResponse) map[string]interface{} {
  184. return map[string]interface{}{
  185. "response": e.responseBodyToMap(resp.Response),
  186. }
  187. }
  188. // responseBodyToMap converts ResponseBody to AMF3-compatible map
  189. func (e *AMFEncoder) responseBodyToMap(body ResponseBody) map[string]interface{} {
  190. m := map[string]interface{}{
  191. "statusCode": body.StatusCode,
  192. "statusText": body.StatusText,
  193. }
  194. if body.Data != nil {
  195. m["data"] = e.toAMF3Compatible(body.Data)
  196. } else {
  197. // For AMF3, always include data field even if empty to prevent truncation
  198. m["data"] = map[string]interface{}{}
  199. }
  200. return m
  201. }
  202. // errorResponseToMap converts ErrorResponse to AMF3-compatible map
  203. func (e *AMFEncoder) errorResponseToMap(err ErrorResponse) map[string]interface{} {
  204. return map[string]interface{}{
  205. "response": map[string]interface{}{
  206. "statusCode": err.Response.StatusCode,
  207. "statusText": err.Response.StatusText,
  208. },
  209. }
  210. }
  211. // structToMap converts a struct to a map using JSON tags for AMF3
  212. func (e *AMFEncoder) structToMap(v reflect.Value) map[string]interface{} {
  213. result := make(map[string]interface{})
  214. t := v.Type()
  215. for i := 0; i < v.NumField(); i++ {
  216. field := t.Field(i)
  217. fieldValue := v.Field(i)
  218. // Skip unexported fields
  219. if !fieldValue.CanInterface() {
  220. continue
  221. }
  222. // Get JSON tag
  223. jsonTag := field.Tag.Get("json")
  224. if jsonTag == "-" {
  225. continue
  226. }
  227. // Parse JSON tag
  228. tagParts := strings.Split(jsonTag, ",")
  229. fieldName := tagParts[0]
  230. if fieldName == "" {
  231. fieldName = field.Name
  232. }
  233. // Check for omitempty
  234. omitEmpty := false
  235. for _, part := range tagParts[1:] {
  236. if part == "omitempty" {
  237. omitEmpty = true
  238. break
  239. }
  240. }
  241. // Skip if omitempty and value is zero
  242. if omitEmpty && e.isZeroValue(fieldValue) {
  243. continue
  244. }
  245. // Get field value and convert recursively
  246. fieldData := fieldValue.Interface()
  247. result[fieldName] = e.toAMF3Compatible(fieldData)
  248. }
  249. return result
  250. }
  251. // sliceToArray converts a slice to an AMF3-compatible array
  252. func (e *AMFEncoder) sliceToArray(v reflect.Value) []interface{} {
  253. length := v.Len()
  254. result := make([]interface{}, length)
  255. for i := 0; i < length; i++ {
  256. elem := v.Index(i)
  257. if elem.CanInterface() {
  258. result[i] = e.toAMF3Compatible(elem.Interface())
  259. } else {
  260. result[i] = nil
  261. }
  262. }
  263. return result
  264. }
  265. // mapToAMFMap converts a Go map to an AMF3-compatible map
  266. func (e *AMFEncoder) mapToAMFMap(v reflect.Value) map[string]interface{} {
  267. result := make(map[string]interface{})
  268. for _, key := range v.MapKeys() {
  269. // Convert key to string (AMF only supports string keys)
  270. keyStr := fmt.Sprintf("%v", key.Interface())
  271. value := v.MapIndex(key)
  272. if value.CanInterface() {
  273. result[keyStr] = e.toAMF3Compatible(value.Interface())
  274. }
  275. }
  276. return result
  277. }
  278. // convertToMap converts any data to a map structure for AMF3
  279. func (e *AMFEncoder) convertToMap(data interface{}) interface{} {
  280. if data == nil {
  281. // For AMF3, return empty map instead of nil to avoid truncation
  282. return map[string]interface{}{}
  283. }
  284. // If already a map, return as-is (even if empty)
  285. if m, ok := data.(map[string]interface{}); ok {
  286. if m == nil {
  287. return map[string]interface{}{}
  288. }
  289. return m
  290. }
  291. v := reflect.ValueOf(data)
  292. // Handle pointers
  293. if v.Kind() == reflect.Ptr {
  294. if v.IsNil() {
  295. return nil
  296. }
  297. v = v.Elem()
  298. data = v.Interface()
  299. }
  300. // Handle different types
  301. switch v.Kind() {
  302. case reflect.Struct:
  303. return e.structToMap(v)
  304. case reflect.Map:
  305. return e.mapToAMFMap(v)
  306. case reflect.Slice, reflect.Array:
  307. result := make([]interface{}, v.Len())
  308. for i := 0; i < v.Len(); i++ {
  309. elem := v.Index(i)
  310. if elem.CanInterface() {
  311. result[i] = e.convertToMap(elem.Interface())
  312. }
  313. }
  314. return result
  315. default:
  316. // For basic types, return as-is
  317. return data
  318. }
  319. }
  320. // isZeroValue checks if a reflect.Value is a zero value
  321. func (e *AMFEncoder) isZeroValue(v reflect.Value) bool {
  322. switch v.Kind() {
  323. case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
  324. return v.Len() == 0
  325. case reflect.Bool:
  326. return !v.Bool()
  327. case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
  328. return v.Int() == 0
  329. case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
  330. return v.Uint() == 0
  331. case reflect.Float32, reflect.Float64:
  332. return v.Float() == 0
  333. case reflect.Interface, reflect.Ptr:
  334. return v.IsNil()
  335. case reflect.Struct:
  336. // For time.Time, check if it's zero
  337. if t, ok := v.Interface().(time.Time); ok {
  338. return t.IsZero()
  339. }
  340. // For other structs, we can't easily determine zero value
  341. return false
  342. }
  343. return false
  344. }
  345. // DetectAMFVersion determines which AMF version to use based on the request
  346. func DetectAMFVersion(r *http.Request) AMFVersion {
  347. if r == nil {
  348. return AMF3
  349. }
  350. // Check query parameter first (highest priority)
  351. format := strings.ToLower(r.URL.Query().Get("f"))
  352. switch format {
  353. case "amf3":
  354. return AMF3
  355. case "amf":
  356. // Default to AMF3 for modern clients (Gromit expects AMF3)
  357. return AMF3
  358. }
  359. // Check Accept header for version hint
  360. accept := r.Header.Get("Accept")
  361. if strings.Contains(accept, "amf3") || strings.Contains(accept, "AMF3") {
  362. return AMF3
  363. }
  364. if strings.Contains(accept, "amf") || strings.Contains(accept, "AMF") {
  365. return AMF3 // Default to AMF3 for AMF requests
  366. }
  367. // Check Content-Type header (for POST requests)
  368. contentType := r.Header.Get("Content-Type")
  369. if strings.Contains(contentType, "amf3") || strings.Contains(contentType, "AMF3") {
  370. return AMF3
  371. }
  372. if strings.Contains(contentType, "amf") || strings.Contains(contentType, "AMF") {
  373. return AMF3 // Default to AMF3 for AMF requests
  374. }
  375. // Default to AMF3 for modern clients
  376. return AMF3
  377. }
  378. // IsAMFRequest checks if the request is asking for AMF format
  379. func IsAMFRequest(r *http.Request) bool {
  380. if r == nil {
  381. return false
  382. }
  383. // Check query parameter
  384. format := strings.ToLower(r.URL.Query().Get("f"))
  385. if format == "amf" || format == "amf0" || format == "amf3" {
  386. return true
  387. }
  388. // Check Accept header
  389. accept := strings.ToLower(r.Header.Get("Accept"))
  390. if strings.Contains(accept, "application/x-amf") ||
  391. strings.Contains(accept, "application/amf") {
  392. return true
  393. }
  394. return false
  395. }