diff --git a/deviceList.go b/deviceList.go deleted file mode 100644 index 051142ae..00000000 --- a/deviceList.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "golbat/config" - "time" - - "github.com/jellydator/ttlcache/v3" -) - -type DeviceLocation struct { - Latitude float64 - Longitude float64 - LastUpdate int64 - ScanContext string -} - -var deviceLocation *ttlcache.Cache[string, DeviceLocation] - -func InitDeviceCache() { - deviceLocation = ttlcache.New[string, DeviceLocation]( - ttlcache.WithTTL[string, DeviceLocation](time.Hour * time.Duration(config.Config.Cleanup.DeviceHours)), - ) - go deviceLocation.Start() -} - -func UpdateDeviceLocation(deviceId string, lat, lon float64, scanContext string) { - deviceLocation.Set(deviceId, DeviceLocation{ - Latitude: lat, - Longitude: lon, - LastUpdate: time.Now().Unix(), - ScanContext: scanContext, - }, time.Hour*time.Duration(config.Config.Cleanup.DeviceHours)) -} - -type ApiDeviceLocation struct { - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - LastUpdate int64 `json:"last_update"` - ScanContext string `json:"scan_context"` -} - -func GetAllDevices() map[string]ApiDeviceLocation { - locations := map[string]ApiDeviceLocation{} - for _, key := range deviceLocation.Items() { - deviceLocation := key.Value() - locations[key.Key()] = ApiDeviceLocation{ - Latitude: deviceLocation.Latitude, - Longitude: deviceLocation.Longitude, - LastUpdate: deviceLocation.LastUpdate, - ScanContext: deviceLocation.ScanContext, - } - } - return locations -} diff --git a/device_tracker/device_tracker.go b/device_tracker/device_tracker.go new file mode 100644 index 00000000..c1453dfd --- /dev/null +++ b/device_tracker/device_tracker.go @@ -0,0 +1,79 @@ +package device_tracker + +import ( + "context" + "time" + + "github.com/jellydator/ttlcache/v3" +) + +type DeviceTracker interface { + // UpdateDevice location updates a device location. If any of deviceId, lat, + // and lng are their zero values, no update will occur. + UpdateDeviceLocation(deviceId string, lat, lon float64, scanContext string) + // GetAllDevices returns info about all devices. + GetAllDevices() map[string]ApiDeviceLocation + // Run runs the automatic cleanup process, blocking until `ctx` is cancelled. + Run(ctx context.Context) +} + +type ApiDeviceLocation struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + LastUpdate int64 `json:"last_update"` + ScanContext string `json:"scan_context"` +} + +type DeviceLocation struct { + Latitude float64 + Longitude float64 + LastUpdate int64 + ScanContext string +} + +type deviceTracker struct { + maxDeviceTTL time.Duration + deviceLocation *ttlcache.Cache[string, DeviceLocation] +} + +func (tracker *deviceTracker) UpdateDeviceLocation(deviceId string, lat, lon float64, scanContext string) { + if lat == 0 || lon == 0 || deviceId == "" { + return + } + tracker.deviceLocation.Set(deviceId, DeviceLocation{ + Latitude: lat, + Longitude: lon, + LastUpdate: time.Now().Unix(), + ScanContext: scanContext, + }, tracker.maxDeviceTTL) +} + +func (tracker *deviceTracker) GetAllDevices() map[string]ApiDeviceLocation { + locations := map[string]ApiDeviceLocation{} + for _, key := range tracker.deviceLocation.Items() { + deviceLocation := key.Value() + locations[key.Key()] = ApiDeviceLocation(deviceLocation) + } + return locations +} + +func (tracker *deviceTracker) Run(ctx context.Context) { + ctx, cancel_fn := context.WithCancel(ctx) + defer cancel_fn() + go func() { + defer tracker.deviceLocation.Stop() + <-ctx.Done() + }() + tracker.deviceLocation.Start() +} + +func NewDeviceTracker(maxDeviceTTLHours int) DeviceTracker { + maxDeviceTTL := time.Hour * time.Duration(maxDeviceTTLHours) + tracker := &deviceTracker{ + maxDeviceTTL: maxDeviceTTL, + deviceLocation: ttlcache.New[string, DeviceLocation]( + ttlcache.WithTTL[string, DeviceLocation](maxDeviceTTL), + ), + } + return tracker +} diff --git a/grpc_server_raw.go b/grpc_server_raw.go index 3ff615e9..52179840 100644 --- a/grpc_server_raw.go +++ b/grpc_server_raw.go @@ -4,9 +4,9 @@ import ( "context" "golbat/config" pb "golbat/grpc" + _ "google.golang.org/grpc/encoding/gzip" // Install the gzip compressor "google.golang.org/grpc/metadata" - "time" ) // server is used to implement helloworld.GreeterServer. @@ -24,59 +24,11 @@ func (s *grpcRawServer) SubmitRawProto(ctx context.Context, in *pb.RawProtoReque } } - uuid := in.DeviceId - account := in.Username - level := int(in.TrainerLevel) - scanContext := "" - if in.ScanContext != nil { - scanContext = *in.ScanContext - } - - latTarget, lonTarget := float64(in.LatTarget), float64(in.LonTarget) - globalHaveAr := in.HaveAr - var protoData []ProtoData - - for _, v := range in.Contents { - - inboundRawData := ProtoData{ - Method: int(v.Method), - Account: account, - Level: level, - ScanContext: scanContext, - Lat: latTarget, - Lon: lonTarget, - Data: v.ResponsePayload, - Request: v.RequestPayload, - Uuid: uuid, - HaveAr: func() *bool { - if v.HaveAr != nil { - return v.HaveAr - } - return globalHaveAr - }(), - } - - protoData = append(protoData, inboundRawData) - } - + protoData := rawProtoDecoder.GetProtoDataFromGRPC(in) // Process each proto in a packet in sequence, but in a go-routine - go func() { - timeout := 5 * time.Second - if config.Config.Tuning.ExtendedTimeout { - timeout = 30 * time.Second - } + go rawProtoDecoder.Decode(context.Background(), protoData, decode) - for _, entry := range protoData { - // provide independent cancellation contexts for each proto decode - ctx, cancel := context.WithTimeout(context.Background(), timeout) - decode(ctx, entry.Method, &entry) - cancel() - } - }() - - if latTarget != 0 && lonTarget != 0 && uuid != "" { - UpdateDeviceLocation(uuid, latTarget, lonTarget, scanContext) - } + deviceTracker.UpdateDeviceLocation(protoData.Uuid, protoData.Lat(), protoData.Lon(), protoData.ScanContext) return &pb.RawProtoResponse{Message: "Processed"}, nil } diff --git a/main.go b/main.go index cbed5fcc..ea04c1e3 100644 --- a/main.go +++ b/main.go @@ -24,9 +24,11 @@ import ( "golbat/config" db2 "golbat/db" "golbat/decoder" + "golbat/device_tracker" "golbat/external" pb "golbat/grpc" "golbat/pogo" + "golbat/raw_decoder" "golbat/stats_collector" "golbat/webhooks" ) @@ -71,12 +73,6 @@ func main() { cfg.Logging.Compress, ) - webhooksSender, err := webhooks.NewWebhooksSender(cfg) - if err != nil { - log.Fatalf("failed to setup webhooks sender: %s", err) - } - decoder.SetWebhooksSender(webhooksSender) - log.Infof("Golbat starting") // Capture connection properties. @@ -187,8 +183,28 @@ func main() { decoder.InitialiseOhbem() decoder.LoadStatsGeofences() decoder.LoadNests(dbDetails) - InitDeviceCache() + { + timeout := 5 * time.Second + if cfg.Tuning.ExtendedTimeout { + timeout = 30 * time.Second + } + rawProtoDecoder = raw_decoder.NewRawDecoder(timeout) + } + + deviceTracker = device_tracker.NewDeviceTracker(cfg.Cleanup.DeviceHours) + wg.Add(1) + go func() { + defer cancelFn() + defer wg.Done() + deviceTracker.Run(ctx) + }() + + webhooksSender, err := webhooks.NewWebhooksSender(cfg) + if err != nil { + log.Fatalf("failed to setup webhooks sender: %s", err) + } + decoder.SetWebhooksSender(webhooksSender) wg.Add(1) go func() { defer cancelFn() @@ -319,8 +335,33 @@ func main() { log.Info("Golbat exiting!") } -func decode(ctx context.Context, method int, protoData *ProtoData) { - getMethodName := func(method int, trimString bool) string { +type decodeProtoMethod struct { + Decode func(context.Context, *raw_decoder.Proto) (bool, string) + MinLevel int +} + +var decodeMethods = map[pogo.Method]*decodeProtoMethod{ + pogo.Method_METHOD_START_INCIDENT: &decodeProtoMethod{decodeStartIncident, 30}, + pogo.Method_METHOD_INVASION_OPEN_COMBAT_SESSION: &decodeProtoMethod{decodeOpenInvasion, 30}, + pogo.Method_METHOD_FORT_DETAILS: &decodeProtoMethod{decodeFortDetails, 30}, + pogo.Method_METHOD_GET_MAP_OBJECTS: &decodeProtoMethod{decodeGMO, 0}, + pogo.Method_METHOD_GYM_GET_INFO: &decodeProtoMethod{decodeGetGymInfo, 10}, + pogo.Method_METHOD_ENCOUNTER: &decodeProtoMethod{decodeEncounter, 30}, + pogo.Method_METHOD_DISK_ENCOUNTER: &decodeProtoMethod{decodeDiskEncounter, 30}, + pogo.Method_METHOD_FORT_SEARCH: &decodeProtoMethod{decodeFortSearch, 10}, + pogo.Method(pogo.ClientAction_CLIENT_ACTION_PROXY_SOCIAL_ACTION): &decodeProtoMethod{decodeSocialAction, 0}, + pogo.Method_METHOD_GET_MAP_FORTS: &decodeProtoMethod{decodeGetMapForts, 10}, + pogo.Method_METHOD_GET_ROUTES: &decodeProtoMethod{decodeGetRoutes, 30}, + pogo.Method_METHOD_GET_CONTEST_DATA: &decodeProtoMethod{decodeGetContestData, 10}, + pogo.Method_METHOD_GET_POKEMON_SIZE_CONTEST_ENTRY: &decodeProtoMethod{decodeGetPokemonSizeContestEntry, 10}, + // ignores + pogo.Method_METHOD_GET_PLAYER: nil, + pogo.Method_METHOD_GET_HOLOHOLO_INVENTORY: nil, + pogo.Method_METHOD_CREATE_COMBAT_CHALLENGE: nil, +} + +func decode(ctx context.Context, protoData *raw_decoder.Proto) { + getMethodName := func(method pogo.Method, trimString bool) string { if val, ok := pogo.Method_name[int32(method)]; ok { if trimString && strings.HasPrefix(val, "METHOD_") { return strings.TrimPrefix(val, "METHOD_") @@ -330,83 +371,28 @@ func decode(ctx context.Context, method int, protoData *ProtoData) { return fmt.Sprintf("#%d", method) } - if method != int(pogo.ClientAction_CLIENT_ACTION_PROXY_SOCIAL_ACTION) && protoData.Level < 30 { - statsCollector.IncDecodeMethods("error", "low_level", getMethodName(method, true)) - log.Debugf("Insufficient Level %d Did not process hook type %s", protoData.Level, pogo.Method(method)) - return - } - processed := false ignore := false start := time.Now() result := "" - switch pogo.Method(method) { - case pogo.Method_METHOD_START_INCIDENT: - result = decodeStartIncident(ctx, protoData.Data) - processed = true - case pogo.Method_METHOD_INVASION_OPEN_COMBAT_SESSION: - if protoData.Request != nil { - result = decodeOpenInvasion(ctx, protoData.Request, protoData.Data) - processed = true - } - break - case pogo.Method_METHOD_FORT_DETAILS: - result = decodeFortDetails(ctx, protoData.Data) - processed = true - case pogo.Method_METHOD_GET_MAP_OBJECTS: - result = decodeGMO(ctx, protoData, getScanParameters(protoData)) - processed = true - case pogo.Method_METHOD_GYM_GET_INFO: - result = decodeGetGymInfo(ctx, protoData.Data) - processed = true - case pogo.Method_METHOD_ENCOUNTER: - if getScanParameters(protoData).ProcessPokemon { - result = decodeEncounter(ctx, protoData.Data, protoData.Account) + method := pogo.Method(protoData.Method) + decodeMethod, ok := decodeMethods[method] + if ok { + if decodeMethod == nil { + // completely ignore + return } - processed = true - case pogo.Method_METHOD_DISK_ENCOUNTER: - result = decodeDiskEncounter(ctx, protoData.Data, protoData.Account) - processed = true - case pogo.Method_METHOD_FORT_SEARCH: - result = decodeQuest(ctx, protoData.Data, protoData.HaveAr) - processed = true - case pogo.Method_METHOD_GET_PLAYER: - ignore = true - break - case pogo.Method_METHOD_GET_HOLOHOLO_INVENTORY: - ignore = true - break - case pogo.Method_METHOD_CREATE_COMBAT_CHALLENGE: - ignore = true - break - case pogo.Method(pogo.ClientAction_CLIENT_ACTION_PROXY_SOCIAL_ACTION): - if protoData.Request != nil { - result = decodeSocialActionWithRequest(protoData.Request, protoData.Data) - processed = true + if protoData.Level < decodeMethod.MinLevel { + statsCollector.IncDecodeMethods("error", "low_level", getMethodName(method, true)) + log.Debugf("Insufficient Level %d Did not process hook type %s", protoData.Level, method) + return } - break - case pogo.Method_METHOD_GET_MAP_FORTS: - result = decodeGetMapForts(ctx, protoData.Data) - processed = true - case pogo.Method_METHOD_GET_ROUTES: - result = decodeGetRoutes(protoData.Data) - processed = true - case pogo.Method_METHOD_GET_CONTEST_DATA: - // Request helps, but can be decoded without it - result = decodeGetContestData(ctx, protoData.Request, protoData.Data) - processed = true - break - case pogo.Method_METHOD_GET_POKEMON_SIZE_CONTEST_ENTRY: - // Request is essential to decode this - if protoData.Request != nil { - result = decodeGetPokemonSizeContestEntry(ctx, protoData.Request, protoData.Data) - processed = true - } - break - default: - log.Debugf("Did not know hook type %s", pogo.Method(method)) + processed, result = decodeMethod.Decode(ctx, protoData) + } else { + log.Debugf("Did not know hook type %s", method) } + if !ignore { elapsed := time.Since(start) if processed == true { @@ -419,69 +405,77 @@ func decode(ctx context.Context, method int, protoData *ProtoData) { } } -func getScanParameters(protoData *ProtoData) decoder.ScanParameters { +func getScanParameters(protoData *raw_decoder.Proto) decoder.ScanParameters { return decoder.FindScanConfiguration(protoData.ScanContext, protoData.Lat, protoData.Lon) } -func decodeQuest(ctx context.Context, sDec []byte, haveAr *bool) string { +func decodeFortSearch(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + response := protoData.ResponseProtoBytes() + haveAr := protoData.HaveAr + if haveAr == nil { statsCollector.IncDecodeQuest("error", "missing_ar_info") log.Infoln("Cannot determine AR quest - ignoring") // We should either assume AR quest, or trace inventory like RDM probably - return "No AR quest info" + return true, "No AR quest info" } decodedQuest := &pogo.FortSearchOutProto{} - if err := proto.Unmarshal(sDec, decodedQuest); err != nil { + if err := proto.Unmarshal(response, decodedQuest); err != nil { log.Errorf("Failed to parse %s", err) statsCollector.IncDecodeQuest("error", "parse") - return "Parse failure" + return true, "Parse failure" } if decodedQuest.Result != pogo.FortSearchOutProto_SUCCESS { statsCollector.IncDecodeQuest("error", "non_success") res := fmt.Sprintf(`GymGetInfoOutProto: Ignored non-success value %d:%s`, decodedQuest.Result, pogo.FortSearchOutProto_Result_name[int32(decodedQuest.Result)]) - return res + return true, res } - return decoder.UpdatePokestopWithQuest(ctx, dbDetails, decodedQuest, *haveAr) - + return true, decoder.UpdatePokestopWithQuest(ctx, dbDetails, decodedQuest, *haveAr) } -func decodeSocialActionWithRequest(request []byte, payload []byte) string { +func decodeSocialAction(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + request := protoData.RequestProtoBytes() + if request == nil { + return false, "no request" + } + response := protoData.ResponseProtoBytes() + var proxyRequestProto pogo.ProxyRequestProto if err := proto.Unmarshal(request, &proxyRequestProto); err != nil { log.Errorf("Failed to parse %s", err) statsCollector.IncDecodeSocialActionWithRequest("error", "request_parse") - return fmt.Sprintf("Failed to parse %s", err) + return true, fmt.Sprintf("Failed to parse %s", err) } var proxyResponseProto pogo.ProxyResponseProto - if err := proto.Unmarshal(payload, &proxyResponseProto); err != nil { + if err := proto.Unmarshal(response, &proxyResponseProto); err != nil { log.Errorf("Failed to parse %s", err) statsCollector.IncDecodeSocialActionWithRequest("error", "response_parse") - return fmt.Sprintf("Failed to parse %s", err) + return true, fmt.Sprintf("Failed to parse %s", err) } if proxyResponseProto.Status != pogo.ProxyResponseProto_COMPLETED && proxyResponseProto.Status != pogo.ProxyResponseProto_COMPLETED_AND_REASSIGNED { statsCollector.IncDecodeSocialActionWithRequest("error", "non_success") - return fmt.Sprintf("unsuccessful proxyResponseProto response %d %s", int(proxyResponseProto.Status), proxyResponseProto.Status) + return true, fmt.Sprintf("unsuccessful proxyResponseProto response %d %s", int(proxyResponseProto.Status), proxyResponseProto.Status) } switch pogo.SocialAction(proxyRequestProto.GetAction()) { case pogo.SocialAction_SOCIAL_ACTION_LIST_FRIEND_STATUS: statsCollector.IncDecodeSocialActionWithRequest("ok", "list_friend_status") - return decodeGetFriendDetails(proxyResponseProto.Payload) + return true, decodeGetFriendDetails(proxyResponseProto.Payload) case pogo.SocialAction_SOCIAL_ACTION_SEARCH_PLAYER: statsCollector.IncDecodeSocialActionWithRequest("ok", "search_player") - return decodeSearchPlayer(proxyRequestProto, proxyResponseProto.Payload) + return true, decodeSearchPlayer(proxyRequestProto, proxyResponseProto.Payload) } statsCollector.IncDecodeSocialActionWithRequest("ok", "unknown") - return fmt.Sprintf("Did not process %s", pogo.SocialAction(proxyRequestProto.GetAction()).String()) + return true, fmt.Sprintf("Did not process %s", pogo.SocialAction(proxyRequestProto.GetAction()).String()) } func decodeGetFriendDetails(payload []byte) string { @@ -548,40 +542,42 @@ func decodeSearchPlayer(proxyRequestProto pogo.ProxyRequestProto, payload []byte return fmt.Sprintf("1 player decoded from SearchPlayerProto") } -func decodeFortDetails(ctx context.Context, sDec []byte) string { +func decodeFortDetails(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + response := protoData.ResponseProtoBytes() decodedFort := &pogo.FortDetailsOutProto{} - if err := proto.Unmarshal(sDec, decodedFort); err != nil { + if err := proto.Unmarshal(response, decodedFort); err != nil { log.Errorf("Failed to parse %s", err) statsCollector.IncDecodeFortDetails("error", "parse") - return fmt.Sprintf("Failed to parse %s", err) + return true, fmt.Sprintf("Failed to parse %s", err) } switch decodedFort.FortType { case pogo.FortType_CHECKPOINT: statsCollector.IncDecodeFortDetails("ok", "pokestop") - return decoder.UpdatePokestopRecordWithFortDetailsOutProto(ctx, dbDetails, decodedFort) + return true, decoder.UpdatePokestopRecordWithFortDetailsOutProto(ctx, dbDetails, decodedFort) case pogo.FortType_GYM: statsCollector.IncDecodeFortDetails("ok", "gym") - return decoder.UpdateGymRecordWithFortDetailsOutProto(ctx, dbDetails, decodedFort) + return true, decoder.UpdateGymRecordWithFortDetailsOutProto(ctx, dbDetails, decodedFort) } statsCollector.IncDecodeFortDetails("ok", "unknown") - return "Unknown fort type" + return true, "Unknown fort type" } -func decodeGetMapForts(ctx context.Context, sDec []byte) string { +func decodeGetMapForts(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + response := protoData.ResponseProtoBytes() decodedMapForts := &pogo.GetMapFortsOutProto{} - if err := proto.Unmarshal(sDec, decodedMapForts); err != nil { + if err := proto.Unmarshal(response, decodedMapForts); err != nil { log.Errorf("Failed to parse %s", err) statsCollector.IncDecodeGetMapForts("error", "parse") - return fmt.Sprintf("Failed to parse %s", err) + return true, fmt.Sprintf("Failed to parse %s", err) } if decodedMapForts.Status != pogo.GetMapFortsOutProto_SUCCESS { statsCollector.IncDecodeGetMapForts("error", "non_success") res := fmt.Sprintf(`GetMapFortsOutProto: Ignored non-success value %d:%s`, decodedMapForts.Status, pogo.GetMapFortsOutProto_Status_name[int32(decodedMapForts.Status)]) - return res + return true, res } statsCollector.IncDecodeGetMapForts("ok", "") @@ -597,19 +593,20 @@ func decodeGetMapForts(ctx context.Context, sDec []byte) string { } if processedForts > 0 { - return fmt.Sprintf("Updated %d forts: %s", processedForts, outputString) + return true, fmt.Sprintf("Updated %d forts: %s", processedForts, outputString) } - return "No forts updated" + return true, "No forts updated" } -func decodeGetRoutes(payload []byte) string { +func decodeGetRoutes(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + response := protoData.ResponseProtoBytes() getRoutesOutProto := &pogo.GetRoutesOutProto{} - if err := proto.Unmarshal(payload, getRoutesOutProto); err != nil { - return fmt.Sprintf("failed to decode GetRoutesOutProto %s", err) + if err := proto.Unmarshal(response, getRoutesOutProto); err != nil { + return true, fmt.Sprintf("failed to decode GetRoutesOutProto %s", err) } if getRoutesOutProto.Status != pogo.GetRoutesOutProto_SUCCESS { - return fmt.Sprintf("GetRoutesOutProto: Ignored non-success value %d:%s", getRoutesOutProto.Status, getRoutesOutProto.Status.String()) + return true, fmt.Sprintf("GetRoutesOutProto: Ignored non-success value %d:%s", getRoutesOutProto.Status, getRoutesOutProto.Status.String()) } decodeSuccesses := map[string]bool{} @@ -633,7 +630,7 @@ func decodeGetRoutes(payload []byte) string { } } - return fmt.Sprintf( + return true, fmt.Sprintf( "Decoded %d routes, failed to decode %d routes, from %d cells", len(decodeSuccesses), len(decodeErrors), @@ -641,125 +638,143 @@ func decodeGetRoutes(payload []byte) string { ) } -func decodeGetGymInfo(ctx context.Context, sDec []byte) string { +func decodeGetGymInfo(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + response := protoData.ResponseProtoBytes() decodedGymInfo := &pogo.GymGetInfoOutProto{} - if err := proto.Unmarshal(sDec, decodedGymInfo); err != nil { + if err := proto.Unmarshal(response, decodedGymInfo); err != nil { log.Errorf("Failed to parse %s", err) statsCollector.IncDecodeGetGymInfo("error", "parse") - return fmt.Sprintf("Failed to parse %s", err) + return true, fmt.Sprintf("Failed to parse %s", err) } if decodedGymInfo.Result != pogo.GymGetInfoOutProto_SUCCESS { statsCollector.IncDecodeGetGymInfo("error", "non_success") res := fmt.Sprintf(`GymGetInfoOutProto: Ignored non-success value %d:%s`, decodedGymInfo.Result, pogo.GymGetInfoOutProto_Result_name[int32(decodedGymInfo.Result)]) - return res + return true, res } statsCollector.IncDecodeGetGymInfo("ok", "") - return decoder.UpdateGymRecordWithGymInfoProto(ctx, dbDetails, decodedGymInfo) + return true, decoder.UpdateGymRecordWithGymInfoProto(ctx, dbDetails, decodedGymInfo) } -func decodeEncounter(ctx context.Context, sDec []byte, username string) string { +func decodeEncounter(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + if !getScanParameters(protoData).ProcessPokemon { + return true, "" + } + response := protoData.ResponseProtoBytes() + username := protoData.Account + decodedEncounterInfo := &pogo.EncounterOutProto{} - if err := proto.Unmarshal(sDec, decodedEncounterInfo); err != nil { + if err := proto.Unmarshal(response, decodedEncounterInfo); err != nil { log.Errorf("Failed to parse %s", err) statsCollector.IncDecodeEncounter("error", "parse") - return fmt.Sprintf("Failed to parse %s", err) + return true, fmt.Sprintf("Failed to parse %s", err) } if decodedEncounterInfo.Status != pogo.EncounterOutProto_ENCOUNTER_SUCCESS { statsCollector.IncDecodeEncounter("error", "non_success") res := fmt.Sprintf(`GymGetInfoOutProto: Ignored non-success value %d:%s`, decodedEncounterInfo.Status, pogo.EncounterOutProto_Status_name[int32(decodedEncounterInfo.Status)]) - return res + return true, res } statsCollector.IncDecodeEncounter("ok", "") - return decoder.UpdatePokemonRecordWithEncounterProto(ctx, dbDetails, decodedEncounterInfo, username) + return true, decoder.UpdatePokemonRecordWithEncounterProto(ctx, dbDetails, decodedEncounterInfo, username) } -func decodeDiskEncounter(ctx context.Context, sDec []byte, username string) string { - decodedEncounterInfo := &pogo.DiskEncounterOutProto{} - if err := proto.Unmarshal(sDec, decodedEncounterInfo); err != nil { +func decodeDiskEncounter(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + response := protoData.ResponseProtoBytes() + encounterProto := &pogo.DiskEncounterOutProto{} + if err := proto.Unmarshal(response, encounterProto); err != nil { log.Errorf("Failed to parse %s", err) statsCollector.IncDecodeDiskEncounter("error", "parse") - return fmt.Sprintf("Failed to parse %s", err) + return true, fmt.Sprintf("Failed to parse %s", err) } - if decodedEncounterInfo.Result != pogo.DiskEncounterOutProto_SUCCESS { + if encounterProto.Result != pogo.DiskEncounterOutProto_SUCCESS { statsCollector.IncDecodeDiskEncounter("error", "non_success") - res := fmt.Sprintf(`DiskEncounterOutProto: Ignored non-success value %d:%s`, decodedEncounterInfo.Result, - pogo.DiskEncounterOutProto_Result_name[int32(decodedEncounterInfo.Result)]) - return res + res := fmt.Sprintf(`DiskEncounterOutProto: Ignored non-success value %d:%s`, encounterProto.Result, + pogo.DiskEncounterOutProto_Result_name[int32(encounterProto.Result)]) + return true, res } statsCollector.IncDecodeDiskEncounter("ok", "") - return decoder.UpdatePokemonRecordWithDiskEncounterProto(ctx, dbDetails, decodedEncounterInfo, username) + return true, decoder.UpdatePokemonRecordWithDiskEncounterProto( + ctx, dbDetails, encounterProto, protoData.Account, + ) } -func decodeStartIncident(ctx context.Context, sDec []byte) string { +func decodeStartIncident(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + sDec := protoData.ResponseProtoBytes() decodedIncident := &pogo.StartIncidentOutProto{} if err := proto.Unmarshal(sDec, decodedIncident); err != nil { log.Errorf("Failed to parse %s", err) statsCollector.IncDecodeStartIncident("error", "parse") - return fmt.Sprintf("Failed to parse %s", err) + return true, fmt.Sprintf("Failed to parse %s", err) } if decodedIncident.Status != pogo.StartIncidentOutProto_SUCCESS { statsCollector.IncDecodeStartIncident("error", "non_success") res := fmt.Sprintf(`GiovanniOutProto: Ignored non-success value %d:%s`, decodedIncident.Status, pogo.StartIncidentOutProto_Status_name[int32(decodedIncident.Status)]) - return res + return true, res } statsCollector.IncDecodeStartIncident("ok", "") - return decoder.ConfirmIncident(ctx, dbDetails, decodedIncident) + return true, decoder.ConfirmIncident(ctx, dbDetails, decodedIncident) } -func decodeOpenInvasion(ctx context.Context, request []byte, payload []byte) string { +func decodeOpenInvasion(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + request := protoData.RequestProtoBytes() + if request == nil { + return false, "" + } + response := protoData.ResponseProtoBytes() + decodeOpenInvasionRequest := &pogo.OpenInvasionCombatSessionProto{} if err := proto.Unmarshal(request, decodeOpenInvasionRequest); err != nil { log.Errorf("Failed to parse %s", err) statsCollector.IncDecodeOpenInvasion("error", "parse") - return fmt.Sprintf("Failed to parse %s", err) + return true, fmt.Sprintf("Failed to parse %s", err) } if decodeOpenInvasionRequest.IncidentLookup == nil { - return "Invalid OpenInvasionCombatSessionProto received" + return true, "Invalid OpenInvasionCombatSessionProto received" } decodedOpenInvasionResponse := &pogo.OpenInvasionCombatSessionOutProto{} - if err := proto.Unmarshal(payload, decodedOpenInvasionResponse); err != nil { + if err := proto.Unmarshal(response, decodedOpenInvasionResponse); err != nil { log.Errorf("Failed to parse %s", err) statsCollector.IncDecodeOpenInvasion("error", "parse") - return fmt.Sprintf("Failed to parse %s", err) + return true, fmt.Sprintf("Failed to parse %s", err) } if decodedOpenInvasionResponse.Status != pogo.InvasionStatus_SUCCESS { statsCollector.IncDecodeOpenInvasion("error", "non_success") res := fmt.Sprintf(`InvasionLineupOutProto: Ignored non-success value %d:%s`, decodedOpenInvasionResponse.Status, pogo.InvasionStatus_Status_name[int32(decodedOpenInvasionResponse.Status)]) - return res + return true, res } statsCollector.IncDecodeOpenInvasion("ok", "") - return decoder.UpdateIncidentLineup(ctx, dbDetails, decodeOpenInvasionRequest, decodedOpenInvasionResponse) + return true, decoder.UpdateIncidentLineup(ctx, dbDetails, decodeOpenInvasionRequest, decodedOpenInvasionResponse) } -func decodeGMO(ctx context.Context, protoData *ProtoData, scanParameters decoder.ScanParameters) string { +func decodeGMO(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + scanParameters := getScanParameters(protoData) decodedGmo := &pogo.GetMapObjectsOutProto{} - - if err := proto.Unmarshal(protoData.Data, decodedGmo); err != nil { + if err := proto.Unmarshal(protoData.ResponseProtoBytes(), decodedGmo); err != nil { statsCollector.IncDecodeGMO("error", "parse") log.Errorf("Failed to parse %s", err) + return true, fmt.Sprintf("Failed to parse %s", err) } if decodedGmo.Status != pogo.GetMapObjectsOutProto_SUCCESS { statsCollector.IncDecodeGMO("error", "non_success") res := fmt.Sprintf(`GetMapObjectsOutProto: Ignored non-success value %d:%s`, decodedGmo.Status, pogo.GetMapObjectsOutProto_Status_name[int32(decodedGmo.Status)]) - return res + return true, res } var newForts []decoder.RawFortData @@ -831,7 +846,7 @@ func decodeGMO(ctx context.Context, protoData *ProtoData, scanParameters decoder statsCollector.AddDecodeGMOType("weather", float64(newClientWeatherLen)) statsCollector.AddDecodeGMOType("cell", float64(newMapCellsLen)) - return fmt.Sprintf("%d cells containing %d forts %d mon %d nearby", newMapCellsLen, newFortsLen, newWildPokemonLen, newNearbyPokemonLen) + return true, fmt.Sprintf("%d cells containing %d forts %d mon %d nearby", newMapCellsLen, newFortsLen, newWildPokemonLen, newNearbyPokemonLen) } func isCellNotEmpty(mapCell *pogo.ClientMapCellProto) bool { @@ -842,41 +857,52 @@ func cellContainsForts(mapCell *pogo.ClientMapCellProto) bool { return len(mapCell.Fort) > 0 } -func decodeGetContestData(ctx context.Context, request []byte, data []byte) string { +func decodeGetContestData(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + // Request helps, but can be decoded without it + request := protoData.RequestProtoBytes() + response := protoData.ResponseProtoBytes() var decodedContestData pogo.GetContestDataOutProto - if err := proto.Unmarshal(data, &decodedContestData); err != nil { + + if err := proto.Unmarshal(response, &decodedContestData); err != nil { log.Errorf("Failed to parse GetContestDataOutProto %s", err) - return fmt.Sprintf("Failed to parse GetContestDataOutProto %s", err) + return true, fmt.Sprintf("Failed to parse GetContestDataOutProto %s", err) } var decodedContestDataRequest pogo.GetContestDataProto if request != nil { if err := proto.Unmarshal(request, &decodedContestDataRequest); err != nil { log.Errorf("Failed to parse GetContestDataProto %s", err) - return fmt.Sprintf("Failed to parse GetContestDataProto %s", err) + return true, fmt.Sprintf("Failed to parse GetContestDataProto %s", err) } } - return decoder.UpdatePokestopWithContestData(ctx, dbDetails, &decodedContestDataRequest, &decodedContestData) + return true, decoder.UpdatePokestopWithContestData(ctx, dbDetails, &decodedContestDataRequest, &decodedContestData) } -func decodeGetPokemonSizeContestEntry(ctx context.Context, request []byte, data []byte) string { +func decodeGetPokemonSizeContestEntry(ctx context.Context, protoData *raw_decoder.Proto) (bool, string) { + // Request is essential to decode this + request := protoData.RequestProtoBytes() + if request == nil { + return false, "no request" + } + response := protoData.ResponseProtoBytes() + var decodedPokemonSizeContestEntry pogo.GetPokemonSizeContestEntryOutProto - if err := proto.Unmarshal(data, &decodedPokemonSizeContestEntry); err != nil { + if err := proto.Unmarshal(response, &decodedPokemonSizeContestEntry); err != nil { log.Errorf("Failed to parse GetPokemonSizeContestEntryOutProto %s", err) - return fmt.Sprintf("Failed to parse GetPokemonSizeContestEntryOutProto %s", err) + return true, fmt.Sprintf("Failed to parse GetPokemonSizeContestEntryOutProto %s", err) } if decodedPokemonSizeContestEntry.Status != pogo.GetPokemonSizeContestEntryOutProto_SUCCESS { - return fmt.Sprintf("Ignored GetPokemonSizeContestEntryOutProto non-success status %s", decodedPokemonSizeContestEntry.Status) + return true, fmt.Sprintf("Ignored GetPokemonSizeContestEntryOutProto non-success status %s", decodedPokemonSizeContestEntry.Status) } var decodedPokemonSizeContestEntryRequest pogo.GetPokemonSizeContestEntryProto if request != nil { if err := proto.Unmarshal(request, &decodedPokemonSizeContestEntryRequest); err != nil { log.Errorf("Failed to parse GetPokemonSizeContestEntryProto %s", err) - return fmt.Sprintf("Failed to parse GetPokemonSizeContestEntryProto %s", err) + return true, fmt.Sprintf("Failed to parse GetPokemonSizeContestEntryProto %s", err) } } - return decoder.UpdatePokestopWithPokemonSizeContestEntry(ctx, dbDetails, &decodedPokemonSizeContestEntryRequest, &decodedPokemonSizeContestEntry) + return true, decoder.UpdatePokestopWithPokemonSizeContestEntry(ctx, dbDetails, &decodedPokemonSizeContestEntryRequest, &decodedPokemonSizeContestEntry) } diff --git a/raw_decoder/raw_decoder.go b/raw_decoder/raw_decoder.go new file mode 100644 index 00000000..13f927a3 --- /dev/null +++ b/raw_decoder/raw_decoder.go @@ -0,0 +1,317 @@ +package raw_decoder + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "golbat/grpc" + "golbat/pogo" + "net/http" + "time" + + log "github.com/sirupsen/logrus" +) + +type RawDecoder interface { + GetProtoDataFromHTTP(http.Header, []byte) (*ProtoData, error) + GetProtoDataFromGRPC(*grpc.RawProtoRequest) *ProtoData + Decode(context.Context, *ProtoData, func(context.Context, *Proto)) +} + +type ProtoData struct { + *CommonData + Protos []Proto +} + +func (pd ProtoData) Lat() float64 { + l := len(pd.Protos) + if l == 0 { + return 0 + } + return pd.Protos[l-1].Lat +} + +func (pd ProtoData) Lon() float64 { + l := len(pd.Protos) + if l == 0 { + return 0 + } + return pd.Protos[l-1].Lon +} + +type CommonData struct { + Account string + Level int + Uuid string + ScanContext string +} + +type Proto struct { + *CommonData + Method int + HaveAr *bool + Lat float64 + Lon float64 + base64Request string + base64Response string + requestBytes []byte + responseBytes []byte +} + +func (pd *Proto) RequestProtoBytes() []byte { + if pd.requestBytes != nil { + return pd.requestBytes + } + if pd.base64Request == "" { + return nil + } + reqBytes, err := base64.StdEncoding.DecodeString(pd.base64Request) + if err != nil { + return nil + } + pd.requestBytes = reqBytes + return reqBytes +} + +func (pd *Proto) ResponseProtoBytes() []byte { + if pd.responseBytes != nil { + return pd.responseBytes + } + if pd.base64Response == "" { + return nil + } + respBytes, err := base64.StdEncoding.DecodeString(pd.base64Response) + if err != nil { + return nil + } + pd.responseBytes = respBytes + return respBytes +} + +var _ RawDecoder = (*rawDecoder)(nil) + +type rawDecoder struct { + decodeTimeout time.Duration +} + +func (dec *rawDecoder) parsePogodroidBody(headers http.Header, body []byte, origin string) (*ProtoData, error) { + const arQuestId = int(pogo.QuestType_QUEST_GEOTARGETED_AR_SCAN) + + type pogoDroidRawEntry struct { + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` + Payload string `json:"payload"` + Type int `json:"type"` + QuestsHeld []int `json:"quests_held"` + } + + var entries []pogoDroidRawEntry + + if err := json.Unmarshal(body, &entries); err != nil { + return nil, err + } + + commonData := &CommonData{ + Uuid: origin, + Account: "Pogodroid", + Level: 30, + } + + protos := make([]Proto, len(entries)) + + for entryIdx, entry := range entries { + var lat, lon float64 + + if entry.Lat != 0 && entry.Lng != 0 { + lat = entry.Lat + lon = entry.Lng + } + + var haveAr *bool + + if entry.QuestsHeld != nil { + for _, quest_id := range entry.QuestsHeld { + if quest_id == arQuestId { + value := true + haveAr = &value + break + } + } + if haveAr == nil { + value := false + haveAr = &value + } + } + + protos[entryIdx] = Proto{ + CommonData: commonData, + base64Response: entry.Payload, + Method: entry.Type, + HaveAr: haveAr, + Lat: lat, + Lon: lon, + } + } + + return &ProtoData{ + CommonData: commonData, + Protos: protos, + }, nil +} + +func decodeAlternate(data map[string]interface{}, key1, key2 string) interface{} { + if v := data[key1]; v != nil { + return v + } + if v := data[key2]; v != nil { + return v + } + return nil +} + +func (dec *rawDecoder) GetProtoDataFromHTTP(headers http.Header, body []byte) (*ProtoData, error) { + if origin := headers.Get("origin"); origin != "" { + return dec.parsePogodroidBody(headers, body, origin) + } + + var raw map[string]any + + if err := json.Unmarshal(body, &raw); err != nil { + return nil, err + } + + commonData := &CommonData{ + Level: 30, + } + + baseProto := Proto{ + CommonData: commonData, + } + + if v := raw["have_ar"]; v != nil { + res, ok := v.(bool) + if ok { + baseProto.HaveAr = &res + } + } + if v := raw["uuid"]; v != nil { + baseProto.Uuid, _ = v.(string) + } + if v := raw["username"]; v != nil { + baseProto.Account, _ = v.(string) + } + if v := raw["trainerlvl"]; v != nil { + lvl, ok := v.(float64) + if ok { + baseProto.Level = int(lvl) + } + } + if v := raw["scan_context"]; v != nil { + baseProto.ScanContext, _ = v.(string) + } + if v := raw["lat_target"]; v != nil { + baseProto.Lat, _ = v.(float64) + } + if v := raw["lon_target"]; v != nil { + baseProto.Lon, _ = v.(float64) + } + + contents, ok := raw["contents"].([]any) + if !ok { + return nil, errors.New("failed to decode 'contents'") + } + + var protos []Proto + + for _, v := range contents { + entry, ok := v.(map[string]any) + if !ok { + continue + } + // Try to decode the payload automatically without requiring any knowledge of the + // provider type + + base64data := decodeAlternate(entry, "data", "payload") + method := decodeAlternate(entry, "method", "type") + if method == nil || base64data == nil { + log.Errorf("Error decoding raw (no method or base64data)") + continue + } + + proto := baseProto + proto.base64Response, _ = base64data.(string) + proto.Method = func() int { + if res, ok := method.(float64); ok { + return int(res) + } + return 0 + }() + + if request := entry["request"]; request != nil { + proto.base64Request, _ = request.(string) + } + if haveAr := entry["have_ar"]; haveAr != nil { + res, ok := haveAr.(bool) + if ok { + proto.HaveAr = &res + } + } + + protos = append(protos, proto) + } + return &ProtoData{ + CommonData: commonData, + Protos: protos, + }, nil +} + +func (dec *rawDecoder) GetProtoDataFromGRPC(in *grpc.RawProtoRequest) *ProtoData { + commonData := &CommonData{ + Uuid: in.DeviceId, + Account: in.Username, + Level: int(in.TrainerLevel), + } + + baseProto := Proto{ + CommonData: commonData, + Lat: float64(in.LatTarget), + Lon: float64(in.LonTarget), + HaveAr: in.HaveAr, + } + + if in.ScanContext != nil { + baseProto.ScanContext = *in.ScanContext + } + + protos := make([]Proto, len(in.Contents)) + for i, v := range in.Contents { + proto := baseProto + proto.Method = int(v.Method) + proto.requestBytes = v.RequestPayload + proto.responseBytes = v.ResponsePayload + if v.HaveAr != nil { + proto.HaveAr = v.HaveAr + } + protos[i] = proto + } + return &ProtoData{ + CommonData: commonData, + Protos: protos, + } +} + +func (dec *rawDecoder) Decode(ctx context.Context, protoData *ProtoData, decodeFn func(context.Context, *Proto)) { + for _, proto := range protoData.Protos { + // provide independent cancellation contexts for each proto decode + ctx, cancel := context.WithTimeout(ctx, dec.decodeTimeout) + decodeFn(ctx, &proto) + cancel() + } +} + +func NewRawDecoder(decodeTimeout time.Duration) RawDecoder { + return &rawDecoder{ + decodeTimeout: decodeTimeout, + } +} diff --git a/routes.go b/routes.go index 54cf7219..759577f4 100644 --- a/routes.go +++ b/routes.go @@ -2,8 +2,6 @@ package main import ( "context" - b64 "encoding/base64" - "encoding/json" "io" "net/http" "strconv" @@ -14,53 +12,13 @@ import ( "golbat/config" "golbat/decoder" + "golbat/device_tracker" "golbat/geo" - "golbat/pogo" + "golbat/raw_decoder" ) -type ProtoData struct { - Method int - Data []byte - Request []byte - HaveAr *bool - Account string - Level int - Uuid string - ScanContext string - Lat float64 - Lon float64 -} - -type InboundRawData struct { - Base64Data string - Request string - Method int - HaveAr *bool -} - -func questsHeldHasARTask(quests_held any) *bool { - const ar_quest_id = int64(pogo.QuestType_QUEST_GEOTARGETED_AR_SCAN) - - quests_held_list, ok := quests_held.([]any) - if !ok { - log.Errorf("Raw: unexpected quests_held type in data: %T", quests_held) - return nil - } - for _, quest_id := range quests_held_list { - if quest_id_f, ok := quest_id.(float64); ok { - if int64(quest_id_f) == ar_quest_id { - res := true - return &res - } - continue - } - // quest_id is not float64? Treat the whole thing as unknown. - log.Errorf("Raw: unexpected quest_id type in quests_held: %T", quest_id) - return nil - } - res := false - return &res -} +var rawProtoDecoder raw_decoder.RawDecoder +var deviceTracker device_tracker.DeviceTracker func Raw(c *gin.Context) { var w http.ResponseWriter = c.Writer @@ -87,212 +45,20 @@ func Raw(c *gin.Context) { return } - decodeError := false - uuid := "" - account := "" - level := 30 - scanContext := "" - var latTarget, lonTarget float64 - var globalHaveAr *bool - var protoData []InboundRawData - - // Objective is to normalise incoming proto data. Unfortunately each provider seems - // to be just different enough that this ends up being a little bit more of a mess - // than I would like - - pogodroidHeader := r.Header.Get("origin") - userAgent := r.Header.Get("User-Agent") - - //log.Infof("Raw: Received data from %s", body) - //log.Infof("User agent is %s", userAgent) - - if pogodroidHeader != "" { - var raw []map[string]interface{} - if err := json.Unmarshal(body, &raw); err != nil { - decodeError = true - } else { - for _, entry := range raw { - if latTarget == 0 && lonTarget == 0 { - lat := entry["lat"] - lng := entry["lng"] - if lat != nil && lng != nil { - lat_f, _ := lat.(float64) - lng_f, _ := lng.(float64) - if lat_f != 0 && lng_f != 0 { - latTarget = lat_f - lonTarget = lng_f - } - } - } - protoData = append(protoData, InboundRawData{ - Base64Data: entry["payload"].(string), - Method: int(entry["type"].(float64)), - HaveAr: func() *bool { - if v := entry["quests_held"]; v != nil { - return questsHeldHasARTask(v) - } - return nil - }(), - }) - } - } - uuid = pogodroidHeader - account = "Pogodroid" - } else { - var raw map[string]interface{} - if err := json.Unmarshal(body, &raw); err != nil { - decodeError = true - } else { - if v := raw["have_ar"]; v != nil { - res, ok := v.(bool) - if ok { - globalHaveAr = &res - } - } - if v := raw["uuid"]; v != nil { - uuid, _ = v.(string) - } - if v := raw["username"]; v != nil { - account, _ = v.(string) - } - if v := raw["trainerlvl"]; v != nil { - lvl, ok := v.(float64) - if ok { - level = int(lvl) - } - } - if v := raw["scan_context"]; v != nil { - scanContext, _ = v.(string) - } - - if v := raw["lat_target"]; v != nil { - latTarget, _ = v.(float64) - } - - if v := raw["lon_target"]; v != nil { - lonTarget, _ = v.(float64) - } - - contents, ok := raw["contents"].([]interface{}) - if !ok { - decodeError = true - - } else { - - decodeAlternate := func(data map[string]interface{}, key1, key2 string) interface{} { - if v := data[key1]; v != nil { - return v - } - if v := data[key2]; v != nil { - return v - } - return nil - } - - for _, v := range contents { - entry := v.(map[string]interface{}) - // Try to decode the payload automatically without requiring any knowledge of the - // provider type - - b64data := decodeAlternate(entry, "data", "payload") - method := decodeAlternate(entry, "method", "type") - request := entry["request"] - haveAr := entry["have_ar"] - - if method == nil || b64data == nil { - log.Errorf("Error decoding raw") - continue - } - inboundRawData := InboundRawData{ - Base64Data: func() string { - if res, ok := b64data.(string); ok { - return res - } - return "" - }(), - Method: func() int { - if res, ok := method.(float64); ok { - return int(res) - } - - return 0 - }(), - Request: func() string { - if request != nil { - res, ok := request.(string) - if ok { - return res - } - } - return "" - }(), - HaveAr: func() *bool { - if haveAr != nil { - res, ok := haveAr.(bool) - if ok { - return &res - } - } - return nil - }(), - } - - protoData = append(protoData, inboundRawData) - } - } - } - } - - if decodeError == true { + protoData, err := rawProtoDecoder.GetProtoDataFromHTTP(r.Header, body) + if err != nil { statsCollector.IncRawRequests("error", "decode") - log.Infof("Raw: Data could not be decoded. From User agent %s - Received data %s", userAgent, body) - + userAgent := r.Header.Get("User-Agent") + log.Infof("Raw: Data could not be decoded. From User agent %s - Received data %s, err: %s", userAgent, body, err) w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusUnprocessableEntity) return } // Process each proto in a packet in sequence, but in a go-routine - go func() { - timeout := 5 * time.Second - if config.Config.Tuning.ExtendedTimeout { - timeout = 30 * time.Second - } + go rawProtoDecoder.Decode(context.Background(), protoData, decode) - for _, entry := range protoData { - method := entry.Method - payload := entry.Base64Data - request := entry.Request - - haveAr := globalHaveAr - if entry.HaveAr != nil { - haveAr = entry.HaveAr - } - - protoData := ProtoData{ - Account: account, - Level: level, - HaveAr: haveAr, - Uuid: uuid, - Lat: latTarget, - Lon: lonTarget, - ScanContext: scanContext, - } - protoData.Data, _ = b64.StdEncoding.DecodeString(payload) - if request != "" { - protoData.Request, _ = b64.StdEncoding.DecodeString(request) - } - - // provide independent cancellation contexts for each proto decode - ctx, cancel := context.WithTimeout(context.Background(), timeout) - decode(ctx, method, &protoData) - cancel() - } - }() - - if latTarget != 0 && lonTarget != 0 && uuid != "" { - UpdateDeviceLocation(uuid, latTarget, lonTarget, scanContext) - } + deviceTracker.UpdateDeviceLocation(protoData.Uuid, protoData.Lat(), protoData.Lon(), protoData.ScanContext) statsCollector.IncRawRequests("ok", "") w.Header().Set("Content-Type", "application/json; charset=UTF-8") @@ -530,5 +296,5 @@ func GetPokestop(c *gin.Context) { } func GetDevices(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"devices": GetAllDevices()}) + c.JSON(http.StatusOK, gin.H{"devices": deviceTracker.GetAllDevices()}) }