Initial cloud-services repo - gateway service + pkg modules

This commit is contained in:
Chris Rai
2026-01-30 23:14:52 -05:00
commit fbb820d7b3
1037 changed files with 171318 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
ARG BASE_IMAGE=cloud_base_go
FROM ${BASE_IMAGE} as builder-go
WORKDIR /build/gateway_go
COPY ./gateway_go/go.mod ./gateway_go/go.sum ./
RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \
&& go mod download
COPY ./gateway_go ./
RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \
&& go build -tags musl
FROM alpine:3.17
RUN apk add --no-cache librdkafka --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \
&& apk add --no-cache ca-certificates
COPY ./modules_go/logger/log_config .
ENV LOG_CONFIG=log_config
COPY ./gateway_go/docs ./docs
COPY --from=builder-go /build/gateway_go/gateway .
EXPOSE 8077
CMD ./gateway

View File

@@ -0,0 +1,15 @@
package controllers
import (
"fiskerinc.com/modules/health"
"fiskerinc.com/modules/logger"
)
func HealthCheck() {
server := health.HealthCheckServer{}
err := server.Serve([]health.Config{})
if err != nil {
logger.Error().Err(err).Send()
}
}

View File

@@ -0,0 +1,11 @@
# Generate Async API Docs
```
npm install -g @asyncapi/generator
```
```
ag asyncapi_hmi.yaml @asyncapi/html-template -o static/hmi
ag asyncapi_mobile.yaml @asyncapi/html-template -o static/secret_mobile
ag asyncapi_trex.yaml @asyncapi/html-template -o static/trex
```

View File

@@ -0,0 +1,331 @@
asyncapi: 2.0.0
info:
title: HMI Websocket API
version: 1.0.0
description: This serves as the documentation for websocket connections made between the HMI and the gateway.
servers:
local:
url: localhost/secret_mobile
description: local
protocol: wss
development:
url: dev-gw.cloud.fiskerinc.com/secret_mobile
description: dev
protocol: wss
preproduction:
url: gw.cloud.fiskerinc.com/secret_mobile
description: preprod
protocol: wss
production:
url: gw.cec-prd.fiskerinc.com/secret_mobile
description: prod
protocol: wss
production-eu:
url: gw.cec-euprd.fiskerinc.com/secret_mobile
description: prod-eu
protocol: wss
channels:
Verify:
publish:
message:
$ref: '#/components/messages/Verify'
subscribe:
message:
$ref: '#/components/messages/VerifyResponse'
SessionId:
subscribe:
message:
$ref: '#/components/messages/SessionId'
MapDestination:
subscribe:
message:
$ref: '#/components/messages/MapDestinationSource'
MapRoute:
subscribe:
message:
$ref: '#/components/messages/MapRouteSource'
Profiles:
publish:
message:
$ref: '#/components/messages/Profiles'
subscribe:
message:
$ref: '#/components/messages/ProfilesResponse'
ProfileNew:
subscribe:
message:
$ref: '#/components/messages/ProfileNew'
ProfileUpdate:
subscribe:
message:
$ref: '#/components/messages/ProfileUpdate'
ProfileDelete:
subscribe:
message:
$ref: '#/components/messages/ProfileDelete'
SettingsUpdate:
publish:
message:
$ref: '#/components/messages/SettingsUpdate'
subscribe:
message:
$ref: '#/components/messages/SettingsUpdateNotification'
SubscriptionsUpdate:
subscribe:
message:
$ref: '#/components/messages/SubscriptionsUpdate'
UpdateManifest:
subscribe:
message:
$ref: '#/components/messages/UpdateManifest'
UpdateApprove:
publish:
message:
$ref: '#/components/messages/UpdateApprove'
CarUpdate:
subscribe:
message:
$ref: '#/components/messages/CarUpdate'
Error:
subscribe:
message:
$ref: '#/components/messages/Error'
components:
messages:
Verify:
description: Sent by the HMI to authenticate itself with the cloud. Fields vary depending on whether the HMI already has a driver profile.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "verify"
const: verify
data:
$ref: './schema/hmi/TXMessage.json#/$defs/VerifyModel'
VerifyResponse:
description: Received by the HMI after sending Verify.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "verify"
const: verify
data:
$ref: './schema/hmi/RXMessage.json#/$defs/VerifyResponseModel'
SessionId:
description: Provides a session ID unique to the HMI.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "session_id"
const: session_id
data:
$ref: './schema/hmi/RXMessage.json#/$defs/SessionIdModel'
MapDestinationSource:
description: Provides map destination to the HMI sent from mobile.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "map_destination"
const: map_destination
data:
$ref: './schema/hmi/RXMessage.json#/$defs/MapDestinationSourceModel'
MapRouteSource:
description: Provides map route to the HMI sent from mobile.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "map_route"
const: map_route
data:
$ref: './schema/hmi/RXMessage.json#/$defs/MapRouteSourceModel'
Profiles:
description: Prompts the cloud to send down all driver profiles on the car.
payload:
type: object
required:
- handler
properties:
handler:
type: string
description: expects the string "profiles"
const: profiles
ProfilesResponse:
description: Provides an updated version of all driver profiles on the car.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "profiles"
const: profiles
data:
type: array
description: stores data for response
items:
$ref: './schema/hmi/RXMessage.json#/$defs/CarToDriverModel'
ProfileNew:
description: Provides a single driver profile to be added on the car.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "profile_new"
const: profile_new
data:
$ref: './schema/hmi/RXMessage.json#/$defs/CarToDriverModel'
ProfileUpdate:
description: Provides an updated version of a single driver profile on the car.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "profile_update"
const: profile_update
data:
$ref: './schema/hmi/RXMessage.json#/$defs/CarToDriverUpdateModel'
ProfileDelete:
description: Describes the driver profile to remove from HMI.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "profile_delete"
const: profile_delete
data:
$ref: './schema/hmi/RXMessage.json#/$defs/CarToDriverDeleteModel'
SettingsUpdate:
description: Notifies the cloud of a change in settings on the HMI.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "settings_update"
const: settings_update
data:
$ref: './schema/hmi/TXMessage.json#/$defs/SettingsUpdateModel'
SettingsUpdateNotification:
description: Notifies the HMI of a change in settings.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "settings_update"
const: settings_update
data:
$ref: './schema/hmi/RXMessage.json#/$defs/SettingsUpdateModel'
SubscriptionsUpdate:
description: Notifies the HMI of a change in a subscription.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "subscriptions_update"
const: subscriptions_update
data:
$ref: './schema/hmi/RXMessage.json#/$defs/SubscriptionsUpdateModel'
UpdateManifest:
description: Provides a summary of an OTA update event.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "update_manifest"
const: update_manifest
data:
$ref: './schema/hmi/RXMessage.json#/$defs/UpdateManifest'
UpdateApprove:
description: Notifies the cloud that an update has been approved and can be downloaded.
payload:
type: object
required:
- handler
properties:
handler:
type: string
description: expects the string "update_approve"
const: update_approve
CarUpdate:
description: Provides a status update on a specific car update.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "car_update"
const: car_update
data:
$ref: './schema/hmi/RXMessage.json#/$defs/UpdateProgressModel'
Error:
description: Generic error message
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "error"
const: error
data:
$ref: './schema/hmi/RXMessage.json#/$defs/ErrorModel'

View File

@@ -0,0 +1,447 @@
asyncapi: 2.0.0
info:
title: Mobile Websocket API
version: 1.0.0
description: This serves as the documentation for websocket connections made between the mobile app and the gateway.
servers:
local:
url: localhost/secret_mobile
description: local
protocol: wss
development:
url: dev-gw.cloud.fiskerinc.com/secret_mobile
description: dev
protocol: wss
preproduction:
url: gw.cloud.fiskerinc.com/secret_mobile
description: preprod
protocol: wss
production:
url: gw.cec-prd.fiskerinc.com/secret_mobile
description: prod
protocol: wss
production-eu:
url: gw.cec-euprd.fiskerinc.com/secret_mobile
description: prod-eu
protocol: wss
channels:
Verify:
publish:
message:
$ref: '#/components/messages/Verify'
subscribe:
message:
$ref: '#/components/messages/VerifyResponse'
DigitalTwin:
publish:
message:
$ref: '#/components/messages/DigitalTwinRequest'
subscribe:
message:
$ref: '#/components/messages/DigitalTwin'
MapDestination:
publish:
message:
$ref: '#/components/messages/MapDestinationRequest'
MapRoute:
publish:
message:
$ref: '#/components/messages/MapRouteRequest'
Profiles:
publish:
message:
$ref: '#/components/messages/Profiles'
subscribe:
message:
$ref: '#/components/messages/ProfilesResponse'
StoreInventory:
publish:
message:
$ref: '#/components/messages/StoreInventory'
subscribe:
message:
$ref: '#/components/messages/StoreInventoryResponse'
StoreInventoryError:
subscribe:
message:
$ref: '#/components/messages/StoreInventoryError'
StorePurchase:
publish:
message:
$ref: '#/components/messages/StorePurchase'
StorePurchaseError:
subscribe:
message:
$ref: '#/components/messages/StorePurchaseError'
SettingsUpdate:
publish:
message:
$ref: '#/components/messages/SettingsUpdate'
subscribe:
message:
$ref: '#/components/messages/SettingsUpdateNotification'
SubscriptionsUpdate:
subscribe:
message:
$ref: '#/components/messages/SubscriptionsUpdate'
Updates:
publish:
message:
$ref: '#/components/messages/UpdatesGet'
subscribe:
message:
$ref: '#/components/messages/Updates'
UpdateApprove:
publish:
message:
$ref: '#/components/messages/UpdateApprove'
CarUpdate:
subscribe:
message:
$ref: '#/components/messages/CarUpdate'
CarLocations:
publish:
message:
$ref: '#/components/messages/CarLocationsRequest'
subscribe:
message:
$ref: '#/components/messages/CarLocations'
Error:
subscribe:
message:
$ref: '#/components/messages/Error'
components:
messages:
Verify:
description: Sent by mobile to authenticate with the cloud.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "verify"
const: verify
data:
$ref: './schema/mobile/TXMessage.json#/$defs/VerifyModel'
VerifyResponse:
description: Recieved by mobile after authenticating.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "verify"
const: verify
data:
$ref: './schema/mobile/RXMessage.json#/$defs/VerifyResponseModel'
MapDestinationRequest:
description: Sends map destination to the cloud for the HMI.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "map_destination"
const: map_destination
data:
$ref: './schema/mobile/TXMessage.json#/$defs/MapDestinationRequestModel'
MapRouteRequest:
description: Sends map route to the cloud for the HMI.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "map_route"
const: map_route
data:
$ref: './schema/mobile/TXMessage.json#/$defs/MapRouteRequestModel'
DigitalTwinRequest:
description: Sent by mobile to retrieve digital twin data for specified VIN.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "digital_twin"
data:
$ref: './schema/mobile/TXMessage.json#/$defs/DigitalTwinRequestModel'
DigitalTwin:
description: Sent to mobile of digital twin data for specified VIN.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "digital_twin"
data:
$ref: './schema/mobile/RXMessage.json#/$defs/DigitalTwinModel'
Profiles:
description: Prompts the cloud to send down all profiles on all cars for the user.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "profiles"
const: profiles
ProfilesResponse:
description: Provides an updated version of all profiles on all cars for the user.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "profiles"
const: profiles
data:
type: array
description: an array of ProfileModels
items:
$ref: './schema/mobile/RXMessage.json#/$defs/ProfileModel'
StoreInventory:
description: Sent by mobile to ask the cloud for store inventory.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "store_inventory"
const: store_inventory
StoreInventoryResponse:
description: Recieved by mobile after asking the cloud for store inventory.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "store_inventory"
const: store_inventory
data:
type: array
description: an array of StoreInventoryModels
items:
$ref: './schema/mobile/RXMessage.json#/$defs/StoreInventoryModel'
StoreInventoryError:
description: Received by mobile if there is an error retrieving store inventory.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "store_inventory_error"
const: store_inventory_error
data:
$ref: './schema/mobile/RXMessage.json#/$defs/ErrorModel'
StorePurchase:
description: Notifies the cloud of a subscription purchase from mobile.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "store_purchase"
const: store_purchase
data:
$ref: './schema/mobile/TXMessage.json#/$defs/StorePurchaseModel'
StorePurchaseError:
description: Received by mobile if there is an error purchasing a subscription.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "store_purchase_error"
const: store_purchase_error
data:
$ref: './schema/mobile/RXMessage.json#/$defs/ErrorModel'
SettingsUpdate:
description: Notifies the cloud of a setting change from mobile.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "settings_update"
const: settings_update
data:
$ref: './schema/mobile/TXMessage.json#/$defs/SettingsUpdateModel'
SettingsUpdateNotification:
description: Notifies the mobile of a change in settings.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "settings_update"
const: settings_update
data:
$ref: './schema/mobile/RXMessage.json#/$defs/SettingsUpdateModel'
SubscriptionsUpdate:
description: Notifies the mobile of a succesful change in a subscription.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "subscriptions_update"
const: subscriptions_update
data:
$ref: './schema/mobile/RXMessage.json#/$defs/SubscriptionsUpdateModel'
UpdatesGet:
description: Sent by mobile to recieve a summary of all available OTA updates.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "updates_get"
const: updates_get
data:
$ref: './schema/mobile/TXMessage.json#/$defs/GetUpdatesModel'
Updates:
description: Received by mobile as a summary of all available OTA updates.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "updates"
const: updates
data:
type: array
description: stores data for response
items:
$ref: './schema/mobile/RXMessage.json#/$defs/UpdateManifest'
UpdateApprove:
description: Sent by mobile to grant approval to download and install an OTA update.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "update_approve"
const: update_approve
CarUpdate:
description: Provides a status update on a specific car update.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "car_update"
const: car_update
data:
$ref: './schema/mobile/RXMessage.json#/$defs/UpdateProgressModel'
CarLocationsRequest:
description: Provides a status update on a specific car update.
payload:
type: object
required:
- handler
properties:
handler:
type: string
description: expects the string "car_locations"
const: car_locations
CarLocations:
description: Provides a status update on a specific car update.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "car_locations"
const: car_locations
data:
type: array
description: provides car locations with their respective VIN
$ref: './schema/mobile/RXMessage.json#/$defs/CarLocationModel'
Error:
description: Generic error message
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "error"
const: error
data:
$ref: './schema/mobile/RXMessage.json#/$defs/ErrorModel'

View File

@@ -0,0 +1,212 @@
asyncapi: 2.0.0
info:
title: T-Rex Websocket API
version: 1.0.1
description: This serves as the documentation for websocket connections made between the T-Rex and the gateway.
servers:
local:
url: localhost/session
description: local
protocol: wss
development:
url: dev-sec-gw.cloud.fiskerinc.com/session
description: dev
protocol: wss
stage:
url: stg-sec-gw.cloud.fiskerinc.com/session
description: stage
protocol: wss
preproduction:
url: sec-gw.cloud.fiskerinc.com/session
description: preprod
protocol: wss
production:
url: sec-gw.cec-prd.fiskerinc.com/session
description: prod
protocol: wss
production-eu:
url: sec-gw.cec-euprd.fiskerinc.com/session
description: prod-eu
protocol: wss
channels:
config:
subscribe:
message:
$ref: '#/components/messages/config'
filekeys:
subscribe:
message:
$ref: '#/components/messages/filekeys'
remote_command:
subscribe:
message:
$ref: '#/components/messages/remote_command'
update_manifest:
subscribe:
message:
$ref: '#/components/messages/update_manifest'
canbus(pub):
publish:
message:
$ref: '#/components/messages/canbus(pub)'
car_state:
publish:
message:
$ref: '#/components/messages/car_state'
car_update_status:
publish:
message:
$ref: '#/components/messages/car_update_status'
get_filekeys:
publish:
message:
$ref: '#/components/messages/get_filekeys'
error:
publish:
message:
$ref: '#/components/messages/error(pub)'
subscribe:
message:
$ref: '#/components/messages/error(sub)'
components:
messages:
config:
description: Message containing configuration information. Passed down upon connection to gateway.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
const: config
data:
$ref: './schema/trex/RXMessage.json#/$defs/Config'
filekeys:
description: A message containing file keys for OTA update flow. Triggered by publishing 'get_filekeys' message.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
const: filekeys
data:
$ref: './schema/trex/RXMessage.json#/$defs/FileKeyResponse'
remote_command:
description: A message containing remote car command
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
const: remote_command
data:
$ref: './schema/trex/RXMessage.json#/$defs/RemoteCommand'
update_manifest:
description: Manifest describing an OTA update.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
const: update_manifest
data:
$ref: './schema/trex/RXMessage.json#/$defs/UpdateManifest'
canbus(pub):
description: CAN message batch.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
const: canbus
data:
type: array
items:
$ref: './schema/trex/TXMessage.json#/$defs/CanFrame'
car_state:
description: Current car state, describes ECU versions etc.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
const: car_state
data:
$ref: './schema/trex/TXMessage.json#/$defs/CarStateUpdate'
car_update_status:
description: Update status for an OTA update.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
const: car_update_status
data:
$ref: './schema/trex/TXMessage.json#/$defs/CarUpdateProgress'
get_filekeys:
description: Request file keys to decrypt OTA update.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
const: get_filekeys
data:
$ref: './schema/trex/TXMessage.json#/$defs/FileKeysRequest'
error(pub):
description: Report an error.
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
const: error
data:
$ref: './schema/trex/TXMessage.json#/$defs/Error'
error(sub):
description: Generic error message
payload:
type: object
required:
- handler
- data
properties:
handler:
type: string
description: expects the string "error"
const: error
data:
$ref: './schema/trex/RXMessage.json#/$defs/ErrorModel'

View File

@@ -0,0 +1 @@
../../3rdparty/common/schema

View File

@@ -0,0 +1,159 @@
components:
schemas:
CarToDriverModel:
type: object
properties:
user:
type: object
properties:
given_name:
type: string
description: first name associated with the user
family_name:
type: string
description: last name associated with the user
email:
type: string
description: email associated with the user
phone:
type: string
description: phone number associated with the user
driver_id:
type: string
description: a unique ID linking the user to the car
role:
type: string
description: the role of the user in the car
settings:
type: string
description: settings associated with the user on the car
UpdateManifest:
type: object
properties:
name:
type: string
description: name of the update
version:
type: string
description: version of the update
description:
type: string
description: description of the update
release_notes:
type: string
description: release notes for the updates
ecu_updates:
type: array
description: the ECU updates that comprise the car update
items:
$ref: "shared.yaml#/components/schemas/EcuUpdate"
car_update_id:
type: string
format: time
description: ID of the car update
created:
type: string
format: time
description: when this update manifest was created
updated:
type: string
format: time
description: when this update manifest was last updated
EcuUpdate:
type: object
properties:
update_file_id:
type: string
description: ID of the update file
manifest_id:
type: integer
description: ID of the manifest
name:
type: string
description: name of the ECU update
part_number:
type: string
description: part number to be updated
update_version:
type: string
description: version of the ECU update
filename:
type: string
description: name of the ECU update file
update_url:
type: string
description: URL to download the ECU update file from
update_size:
type: integer
description: size of the ECU update in bytes
created:
type: string
description: when the ECU update file was created
updated:
type: string
description: when the ECU update file was last updated
UpdateProgressModel:
type: object
properties:
car_update_id:
type: integer
description: ID of the update associated with this progress message
ecu:
type: string
description: ecu currently being updated
file_current:
type: integer
description: downloaded amount of file in bytes
file_total:
type: integer
description: total download amout of file in bytes
package_current:
type: integer
description: downloaded amount of package in bytes
package_total:
type: integer
description: total download amount of package in bytes
installed:
type: integer
description: number of packages installed
total_files:
type: integer
description: number of packages to be installed in update
msg:
type: integer
description: message of the update
enum:
- download_start
- downloading
- download_complete
- download_error
- install_start
- installing
- install_complete
- install_error
err:
type: integer
description: optional error code associated with the update
CarCommand:
type: object
properties:
car_command_locks:
$ref: "#/components/schemas/CarCommandLocks"
CarCommandLocks:
type: object
properties:
left_front:
type: string
description: command for the left front lock
right_front:
type: string
description: command for the right front lock
left_rear:
type: string
description: command for the left rear lock
right_rear:
type: string
description: command for the right rear lock
trunk:
type: string
description: command for the trunk

124
services/gateway/go.mod Normal file
View File

@@ -0,0 +1,124 @@
module github.com/fiskerinc/cloud-services/services/gateway
go 1.24
toolchain go1.24.3
require (
github.com/fiskerinc/cloud-services/pkg v0.0.0
github.com/gobwas/httphead v0.1.0
github.com/gobwas/ws v1.2.1
github.com/pkg/errors v0.9.1
google.golang.org/protobuf v1.36.1
gopkg.in/DataDog/dd-trace-go.v1 v1.60.1
)
require (
github.com/DataDog/go-libddwaf/v2 v2.2.3 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.25.0 // indirect
github.com/redis/go-redis/v9 v9.5.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
)
require (
github.com/DataDog/appsec-internal-go v1.4.0 // indirect
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 // indirect
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 // indirect
github.com/DataDog/datadog-go/v5 v5.3.0 // indirect
github.com/DataDog/go-tuf v1.0.2-0.5.2 // indirect
github.com/DataDog/sketches-go v1.4.2 // indirect
github.com/Fisker-Inc/project-ai-can-go v1.3.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
github.com/albenik/bcd v0.0.0-20170831201648-635201416bc7 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.5.2 // indirect
github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-pg/pg/v10 v10.11.1 // indirect
github.com/go-pg/zerochecker v0.2.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.1 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/mock v1.7.0-rc.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v1.8.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/schema v1.2.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/jeremywohl/flatten v1.0.1 // indirect
github.com/jinzhu/copier v0.3.5 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/klauspost/compress v1.17.1 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx v1.2.25 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/outcaste-io/ristretto v0.2.3 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/robfig/cron v1.2.0
github.com/rs/zerolog v1.29.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f // indirect
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect
github.com/swaggo/http-swagger v1.3.3 // indirect
github.com/swaggo/swag v1.8.8 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/vmihailenco/bufpool v0.1.11 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser v0.1.2 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.24.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect
mellium.im/sasl v0.3.1 // indirect
)
replace github.com/fiskerinc/cloud-services/pkg => ../../pkg

524
services/gateway/go.sum Normal file
View File

@@ -0,0 +1,524 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/appsec-internal-go v1.4.0 h1:KFI8ElxkJOgpw+cUm9TXK/jh5EZvRaWM07sXlxGg9Ck=
github.com/DataDog/appsec-internal-go v1.4.0/go.mod h1:ONW8aV6R7Thgb4g0bB9ZQCm+oRgyz5eWiW7XoQ19wIc=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1/go.mod h1:Vc+snp0Bey4MrrJyiV2tVxxJb6BmLomPvN1RgAvjGaQ=
github.com/DataDog/datadog-go/v5 v5.3.0 h1:2q2qjFOb3RwAZNU+ez27ZVDwErJv5/VpbBPprz7Z+s8=
github.com/DataDog/datadog-go/v5 v5.3.0/go.mod h1:XRDJk1pTc00gm+ZDiBKsjh7oOOtJfYfglVCmFb8C2+Q=
github.com/DataDog/go-libddwaf/v2 v2.2.3 h1:LpKE8AYhVrEhlmlw6FGD41udtDf7zW/aMdLNbCXpegQ=
github.com/DataDog/go-libddwaf/v2 v2.2.3/go.mod h1:8nX0SYJMB62+fbwYmx5J7zuCGEjiC/RxAo3+AuYJuFE=
github.com/DataDog/go-tuf v1.0.2-0.5.2 h1:EeZr937eKAWPxJ26IykAdWA4A0jQXJgkhUjqEI/w7+I=
github.com/DataDog/go-tuf v1.0.2-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0=
github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4=
github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
github.com/DataDog/sketches-go v1.4.2 h1:gppNudE9d19cQ98RYABOetxIhpTCl4m7CnbRZjvVA/o=
github.com/DataDog/sketches-go v1.4.2/go.mod h1:xJIXldczJyyjnbDop7ZZcLxJdV3+7Kra7H1KMgpgkLk=
github.com/Fisker-Inc/project-ai-can-go v1.3.1 h1:OjqeBun9kQwZA0VP61dANOtMqsdYoDjwBCnDOE4zZsE=
github.com/Fisker-Inc/project-ai-can-go v1.3.1/go.mod h1:8YrzRtqxRfiXEmvXpcQlUvmfCGLlpn+rJE02HiGUm/I=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ReneKroon/ttlcache/v2 v2.11.0 h1:OvlcYFYi941SBN3v9dsDcC2N8vRxyHcCmJb3Vl4QMoM=
github.com/ReneKroon/ttlcache/v2 v2.11.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY=
github.com/albenik/bcd v0.0.0-20170831201648-635201416bc7 h1:m3Ayfs5OcAlIMEdLIQKubBsVLGee4YMUr14+d1256WE=
github.com/albenik/bcd v0.0.0-20170831201648-635201416bc7/go.mod h1:QIAMbrwsnQZ2ES3G26RubSrDB5SPyzsp9Hts5NJdTrI=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 h1:icCHutJouWlQREayFwCc7lxDAhws08td+W3/gdqgZts=
github.com/confluentinc/confluent-kafka-go/v2 v2.3.0/go.mod h1:/VTy8iEpe6mD9pkCH5BhijlUl8ulUXymKv1Qig5Rgb8=
github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg=
github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v23.0.4+incompatible h1:Kd3Bh9V/rO+XpTP/BLqM+gx8z7+Yb0AA2Ibj+nNo4ek=
github.com/docker/docker v23.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/ebitengine/purego v0.5.2 h1:r2MQEtkGzZ4LRtFZVAg5bjYKnUbxxloaeuGxH0t7qfs=
github.com/ebitengine/purego v0.5.2/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk=
github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-pg/pg/v10 v10.11.1 h1:vYwbFpqoMpTDphnzIPshPPepdy3VpzD8qo29OFKp4vo=
github.com/go-pg/pg/v10 v10.11.1/go.mod h1:ExJWndhDNNftBdw1Ow83xqpSf4WMSJK8urmXD5VXS1I=
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo=
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs=
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/klauspost/compress v1.17.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g=
github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA=
github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo=
github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8=
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
github.com/opencontainers/runc v1.1.6 h1:XbhB8IfG/EsnhNvZtNdLB0GBw92GYEFvKlhaJk9jUgA=
github.com/opencontainers/runc v1.1.6/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0=
github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg=
github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI=
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc=
github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo=
github.com/swaggo/swag v1.8.8 h1:/GgJmrJ8/c0z4R4hoEPZ5UeEhVGdvsII4JbVDLbR7Xc=
github.com/swaggo/swag v1.8.8/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
github.com/testcontainers/testcontainers-go v0.14.0 h1:h0D5GaYG9mhOWr2qHdEKDXpkce/VlvaYOCzTRi6UBi8=
github.com/testcontainers/testcontainers-go v0.14.0/go.mod h1:hSRGJ1G8Q5Bw2gXgPulJOLlEBaYJHeBSOkQM5JLG+JQ=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
go4.org/intern v0.0.0-20230525184215-6c62f75575cb h1:ae7kzL5Cfdmcecbh22ll7lYP3iuUdnfnhiPcSaDgH/8=
go4.org/intern v0.0.0-20230525184215-6c62f75575cb/go.mod h1:Ycrt6raEcnF5FTsLiLKkhBTO6DPX3RCUCUVnks3gFJU=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/DataDog/dd-trace-go.v1 v1.60.1 h1:Sqkq62MxQW/RD+sgZsQuUdHWHyXI4JS5x0lxlxrv2Hk=
gopkg.in/DataDog/dd-trace-go.v1 v1.60.1/go.mod h1:6aArYrAHjnuaofJ3lKuSRQbhrBx1LcSpiEYCIScJE5Y=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/gotraceui v0.2.0 h1:dmNsfQ9Vl3GwbiVD7Z8d/osC6WtGGrasyrC2suc4ZIQ=
honnef.co/go/gotraceui v0.2.0/go.mod h1:qHo4/W75cA3bX0QQoSvDjbJa4R8mAyyFjbWAj63XElc=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a h1:1XCVEdxrvL6c0TGOhecLuB7U9zYNdxZEjvOqJreKZiM=
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a/go.mod h1:e83i32mAQOW1LAqEIweALsuK2Uw4mhQadA5r7b0Wobo=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=

View File

@@ -0,0 +1,17 @@
package handlers
import (
"net/http"
"fiskerinc.com/modules/utils/envtool"
)
// DocsHandler serves API docs for the gateway
func DocsHandler() http.Handler {
fp := envtool.GetEnv("DOCS", "")
if fp == "" {
return nil
}
fs := http.FileServer(http.Dir(fp))
return fs
}

View File

@@ -0,0 +1,37 @@
package handlers_test
import (
"net/http"
"os"
"testing"
"gateway/handlers"
"fiskerinc.com/modules/httpclient/tester"
"fiskerinc.com/modules/testhelper"
"fiskerinc.com/modules/testrunner"
)
func TestDocsHandler(t *testing.T) {
os.Setenv("DOCS", "/non-existent-folder")
handler := handlers.DocsHandler()
tests := []testrunner.TestCase{
{
Name: "Simple",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodGet, "http://example.com/docs", nil),
ExpectedStatus: http.StatusNotFound,
ExpectedResponse: `404 page not found
`,
},
},
}
for _, test := range tests {
if test.HttpTestCase != nil {
w := test.HttpTestCase.Test(handler.ServeHTTP)
test.HttpTestCase.ValidateHttp(t, test.Name, w)
}
}
}

View File

@@ -0,0 +1,111 @@
package handlers
import (
"context"
"net/http"
"gateway/services"
"gateway/websocket"
"fiskerinc.com/modules/logger"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
// SecureSessionWebsocketHandler initiates a websocket connection off an HTTP request
func SecureSessionWebsocketHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, err := websocket.NewSecureSession(w, r)
if err != nil {
logger.Warn().Err(err).Send()
logger.Warn().Msgf("wshandler: bad request %v", websocket.PrintRequest(r))
return
}
go runSessionLifeCycle(ctx, session)
}
// InsecureSessionWebsocketHandler initiates a websocket connection off an HTTP request from mobile
func InsecureSessionWebsocketHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, err := websocket.NewInsecureSession(w, r)
if err != nil {
logger.Warn().Err(err).Send()
logger.Warn().Msgf("wshandler: bad request %v", websocket.PrintRequest(r))
return
}
go runSessionLifeCycle(ctx, session)
}
// WebsocketSession handles the life cycle of a websocket
func runSessionLifeCycle(ctx context.Context, session websocket.SessionInterface) {
defer session.Close()
span, ctx := tracer.StartSpanFromContext(ctx, "websocket")
defer span.Finish()
err := session.Authenticate()
if err != nil {
logger.At(logger.Warn(), session.Key(), "conn").Str("ip", session.GetIP()).Err(err).Send()
return
}
addServices(session)
defer removeServices(session)
producer, err := services.GetKafkaProducer()
if err != nil {
logger.Error().Str("id", session.Key()).Err(err).Send()
return
}
logger.Debug().Msgf("websocket session: start listening%v", session.GetID())
err = session.Listen(ctx, producer)
if err != nil {
logger.At(logger.Warn(), session.Key(), "conn").Err(err).Send()
return
}
}
// addServices notifies all services upon websocket connection
func addServices(session websocket.SessionInterface) error {
id := session.Key()
services.GetConnections().Add(session)
logger.Debug().Msgf("websocket session: addServices lifecycle %v", id)
services.AddRemoveRedisListeners(true, id)
producer, err := services.GetKafkaProducer()
if err != nil {
logger.At(logger.Error(), session.Key(), "Kafka producer failed").Err(err).Send()
return err
}
if err = session.Load(producer); err != nil {
logger.At(logger.Warn(), session.Key(), "conn").Err(err).Send()
}
logger.At(logger.Debug(), "Session", id).Msgf("connection added %s", id)
return nil
}
// removeServices notifies all services upon websocket disconnection
func removeServices(session websocket.SessionInterface) error {
id := session.Key()
if err := services.GetConnections().Remove(session); err != nil {
// if error returned, the session did not exist or is different session
// that means we should not remove the current id from pub sub and queues
logger.At(logger.Error(), session.Key(), "conn").Err(err).Send()
return err
}
services.AddRemoveRedisListeners(false, id)
producer, err := services.GetKafkaProducer()
if err != nil {
logger.At(logger.Error(), session.Key(), "Kafka producer").Err(err).Send()
return err
}
if err = session.Teardown(producer); err != nil {
logger.At(logger.Warn(), session.Key(), "conn").Err(err).Send()
}
logger.At(logger.Debug(), "Session", id).Msgf("connection removed %s", id)
return nil
}

44
services/gateway/main.go Normal file
View File

@@ -0,0 +1,44 @@
package main
import (
"context"
"gateway/controllers"
"gateway/server"
"gateway/services"
"gateway/sloppy"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/tracer"
"fiskerinc.com/modules/utils/app"
)
func init() {
app.Setup("gateway", cleanup)
}
func main() {
defer cleanup()
tracer.Start()
defer tracer.Stop()
const port string = ":8077"
const kafkaTopic string = "ota_update"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Initiate vin blocking list early
sloppy.GetVINBlocker()
go controllers.HealthCheck()
go server.StartRedisQueue(ctx)
go server.StartRedisSubscriptions(ctx)
go services.GetConnections().RunExpiration(ctx)
go server.StartHTTP(ctx, port)
select {}
}
func cleanup() {
logger.Close()
}

View File

@@ -0,0 +1,26 @@
package server
import (
"context"
"net/http"
"gateway/handlers"
"fiskerinc.com/modules/httphandlers"
"fiskerinc.com/modules/logger"
httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http"
)
// StartHTTP runs server for websockets and docs
func StartHTTP(ctx context.Context, port string) {
mux := httptrace.NewServeMux()
mux.Handle("/docs/", http.StripPrefix("/docs", handlers.DocsHandler()))
mux.HandleFunc("/session", httphandlers.PanicHandler(handlers.SecureSessionWebsocketHandler))
mux.HandleFunc("/secret_mobile", httphandlers.PanicHandler(handlers.InsecureSessionWebsocketHandler))
logger.Info().Msgf("gateway websockets on http://0.0.0.0%s", port)
logger.Fatal().AnErr("http.ListenAndServe", http.ListenAndServe(port, mux)).Send()
}

View File

@@ -0,0 +1,60 @@
package server
import (
"context"
"errors"
"io"
"time"
"gateway/services"
"fiskerinc.com/modules/logger"
)
func logRedisErr(err error) {
if errors.Is(err, io.EOF) {
logger.Warn().Err(err).Send()
} else {
logger.Error().Err(err).Send()
}
}
// StartRedisQueue intiates Redis queue listener
func StartRedisQueue(ctx context.Context) {
logger.Info().Msg("initializing redis queues")
defer func() {
if err := recover(); err != nil {
logger.Error().Msgf("PanicRedis %v", err)
}
}()
for {
err := services.GetRedisQueues().Listen(ctx, services.GetConnections().SendMsgToClient)
for err != nil {
logRedisErr(err)
err = services.GetRedisQueues().Restart()
time.Sleep(time.Second * 10)
}
}
}
// StartRedisSubscriptions initiates Redis subscription listener
func StartRedisSubscriptions(ctx context.Context) {
logger.Info().Msg("initializing redis subscriptions")
defer func() {
if err := recover(); err != nil {
logger.Error().Msgf("PanicRedis %v", err)
}
}()
for {
err := services.GetRedisPubSub().Listen(ctx, services.GetConnections().SendMsgToClient)
for err != nil {
logRedisErr(err)
err = services.GetRedisPubSub().Restart()
time.Sleep(time.Second * 10)
}
}
}

View File

@@ -0,0 +1,64 @@
package services
import (
"sync"
"gateway/websocket"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/logger"
)
var connections *websocket.Connections
var connectionsOnce sync.Once
// GetConnections returns singleton instance of websocket Connections
func GetConnections() *websocket.Connections {
connectionsOnce.Do(func() {
connections = websocket.NewConnections(
// addSessionCallback,
// removeSessionCallback,
)
})
return connections
}
func addSessionCallback(session websocket.SessionInterface) error {
vin := session.GetID()
uid := session.GetUUID()
if session.IsDevice(common.HMI) {
return AddHMISessionToManyCache(vin, uid)
}
return nil
}
func removeSessionCallback(session websocket.SessionInterface) error {
vin := session.GetID()
uid := session.GetUUID()
if session.IsDevice(common.HMI) {
return RemoveHMISessionFromManyCache(vin, uid)
}
return nil
}
func AddRemoveRedisListeners(add bool, id string) {
if !add {
if err := GetRedisPubSub().Remove(id); err != nil {
logger.At(logger.Warn(), id, "Redis PubSub conn remove failed").Err(err).Send()
}
if err := GetRedisQueues().Remove(id); err != nil {
logger.At(logger.Warn(), id, "Redis Queue conn remove failed").Err(err).Send()
}
} else {
if err := GetRedisPubSub().Add(id); err != nil {
logger.At(logger.Error(), id, "Redis PubSub conn add failed").Err(err).Send()
}
if err := GetRedisQueues().Add(id); err != nil {
logger.At(logger.Error(), id, "Redis Queue conn add failed").Err(err).Send()
}
}
}

View File

@@ -0,0 +1,30 @@
package services
import (
"context"
"sync"
"fiskerinc.com/modules/kafka"
"fiskerinc.com/modules/logger"
)
var producer kafka.ProducerInterface
var producerOnce sync.Once
// GetKafkaProducer returns singleton instance of kafka producer
func GetKafkaProducer() (kafka.ProducerInterface, error) {
var err error
producerOnce.Do(func() {
ctx := context.Background()
producer, err = kafka.NewAsyncProducer(ctx)
if err != nil {
logger.Error().Err(err).Send()
}
go producer.ReadEvents()
})
if err != nil {
return nil, err
}
return producer, nil
}

View File

@@ -0,0 +1,83 @@
package services
import (
"sync"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
"github.com/pkg/errors"
)
var pubsub redis.Listener
var pubsubOnce sync.Once
// GetRedisPubSub returns singleton instance of pubsub listener
func GetRedisPubSub() redis.Listener {
pubsubOnce.Do(func() {
pubsub = redis.NewPubSub()
})
return pubsub
}
var queues redis.Listener
var queuesOnce sync.Once
// GetRedisQueues returns singleton instance of queues listener
func GetRedisQueues() redis.Listener {
queuesOnce.Do(func() {
queues = redis.NewQueues()
})
return queues
}
var clientPoolOnce sync.Once
var clientPool redis.ClientPoolInterface
func RedisClientPool() redis.ClientPoolInterface {
clientPoolOnce.Do(func() {
if clientPool != nil {
return
}
clientPool = redis.NewClientPool()
})
return clientPool
}
func AddHMISessionToManyCache(vin string, uid int64) error {
logger.Info().Msgf("Adding a unique connection to hmi:%s:many-sessions", vin)
batch := redis.NewRedisBatchCommands()
batch.Add("LPUSH", redis.HMIManySessionsKey(vin), uid)
batch.Add("LLEN", redis.HMIManySessionsKey(vin))
batchResponse, err := RedisClientPool().GetFromPool().ExecuteBatch(batch)
if err != nil {
errors.WithStack(err)
return err
}
results, ok := batchResponse.([]int)
if ok {
if len(results) != 2 {
return nil
}
total := results[1]
if total > 1 {
logger.Warn().Msgf("Currently storing %d connections in cache", total)
}
} else {
logger.Debug().Msgf("Could not parse redis response when setting %s:%d", vin, uid)
}
return nil
}
func RemoveHMISessionFromManyCache(vin string, uid int64) error {
logger.Info().Msgf("Removing a unique connection from hmi:%s:many-sessions", vin)
_, err := RedisClientPool().GetFromPool().Execute("LREM", redis.HMIManySessionsKey(vin), 1, uid)
if err != nil {
logger.At(logger.Error(), vin, "conn").Err(err).Send()
}
return nil
}

View File

@@ -0,0 +1,28 @@
package sloppy
import (
"sync"
"fiskerinc.com/modules/db"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/logger"
)
var (
carsDB queries.CarsInterface
carsDBOnce sync.Once
)
func GetCarsDB() queries.CarsInterface {
carsDBOnce.Do(func() {
if carsDB != nil {
return
}
client := &db.DBClient{}
logger.Debug().Msg("Init Cars instance")
cars := &queries.Cars{}
cars.SetClient(client)
carsDB = cars
})
return carsDB
}

View File

@@ -0,0 +1,83 @@
package sloppy
import (
"slices"
"sync"
"time"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/utils/whereami"
)
var vinblocker *VINBlocker
var vinblockerOnce sync.Once
// VIN Blocker fetches the list of allowed vins once per day, and stores them locally to check against
func GetVINBlocker() *VINBlocker {
vinblockerOnce.Do(func() {
vinblocker = newVINBlocker()
})
return vinblocker
}
type VINBlocker struct {
allowedList []string // The list of vins to allow
allowAll bool // Should we allow all vins to be used
sync.RWMutex // Control overwriting the allowed list
}
func newVINBlocker() (vb *VINBlocker) {
vb = &VINBlocker{}
vb.allowAll = true // This will for half a second allow all cars to access cloud
// create a new thread to get the list of vins everyday
//On dev, always allow all connections
if whereami.Environment != whereami.DEVELOPMENT {
vb.updateVINList() // Run in thread, so no sneaks can happen
go vb.autoUpdate()
}
return vb
}
func (vb *VINBlocker) IsVINAllowed(vin string) (allow bool) {
// We are allowing all vins to connect
if vb.allowAll {
return true
}
vb.RLock()
defer vb.RUnlock()
// Returns the index of where the string might be, annoying
_, allow = slices.BinarySearch(vb.allowedList, vin)
return allow
}
func (vb *VINBlocker) autoUpdate() {
vb.updateVINList()
// Going to be pretty actively changed, so should change this to more detect when vin list is diff
nextRun := time.Now().Add(1 * time.Hour)
time.AfterFunc(time.Until(nextRun), vb.autoUpdate)
}
func (vb *VINBlocker) updateVINList() {
tempList := fetchVINList()
vb.Lock()
defer vb.Unlock()
if len(tempList) == 0 {
vb.allowAll = true
} else {
vb.allowAll = false
}
vb.allowedList = tempList
}
func fetchVINList() (vinList []string) {
carsDB := GetCarsDB()
var err error
vinList, err = carsDB.GetWhiteListCars()
if err != nil {
logger.Err(err).Msg("Failed to vinBlocker list")
}
// making sure the results are sorted
slices.Sort(vinList)
return
}

View File

@@ -0,0 +1,75 @@
package websocket
import (
"fmt"
"net/http"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/jwt"
"fiskerinc.com/modules/utils/envtool"
"github.com/pkg/errors"
)
var authURL string = envtool.GetEnv("VERIFY_URL", "https://dev-auth.fiskerdps.com/auth/verify/")
// AuthEvent is the authentication message sent over websocket
type AuthEvent struct {
Topic string `json:"topic"`
Key string `json:"key"`
Payload AuthPayload `json:"payload"`
}
// AuthPayload describes the payload received
type AuthPayload struct {
Handler string `json:"handler"`
Data AuthData `json:"data"`
}
// AuthData describes the data received
type AuthData struct {
Token string `json:"token"`
}
// AuthResponse provides format for auth response
type AuthResponse struct {
Handler string `json:"handler"`
Data AuthResponseData `json:"data"`
}
// AuthResponseData provides data for auth response
type AuthResponseData struct {
Authenticated bool `json:"authenticated"`
}
// AuthenticateRequest checks for valid authentication message
func AuthenticateRequest(ae AuthEvent) (bool, error) {
if ae.Topic != "auth_service" || len(ae.Key) == 0 {
return false, errors.New("incorrect format")
}
switch ae.Payload.Handler {
case "verify":
return verifyToken(ae.Payload.Data)
}
return false, errors.New("invalid request")
}
func verifyToken(ad AuthData) (bool, error) {
tokenString := []string{fmt.Sprintf("bearer %s", ad.Token)}
resp, err := httpclient.Get(authURL, http.Header{"authorization": tokenString})
if err != nil {
return false, errors.WithStack(err)
}
return resp.StatusCode == 200, nil
}
func parseIDFromToken(token string) (string, error) {
payload, err := jwt.GetPayload(token)
if err != nil {
return fmt.Sprintf("%+v", payload), err
}
return fmt.Sprintf("%+v", payload), nil
}

View File

@@ -0,0 +1,105 @@
package websocket
import (
"net/http"
"testing"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/httpclient/mock"
"fiskerinc.com/modules/testhelper"
)
func TestVerifyTokenAuthorized(t *testing.T) {
c := mock.Client{
DoFunc: func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200}, nil
},
}
httpclient.Client = &c
ad := AuthData{
Token: "validtoken",
}
ok, err := verifyToken(ad)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVerifyTokenAuthorized", nil, err)
}
if !ok {
t.Errorf(testhelper.TestErrorTemplate, "TestVerifyTokenAuthorized", true, ok)
}
}
func TestVerifyTokenUnauthorized(t *testing.T) {
c := mock.Client{
DoFunc: func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 401}, nil
},
}
httpclient.Client = &c
ad := AuthData{
Token: "invalidtoken",
}
ok, err := verifyToken(ad)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVerifyTokenUnauthorized", nil, err)
}
if ok {
t.Errorf(testhelper.TestErrorTemplate, "TestVerifyTokenUnauthorized", false, ok)
}
}
func TestAuthenticateRequest(t *testing.T) {
c := mock.Client{
DoFunc: func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200}, nil
},
}
httpclient.Client = &c
ae := AuthEvent{
Topic: "auth_service",
Key: "FISKER123",
Payload: AuthPayload{
Handler: "verify",
Data: AuthData{
Token: "validtoken",
},
},
}
ok, err := AuthenticateRequest(ae)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestAuthenticateRequest", nil, err)
}
if !ok {
t.Errorf(testhelper.TestErrorTemplate, "TestAuthenticateRequest", true, ok)
}
}
func TestAuthenticateRequestInvalid(t *testing.T) {
c := mock.Client{
DoFunc: func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 401}, nil
},
}
httpclient.Client = &c
ae := AuthEvent{
Topic: "invalid_topic",
Key: "FISKER123",
Payload: AuthPayload{
Handler: "verify",
Data: AuthData{
Token: "validtoken",
},
},
}
_, err := AuthenticateRequest(ae)
if err == nil {
t.Errorf(testhelper.TestErrorTemplate, "TestAuthenticateRequestInvalid", "error", nil)
}
}

View File

@@ -0,0 +1,166 @@
package websocket
import (
"context"
"encoding/json"
"sync"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/scheduler"
"github.com/pkg/errors"
"github.com/robfig/cron"
)
func NewConnections() *Connections {
return &Connections{
sessions: make(map[string]SessionInterface),
expiration: scheduler.Bucket[SessionInterface]{},
}
}
type Connections struct {
sessions map[string]SessionInterface
expiration scheduler.Bucket[SessionInterface]
mu sync.RWMutex
}
func (c *Connections) getSession(id string) (SessionInterface, bool) {
c.mu.RLock()
session, ok := c.sessions[id]
c.mu.RUnlock()
return session, ok
}
func (c *Connections) addSession(key string, session SessionInterface) {
c.mu.Lock()
c.sessions[key] = session
logger.At(logger.Info(), key, "conn").
Str("ip", session.GetIP()).
Int("connections", c.length()).
Msgf("added connection %s", key)
c.mu.Unlock()
}
func (c *Connections) deleteSession(session SessionInterface) {
c.mu.Lock()
key := session.Key()
delete(c.sessions, key)
logger.At(logger.Info(), key, "conn").
Str("ip", session.GetIP()).
Int("connections", c.length()).
Msgf("removed connection %s", key)
c.mu.Unlock()
}
// Add connection to map
func (c *Connections) Add(session SessionInterface) error {
key := session.Key()
expiredSession, exists := c.getSession(key)
if exists {
expiredSession.SendMsgToClient(DuplicateConnectionMessage())
// if connection already exists, skip teardown when closing connection
// otherwise the car status will be changed to offline
expiredSession.SkipTeardown(true)
c.Remove(expiredSession)
}
c.addSession(key, session)
if exists && expiredSession != nil {
if expiredSession.GetType() == common.HMI.String() { // workaround for HMI sessions
// if connection already exists, skip teardown when closing connection
// otherwise the car status will be changed to offline
logger.At(logger.Info(), "Connections::checkIfExists schedule", key).Send()
c.Schedule(expiredSession)
} else {
expiredSession.Close()
logger.At(logger.Info(), key, "conn").Msgf("existing connection %s is closed", key)
}
logger.At(logger.Info(), key, "conn").Msgf("removing duplicate connection %s", key)
}
return nil
}
// Remove connection from map
//
// if connection is not equal to the connection in map,
// does not remove
func (c *Connections) Remove(session SessionInterface) error {
id := session.Key()
expiredSesssion, ok := c.getSession(id)
if !ok {
return missingWebsocketError(id)
}
if expiredSesssion != session {
return wrongSessionError(id)
}
c.deleteSession(session)
return nil
}
// Send to websocket connection
func (c *Connections) SendMsgToClient(id string, message []byte) error {
session, ok := c.getSession(id)
if !ok {
return missingWebsocketError(id)
}
return session.SendMsgToClient(message)
}
func (c *Connections) length() int {
return len(c.sessions)
}
func missingWebsocketError(id string) error {
return errors.Errorf("no websocket connection found for ID: %v", id)
}
func wrongSessionError(id string) error {
return errors.Errorf("%v does not match with existing connection", id)
}
func DuplicateConnectionMessage() []byte {
m := common.Message{
Handler: "error",
Data: common.MessageString{
Message: "disconnected by duplicate ID",
},
}
p, _ := json.Marshal(m)
return p
}
func (c *Connections) Schedule(session SessionInterface) error {
logger.Info().Msgf("Scheduling session to expire in 6 min %s. type %s", session.GetID(), session.GetType())
c.expiration.Schedule(session)
return nil
}
func (c *Connections) RunExpiration(ctx context.Context) {
cr := cron.New()
cr.AddFunc("@every 30s", func() {
c.expiration.Process(func(session SessionInterface) {
logger.Debug().Msgf("RunExpiration::closing session %s ", session.Key())
session.Close()
})
})
cr.Start()
<-ctx.Done()
cr.Stop()
}

View File

@@ -0,0 +1,73 @@
package websocket
import (
"fmt"
"net"
"sync"
"testing"
"time"
kafka "fiskerinc.com/modules/kafka/mock"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
)
func AddRemoveRedisListeners(add bool, id string, pubsub *redis.PubSub, queueRef *redis.Queues) {
if !add {
if err := pubsub.Remove(id); err != nil {
logger.At(logger.Warn(), id, "Redis PubSub conn remove failed").Err(err).Send()
}
if err := queueRef.Remove(id); err != nil {
logger.At(logger.Warn(), id, "Redis Queue conn remove failed").Err(err).Send()
}
} else {
if err := pubsub.Add(id); err != nil {
logger.At(logger.Error(), id, "Redis PubSub conn add failed").Err(err).Send()
}
if err := queueRef.Add(id); err != nil {
logger.At(logger.Error(), id, "Redis Queue conn add failed").Err(err).Send()
}
}
}
func TestSecureSessionTRexConnections(t *testing.T) {
redis.MockRedisConnection()
kafka.GetKafkaMock(nil)
pubSub := redis.NewPubSub(redis.GetMockPool().Get())
queueRef := redis.NewQueues(redis.GetMockPool())
var wg sync.WaitGroup
connections := NewConnections()
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(i int) {
ws, _ := net.Pipe()
id := fmt.Sprintf("fisker123")
if i%2 == 0 {
id = fmt.Sprintf("%s%d", id, i)
}
s := &SessionTRex{
Session: &Session{
Websocket: ws,
ID: id,
},
DBC: id,
ICCID: "!23454523453",
}
connections.Add(s)
AddRemoveRedisListeners(true, id, pubSub, queueRef)
time.Sleep(200 * time.Millisecond)
connections.Remove(s)
s.Close()
ws.Close()
wg.Done()
}(i)
}
wg.Wait()
t.Log("success")
}

View File

@@ -0,0 +1,23 @@
package websocket
import (
"fmt"
"strings"
"github.com/gobwas/ws"
"github.com/pkg/errors"
)
func isNormalClosure(err error) bool {
return strings.Contains(err.Error(), fmt.Sprintf("%d", ws.StatusNormalClosure))
}
var ErrFailedAuthentication = errors.New("failed authentication")
var ErrFailedToLoad = errors.New("failed loading")
var ErrInvalidHeaders = errors.New("request missing header Ssl-Client-Subject-Dn")
var ErrInvalidToken = errors.New("token missing username field")
func ErrInvalidHandler(handler string) error {
return errors.Errorf("%s is an invalid message handler", handler)
}

View File

@@ -0,0 +1,50 @@
package websocket
import (
"net/http"
"strings"
)
// ParseDBCFromRequest retrieves DBC version from the "fisker-dbc" field
//
// located in header of request
func ParseDBCFromRequest(r *http.Request) string {
return r.Header.Get("Fisker-Dbc-Sha256")
}
func ParseICCIDFromRequest(r *http.Request) (string, error) {
iccid := strings.TrimSpace(r.Header.Get("X-ICCID"))
// ok, err := validator.ValidateICCIDSimple(iccid)
// if err != nil {
// return iccid, err
// } else if !ok {
// return iccid, errors.Errorf("%s failed to pass ICCID validation", iccid)
// }
return iccid, nil
}
// ParseDeviceAndVersionFromRequest parses device type and version
//
// of client from User-Agent field in header
func ParseDeviceAndVersionFromRequest(r *http.Request) (string, string) {
var device string
var version string
userAgent := r.Header.Get("User-Agent")
specs := strings.Split(userAgent, " ")
device = strings.ToLower(specs[0])
switch len(specs) {
case 5:
version = specs[3]
case 4:
version = specs[2]
case 2:
version = specs[1]
}
return device, version
}

View File

@@ -0,0 +1,42 @@
package websocket_test
import (
"net/http"
"testing"
"gateway/websocket"
"fiskerinc.com/modules/testhelper"
)
func TestParseDeviceAndVersionFromRequest(t *testing.T) {
req := &http.Request{Header: http.Header{}}
req.Header.Add("User-Agent", "Fisker T.Rex 1.2.3 [abc123]")
device, version := websocket.ParseDeviceAndVersionFromRequest(req)
if device != "fisker" {
t.Errorf(testhelper.TestErrorTemplate, "TestParseDeviceAndVersionFromRequest", "fisker", device)
}
if version != "1.2.3" {
t.Errorf(testhelper.TestErrorTemplate, "TestParseDeviceAndVersionFromRequest", "1.2.3", version)
}
req = &http.Request{Header: http.Header{}}
req.Header.Add("User-Agent", "HMI 1.2.3.4")
_, version = websocket.ParseDeviceAndVersionFromRequest(req)
if version != "1.2.3.4" {
t.Errorf(testhelper.TestErrorTemplate, "TestParseDeviceAndVersionFromRequest", "1.2.3.4", version)
}
req = &http.Request{Header: http.Header{}}
req.Header.Add("User-Agent", "Fisker T.Rex Ocean 1.2.3 [abc123]")
device, version = websocket.ParseDeviceAndVersionFromRequest(req)
if device != "fisker" {
t.Errorf(testhelper.TestErrorTemplate, "TestParseDeviceAndVersionFromRequest", "fisker", device)
}
if version != "1.2.3" {
t.Errorf(testhelper.TestErrorTemplate, "TestParseDeviceAndVersionFromRequest", "1.2.3", version)
}
}

View File

@@ -0,0 +1,17 @@
package websocket
import (
"net"
"time"
)
type MockConn struct{}
func (c *MockConn) Read(b []byte) (n int, err error) { return 0, nil }
func (c *MockConn) Write(b []byte) (n int, err error) { return 0, nil }
func (c *MockConn) Close() error { return nil }
func (c *MockConn) LocalAddr() net.Addr { return &net.IPAddr{} }
func (c *MockConn) RemoteAddr() net.Addr { return &net.IPAddr{} }
func (c *MockConn) SetDeadline(t time.Time) error { return nil }
func (c *MockConn) SetReadDeadline(t time.Time) error { return nil }
func (c *MockConn) SetWriteDeadline(t time.Time) error { return nil }

View File

@@ -0,0 +1,413 @@
package websocket
import (
"compress/flate"
"context"
"encoding/json"
"fmt"
"gateway/sloppy"
"io"
"net"
"net/http"
"strings"
"time"
"fiskerinc.com/modules/grpc/kafka_grpc"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/kafka"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/utils"
"fiskerinc.com/modules/utils/envtool"
"fiskerinc.com/modules/validator"
"google.golang.org/protobuf/proto"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsflate"
"github.com/gobwas/ws/wsutil"
"github.com/pkg/errors"
)
var deadline = time.Duration(envtool.GetEnvInt("WS_TIMEOUT", 30)) * time.Second
// SessionInterface provides methods for connection
type SessionInterface interface {
Authenticate() error
Key() string
SendMsgToClient(message []byte) error
Receive() ([]byte, ws.OpCode, error)
Listen(context.Context, kafka.ProducerInterface) error
Load(kafka.ProducerInterface) error
Teardown(kafka.ProducerInterface) error
Close() error
GetWebsocket() net.Conn
GetIP() string
GetType() string
IsDevice(device common.Device) bool
GetID() string
GetUUID() int64
GetVIN() (string)
SkipTeardown(skip bool)
}
// NewSecureSession creates session w/ websocket based off user-agent
// given in HTTP request
//
// ex: "Fisker Ocean T.Rex 1.2.3.4 abc123" - T.Rex
// ex: "HMI 2.0.0.0" - HMI
func NewSecureSession(w http.ResponseWriter, r *http.Request) (SessionInterface, error) {
var s SessionInterface
// HERE, get the vin and block the request
vin, err := utils.ParseVINFromRequest(r)
if err != nil {
logger.At(logger.Error(), "no vin from request", "conn").Send()
return s, err
}
ok := validator.ValidateVINSimple(vin)
if !ok {
logger.Error().Str("type", "conn").Str("VIN", vin).Msg("NewSecureSession failed to validate vin")
return s, errors.Errorf("%s failed to validate VIN", vin)
}
vin = strings.ToUpper(vin)
if !sloppy.GetVINBlocker().IsVINAllowed(vin){
return s, errors.Errorf("%s is not an allowed VIN, please contact support", vin)
}
device, version := ParseDeviceAndVersionFromRequest(r)
switch device {
case "fisker":
logger.At(logger.Info(), "1:"+vin, "conn")
iccid, err := ParseICCIDFromRequest(r)
if err != nil {
logger.At(logger.Warn(), "1:"+vin, "conn").
Err(errors.WithMessagef(err, "failed to parse ICCID from request %s", vin)).Send()
}
s, err = NewTRexSession(w, r, vin, version, iccid)
if err != nil {
logger.At(logger.Warn(), "1:"+vin, "conn").
Err(errors.WithMessagef(err, "failed to create Trex session %s", vin)).Send()
return s, err
}
logger.At(logger.Info(), "1:"+vin, "conn").Send()
case "hmi":
s, err = NewHMISession(w, r, vin, version)
if err != nil {
logger.At(logger.Warn(), "2:"+vin, "conn").
Err(errors.WithMessagef(err, "failed to create HMI session %s", vin)).Send()
return s, err
}
logger.At(logger.Info(), "2:"+vin, "conn").Send()
default:
return s, ErrFailedToLoad
}
return s, nil
}
// NewInsecureSession creates session w/ websocket based off user-agent
// given in HTTP request
//
// ex: "Mobile 1.2.3.4" - Mobile
func NewInsecureSession(w http.ResponseWriter, r *http.Request) (SessionInterface, error) {
var s SessionInterface
var err error
device, version := ParseDeviceAndVersionFromRequest(r)
switch device {
case "mobile", "android", "ios":
s, err = NewMobileSession(w, r, version)
if err != nil {
return s, err
}
logger.At(logger.Info(), "3: "+s.GetID(), "conn").Send()
default:
return s, ErrFailedToLoad
}
return s, nil
}
// NewSession is used when device is unknown
func NewSession(w http.ResponseWriter, r *http.Request) (SessionInterface, error) {
var s SessionInterface
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
return s, errors.WithStack(err)
}
return &Session{
Websocket: conn,
Type: common.Unknown,
epoch: time.Now().UnixNano(),
}, nil
}
// Session contains websocket info
type Session struct {
Websocket net.Conn
Type common.Device
ID string // used for key generation to kafka
Version string
epoch int64
skipteardown bool
}
// Authenticate returns id if proper authentication, else returns error
func (s *Session) Authenticate() error {
msg, _, err := s.Receive()
if err != nil {
return err
}
var ae AuthEvent
err = json.Unmarshal(msg, &ae)
if err != nil {
return errors.WithStack(err)
}
authenticated, err := AuthenticateRequest(ae)
if err != nil {
return err
} else if !authenticated {
return errors.New("failed authentication")
}
s.ID = ae.Key
return nil
}
// Key generates key based on type of session and ID
func (s *Session) Key() string {
if s.Type == common.Unknown {
return s.ID
}
return s.Type.Key(s.ID)
}
// SendMsgToClient: Send a message to client
func (s *Session) SendMsgToClient(message []byte) error {
vin := s.GetVIN()
logger.Debug().Str("type", s.GetType()).Str("VIN", vin).Int64("SessionID", s.GetUUID()).Str("value", string(message)).Msg("SendMsgToClient")
err := wsutil.WriteServerMessage(s.Websocket, ws.OpText, message)
if err != nil {
err = errors.WithStack(err)
}
return err
}
func (s *Session) extendDeadline() error {
return s.Websocket.SetDeadline(time.Now().Add(deadline))
}
func (s *Session) receive(postFrame func() error) ([]byte, ws.OpCode, error) {
var (
err error
h ws.Header
msg wsflate.MessageState
)
// Using nil as a source io.Reader since we will Reset() it in the loop
// below.
fr := wsflate.NewReader(nil, func(r io.Reader) wsflate.Decompressor {
return flate.NewReader(r)
})
controlHandler := wsutil.ControlFrameHandler(s.Websocket, ws.StateServerSide)
rd := wsutil.Reader{
Source: s.Websocket,
State: ws.StateServerSide | ws.StateExtended,
OnIntermediate: controlHandler,
Extensions: []wsutil.RecvExtension{&msg},
}
for {
h, err = rd.NextFrame()
if err != nil {
return nil, h.OpCode, err
}
if postFrame != nil {
err = postFrame()
if err != nil {
return nil, 0, err
}
}
if h.OpCode.IsControl() {
if err := controlHandler(h, &rd); err != nil {
return nil, h.OpCode, err
}
continue
}
var src io.Reader = &rd
if msg.IsCompressed() {
fr.Reset(&rd)
src = fr
}
data, err := io.ReadAll(src)
if err != nil {
return nil, h.OpCode, err
}
return data, h.OpCode, err
}
}
func (s *Session) Receive() ([]byte, ws.OpCode, error) {
return s.receive(nil)
}
// Listen to websocket session and use handler upon message received
func (s *Session) Listen(ctx context.Context, producer kafka.ProducerInterface) error {
span, _ := tracer.StartSpanFromContext(ctx, "listen")
defer span.Finish()
key := s.Key()
for {
msg, op, err := s.Receive()
if op == ws.OpClose {
logger.At(logger.Info(), "Socket:Listen::EOF closing session ", key).Msg("OpClose")
return nil
} else if err != nil {
logger.At(logger.Error(), "Socket:Listen::err during receiving session ", key).Err(err).Send()
return err
}
err = s.Route(producer, msg)
if err != nil {
logger.At(logger.Warn(), "Socket:Listen:: failed route session ", key).Err(err).Send()
}
}
}
// Route messages
// - this allows other structs to override the behavior of messages received
func (s *Session) Route(producer kafka.ProducerInterface, data []byte) error {
var e common.EventRawJSON
err := e.Unmarshal(data)
if err != nil {
return errors.WithStack(err)
}
key := s.Key()
return producer.Produce(e.Topic, key, e.Payload, nil)
}
// Load the session - distributes messages to system notifying of new connection
func (s *Session) Load(producer kafka.ProducerInterface) error {
key := s.Key()
logger.At(logger.Info(), "Session::Load connection start notification", key).
Msgf("session.Load %s", key)
payload := kafka_grpc.GRPC_DepotPayload{
Handler: "init",
}
binaryPayload, _ := proto.Marshal(&payload)
err := producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, binaryPayload, nil)
return err
}
// Teardown the session - distributes messages to system notifying of removed connection
func (s *Session) Teardown(producer kafka.ProducerInterface) error {
// Go to send del message to depot service if connection was a duplicate
if s.skipteardown {
return nil
}
key := s.Key()
logger.At(logger.Debug(), "Session::Teardown: Notify services ", key).
Msgf("session.Teardown %s", key)
payload := kafka_grpc.GRPC_DepotPayload{
Handler: "del",
}
binaryPayload, _ := proto.Marshal(&payload)
err := producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, binaryPayload, nil)
return err
}
// Close the session
func (s *Session) Close() error {
key := s.Key()
logger.At(logger.Debug(), "Session:Close connection for ", key)
return s.Websocket.Close()
}
// GetWebsocket returns session's websocket
func (s *Session) GetWebsocket() net.Conn {
return s.Websocket
}
// GetIP returns session's websocket's IP
func (s *Session) GetIP() string {
return s.Websocket.RemoteAddr().String()
}
// GetType returns Device type in string form
func (s *Session) GetType() string {
return s.Type.String()
}
func (s *Session) IsDevice(device common.Device) bool {
return s.Type == device
}
// GetID returns ID of session (not to be mistaken with key)
func (s *Session) GetID() string {
return s.ID
}
// GetUUID returns a unique identifier for the session
func (s *Session) GetUUID() int64 {
return s.epoch
}
func (s *Session) GetVIN() (vin string) {
// For somereason code was changed to do some kind of parsing from session, but VIN is added directly
return s.ID
}
func (s *Session) SkipTeardown(skip bool) {
s.skipteardown = skip
}
func PrintRequest(r *http.Request) string {
// Create return string
var request []string
// Add the request string
url := fmt.Sprintf("%v %v %v", r.Method, r.URL, r.Proto)
request = append(request, url)
// Add the host
request = append(request, fmt.Sprintf("Host: %v", r.Host))
// Loop through headers
for name, headers := range r.Header {
name = strings.ToLower(name)
for _, h := range headers {
request = append(request, fmt.Sprintf("%v: %v", name, h))
}
}
// If this is a POST, add post data
if r.Method == "POST" {
r.ParseForm()
request = append(request, "\n")
request = append(request, r.Form.Encode())
}
// Return the request as a string
return strings.Join(request, "\n")
}

View File

@@ -0,0 +1,242 @@
package websocket
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"fiskerinc.com/modules/grpc/kafka_grpc"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/kafka"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/security"
"github.com/gobwas/ws"
"github.com/pkg/errors"
"google.golang.org/protobuf/proto"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
// NewHMISession serves as the constructor for HMI sessions
func NewHMISession(w http.ResponseWriter, r *http.Request, vin string, version string) (SessionInterface, error) {
var s SessionInterface
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
return s, errors.WithStack(err)
}
return &SessionHMI{
Session: &Session{
Websocket: conn,
Type: common.HMI,
Version: version,
ID: vin,
epoch: time.Now().UnixNano(),
},
InAuthentication: false,
}, nil
}
// SessionHMI contains websocket info
type SessionHMI struct {
*Session
SessionID string
InAuthentication bool
}
// Authenticate returns id if proper authentication, else returns error
//
// validates VIN inputted through "key" field of message
func (s *SessionHMI) Authenticate() error {
s.InAuthentication = true
defer func() { s.InAuthentication = false }()
err := s.authenticate()
data, _ := json.Marshal(AuthResponse{
Handler: "verify",
Data: AuthResponseData{
Authenticated: err == nil,
},
})
s.SendMsgToClient(data)
if err != nil {
errors.Errorf("Unable to send to session %s", s.SessionID)
return errors.WithStack(err)
}
return nil
}
func (s *SessionHMI) SendMsgToClient(message []byte) error {
if !s.InAuthentication && len(s.SessionID) == 0 {
return fmt.Errorf("session is not authorized, %s", s.SessionID)
}
return s.Session.SendMsgToClient(message)
}
func (s *SessionHMI) authenticate() error {
s.InAuthentication = true
defer func() { s.InAuthentication = false }()
msg, _, err := s.Receive()
if err != nil {
errors.Errorf("unable to read socket %s", s.ID)
return err
}
var m common.HMISessionMessage
err = json.Unmarshal(msg, &m)
if err != nil {
return errors.WithStack(err)
} else if m.Handler != "verify" {
return errors.Errorf("incorrect auth handler specified %v", m.Handler)
}
switch {
case m.Data.SessionID != "":
salter, err := security.NewSalter(s.ID)
if err != nil {
errors.Errorf("unable to generate salt %s", s.ID)
return err
}
err = salter.ValidateSessionID(m.Data.SessionID)
if err != nil {
errors.Errorf("unable to validate session %s", m.Data.SessionID)
return err
}
s.SessionID = m.Data.SessionID
case m.Data.Salt != "":
s.SessionID = m.Data.Salt
default:
return ErrFailedAuthentication
}
return nil
}
// Listen to websocket session and use handler upon message received
func (s *SessionHMI) Listen(ctx context.Context, producer kafka.ProducerInterface) error {
span, _ := tracer.StartSpanFromContext(ctx, "listen")
defer span.Finish()
key := s.Key()
for {
msg, op, err := s.Receive()
if op == ws.OpClose {
// logger.At(logger.Info(), key, "conn").Msg("OpClose") //commented out because reported by the call in websocket.go
return nil
} else if err != nil {
// logger.At(logger.Info(), key, "conn").Err(err).Send() //commented out because reported by the call in websocket.go
return err
}
err = s.Route(producer, msg)
if err != nil {
logger.At(logger.Warn(), key, "route").Err(err).Send()
}
}
}
// Route HMI messages
func (s *SessionHMI) Route(producer kafka.ProducerInterface, data []byte) error {
var m common.MessageRawJSON
err := m.Unmarshal(data)
if err != nil {
return err
}
if m.Verify == "ack"{
// Throw out ACK messages
return nil
}
key := s.Key()
logger.Debug().
Str("id", key).
Str("handler", m.Handler).
Int("size", len(data)).
Msgf("received from %v %s", key, string(data))
topic, ok := kafka.HMIHandlerTopics[m.Handler]
if !ok {
err = ErrInvalidHandler(m.Handler)
logger.Err(err).Str("Byte Data", string(data)).Str("Parsed Data", string(m.Data)).Msg("No Handler Insights")
return err
}
switch topic {
case kafka.AttendantServiceGRPCKafka:
valetData := common.AttendantRouteHMIGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.AttendantServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.DepotServiceGRPCKafka:
valetData := common.DepotRouteHMIToGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.ValetServiceGRPCKafka:
valetData := common.ValetRouteHMIPayloadGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.ValetServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
default:
err = producer.Produce(topic, key, m, nil)
}
if err != nil {
return err
}
return nil
}
func (s *SessionHMI) Load(producer kafka.ProducerInterface) error {
key := s.Key()
hmiSession := &kafka_grpc.GRPC_DepotPayload_HmiSession{
HmiSession: &kafka_grpc.HMISessionData{
SessionId: s.SessionID,
},
}
payload := kafka_grpc.GRPC_DepotPayload{
Handler: "init",
Data: hmiSession,
}
binaryPayload, _ := proto.Marshal(&payload)
err := producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, binaryPayload, nil)
return err
}

View File

@@ -0,0 +1,196 @@
package websocket
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"fiskerinc.com/modules/common"
kafka "fiskerinc.com/modules/kafka/mock"
"fiskerinc.com/modules/testhelper"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
func TestSessionHMI(t *testing.T) {
ws, _ := net.Pipe()
s := SessionHMI{
Session: &Session{
Websocket: ws,
},
}
if fmt.Sprintf("%T", s) != "websocket.SessionHMI" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMI", "websocket.SessionHMI", fmt.Sprintf("%T", s))
}
}
func TestNewHMISession(t *testing.T) {
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewTRexSession(w, r, "1F15K3R45N1234567", "2.0.0", "123456789123456789123456789")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionTRex", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.SessionTRex" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionTRex", "*websocket.SessionTRex", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, "")
defer conn.Close()
}
func TestSessionHMIAuthenticate(t *testing.T) {
userAgent := "HMI 1.2.3.4"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIAuthenticate", nil, err)
}
defer s.Close()
err = s.Authenticate()
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIAuthenticate", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
am := common.HMISessionMessage{
Handler: "verify",
Data: common.HMISessionData{
Salt: "XXXXXX",
},
}
msg, err := json.Marshal(am)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIAuthenticate", nil, err)
}
wsutil.WriteClientMessage(conn, ws.OpText, msg)
}
func TestSessionHMIListen(t *testing.T) {
userAgent := "HMI 1.2.3.4"
payload := "hello fisker!"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIListen", nil, err)
}
defer s.Close()
ctx := context.Background()
err = s.Listen(ctx, kafka.GetKafkaMock(nil))
if err.Error() != "EOF" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIListen", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
err := wsutil.WriteClientMessage(conn, ws.OpText, []byte(payload))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIListen", nil, err)
}
}
func TestSessionHMIRoute(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionHMI{
Session: &Session{
Websocket: ws,
},
}
msg := common.Message{
Handler: "update_approve",
Data: "hello fisker!",
}
data, err := json.Marshal(msg)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIRoute", nil, err)
}
err = s.Route(kafka.GetKafkaMock(nil), data)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIRoute", nil, err)
}
msg = common.Message{
Handler: "invalid_handler",
Data: "false",
}
data, err = json.Marshal(msg)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIRoute", nil, err)
}
err = s.Route(kafka.GetKafkaMock(nil), data)
if err == nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIRoute", "error", err)
}
}
func TestSessionHMILoadSession(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionHMI{
Session: &Session{
Websocket: ws,
Type: common.TRex,
ID: "FISKER123",
},
SessionID: "abc123",
}
err := s.Load(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMILoadSession", nil, err)
}
}
func TestSessionHMITeardownSession(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionHMI{
Session: &Session{
Websocket: ws,
Type: common.TRex,
ID: "FISKER123",
},
SessionID: "abc123",
}
err := s.Load(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMILoadSession", nil, err)
}
err = s.Teardown(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMITeardownSession", nil, err)
}
}

View File

@@ -0,0 +1,194 @@
package websocket
import (
"context"
"encoding/json"
"net/http"
"time"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/jwt"
"fiskerinc.com/modules/kafka"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/validator"
"google.golang.org/protobuf/proto"
"github.com/gobwas/ws"
"github.com/pkg/errors"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
// NewMobileSession serves as the constructor for HMI sessions
func NewMobileSession(w http.ResponseWriter, r *http.Request, version string) (SessionInterface, error) {
var s SessionInterface
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
return s, errors.WithStack(err)
}
return &SessionMobile{
Session: &Session{
Websocket: conn,
Type: common.Mobile,
Version: version,
epoch: time.Now().UnixNano(),
},
}, nil
}
// SessionMobile contains websocket info
type SessionMobile struct {
*Session
}
// Authenticate returns id if proper authentication, else returns error
func (s *SessionMobile) Authenticate() error {
err := s.authenticate()
data, _ := json.Marshal(AuthResponse{
Handler: "verify",
Data: AuthResponseData{
Authenticated: err == nil,
},
})
s.SendMsgToClient(data)
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *SessionMobile) authenticate() error {
msg, _, err := s.Receive()
if err != nil {
return err
}
var m common.MobileSessionMessage
err = json.Unmarshal(msg, &m)
if err != nil {
return errors.WithStack(err)
} else if m.Handler != "verify" {
return errors.Errorf("incorrect auth handler specified %v", m.Handler)
}
err = validator.ValidateStruct(m)
if err != nil {
return errors.WithStack(err)
}
valid := jwt.NewJWTValidator("")
payload, err := valid.ValidateToken(m.Data.Token)
if err != nil {
return err
}
id, ok := payload["username"].(string)
if !ok {
return ErrInvalidToken
}
s.ID = id
return nil
}
// Listen to websocket session and use handler upon message received
func (s *SessionMobile) Listen(ctx context.Context, producer kafka.ProducerInterface) error {
span, _ := tracer.StartSpanFromContext(ctx, "listen")
defer span.Finish()
key := s.Key()
for {
msg, op, err := s.Receive()
if op == ws.OpClose {
logger.At(logger.Info(), key, "conn").Msg("OpClose")
return nil
} else if err != nil {
logger.At(logger.Info(), key, "conn").Err(err).Send()
return err
}
err = s.Route(producer, msg)
if err != nil {
logger.At(logger.Warn(), key, "route").
Err(err).Send()
}
}
}
// Route Mobile messages
func (s *SessionMobile) Route(producer kafka.ProducerInterface, data []byte) error {
var m common.MessageRawJSON
err := m.Unmarshal(data)
if err != nil {
return err
}
key := s.Key()
logger.At(logger.Debug(), key, "route").
Str("handler", m.Handler).
Int("size", len(data)).
Msgf("received from %v", key)
topic, ok := kafka.MobileHandlerTopics[m.Handler]
if !ok {
return ErrInvalidHandler(m.Handler)
}
switch topic {
case kafka.AttendantServiceGRPCKafka:
valetData := common.AttendantRouteMobileGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.AttendantServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.DepotServiceGRPCKafka:
valetData := common.DepotRouteMobileToGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.ValetServiceGRPCKafka:
valetData := common.ValetRouteMobilePayloadGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.ValetServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
default:
err = producer.Produce(topic, key, m, nil)
}
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,86 @@
package websocket
import (
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/testhelper"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
func TestSessionMobile(t *testing.T) {
ws, _ := net.Pipe()
s := SessionMobile{
Session: &Session{
Websocket: ws,
},
}
if fmt.Sprintf("%T", s) != "websocket.SessionMobile" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionMobile", "websocket.SessionMobile", fmt.Sprintf("%T", s))
}
}
func TestNewSessionMobile(t *testing.T) {
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewMobileSession(w, r, "1.2.3.4")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionMobile", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.SessionMobile" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionMobile", "*websocket.SessionMobile", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, "")
defer conn.Close()
}
func TestSessionMobileAuthenticate(t *testing.T) {
userAgent := "Mobile 1.2.3.4"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewInsecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionMobileAuthenticate", nil, err)
}
defer s.Close()
err = s.Authenticate()
if err == nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionMobileAuthenticate", "error", nil)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
am := common.MobileSessionMessage{
Handler: "verify",
Data: common.MobileSessionData{
Token: "validtoken",
},
}
msg, err := json.Marshal(am)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionMobileAuthenticate", nil, err)
}
wsutil.WriteClientMessage(conn, ws.OpText, msg)
}

View File

@@ -0,0 +1,420 @@
package websocket
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
m "fiskerinc.com/modules/common"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/httpclient/mock"
kafka "fiskerinc.com/modules/kafka/mock"
"fiskerinc.com/modules/testhelper"
"github.com/gobwas/httphead"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
func createMockWebsocketClient(url, userAgent string) net.Conn {
u := "ws" + strings.TrimPrefix(url, "http")
header := make(http.Header)
header.Add("User-Agent", userAgent)
header.Add("X-ICCID", "12345678912345678923456789")
header.Add("Ssl-Client-Subject-Dn", "CN=1F15K3R45N1234567")
dialer := ws.Dialer{
Header: ws.HandshakeHeaderHTTP(header),
Extensions: []httphead.Option{httphead.NewOption("permessage-deflate", map[string]string{})},
}
ctx := context.Background()
ws, _, _, err := dialer.Dial(ctx, u)
if err != nil {
return nil
}
return ws
}
func TestSecureSessionTRex(t *testing.T) {
userAgent := "Fisker Ocean T.Rex 1.2.3.4 abc123"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionTRex", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.SessionTRex" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionTRex", "*websocket.SessionTRex", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
}
func TestSecureSessionHMI(t *testing.T) {
userAgent := "HMI 2.0.0.0"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionHMI", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.SessionHMI" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionHMI", "*websocket.SessionHMI", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
}
func TestInsecureSessionMobile(t *testing.T) {
userAgent := "Mobile 1.2.3.4"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewInsecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionMobile", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.SessionMobile" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionMobile", "*websocket.SessionMobile", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
}
func TestSession(t *testing.T) {
ws, _ := net.Pipe()
s := &Session{
Websocket: ws,
}
if fmt.Sprintf("%T", s) != "*websocket.Session" {
t.Errorf(testhelper.TestErrorTemplate, "TestSession", "*websocket.Session", fmt.Sprintf("%T", s))
}
}
func TestNewSession(t *testing.T) {
userAgent := ""
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSession", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.Session" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSession", "*websocket.Session", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
}
func TestSessionAuthenticate(t *testing.T) {
c := mock.Client{
DoFunc: func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200}, nil
},
}
httpclient.Client = &c
userAgent := ""
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionAuthenticate", nil, err)
}
defer s.Close()
err = s.Authenticate()
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionAuthenticate", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
ae := AuthEvent{
Topic: "auth_service",
Key: "FISKER123",
Payload: AuthPayload{
Handler: "verify",
Data: AuthData{
Token: "validtoken",
},
},
}
msg, err := json.Marshal(ae)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionAuthenticate", nil, err)
}
wsutil.WriteClientMessage(conn, ws.OpText, msg)
}
func TestSessionKey(t *testing.T) {
ws, _ := net.Pipe()
s := &Session{
Websocket: ws,
Type: m.TRex,
ID: "FISKER123",
}
key := s.Key()
if key != "1:FISKER123" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionKey", "1:FISKER123", key)
}
}
func TestSessionKeyUnknown(t *testing.T) {
ws, _ := net.Pipe()
s := &Session{
Websocket: ws,
Type: m.Unknown,
ID: "FISKER123",
}
key := s.Key()
if key != "FISKER123" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionKeyUnknown", "FISKER123", key)
}
}
func TestSessionReceive(t *testing.T) {
userAgent := ""
payload := "hello fisker!"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionReceive", nil, err)
}
defer s.Close()
data, _, err := s.Receive()
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionReceive", nil, err)
}
msg := string(data)
if msg != payload {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionReceive", payload, msg)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
err := wsutil.WriteClientMessage(conn, ws.OpText, []byte(payload))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionReceive", nil, err)
}
}
func TestSessionListen(t *testing.T) {
userAgent := ""
payload := "hello fisker!"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionListen", nil, err)
}
defer s.Close()
ctx := context.Background()
err = s.Listen(ctx, kafka.GetKafkaMock(nil))
if err.Error() != "EOF" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionListen", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
err := wsutil.WriteClientMessage(conn, ws.OpText, []byte(payload))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionListen", nil, err)
}
}
func TestSessionSend(t *testing.T) {
userAgent := ""
payload := "hello fisker!"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", nil, err)
}
defer s.Close()
data, _, err := s.Receive()
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", nil, err)
}
msg := string(data)
if msg != payload {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", payload, msg)
}
err = s.SendMsgToClient(data)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
err := wsutil.WriteClientMessage(conn, ws.OpText, []byte(payload))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", nil, err)
}
echo, _, err := wsutil.ReadServerData(conn)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", nil, err)
}
message := string(echo)
if message != payload {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", payload, message)
}
}
func TestSessionLoad(t *testing.T) {
ws, _ := net.Pipe()
s := &Session{
Websocket: ws,
Type: m.TRex,
ID: "FISKER123",
}
err := s.Load(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionLoad", nil, err)
}
}
func TestSessionTeardown(t *testing.T) {
ws, _ := net.Pipe()
s := &Session{
Websocket: ws,
Type: m.TRex,
ID: "FISKER123",
}
err := s.Load(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionLoad", nil, err)
}
err = s.Teardown(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionLoad", nil, err)
}
}
func TestSessionGetUUID(t *testing.T) {
ws, _ := net.Pipe()
currentNanoSeconds := time.Now().UnixNano()
s := &Session{
Websocket: ws,
Type: m.TRex,
ID: "FISKER123",
epoch: currentNanoSeconds,
}
uuid := s.GetUUID()
if uuid != currentNanoSeconds {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionLoad", currentNanoSeconds, uuid)
}
}
type SessionGetVINTestCase struct {
Name string
Session *Session
VIN string
ExpectedErrorMsg string
}
func TestSessionGetVIN(t *testing.T) {
ws, _ := net.Pipe()
tests := []SessionGetVINTestCase{
{
Name: "Parsable",
Session: &Session{
Websocket: ws,
Type: m.TRex,
ID: "VCF1EBU22PG001732",
},
VIN: "VCF1EBU22PG001732",
},
{
Name: "Not Parsable",
Session: &Session{
Websocket: ws,
Type: m.TRex,
ID: "FISKER123",
},
VIN: "FISKER123",
ExpectedErrorMsg: "could not get VIN from session",
},
}
for _, tt := range tests {
vin := tt.Session.GetID()
if vin != tt.VIN {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionGetVIN", tt.VIN, vin)
}
}
}

View File

@@ -0,0 +1,307 @@
package websocket
import (
"context"
"fmt"
"net/http"
"time"
"fiskerinc.com/modules/dbc/models"
"google.golang.org/protobuf/proto"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/grpc/kafka_grpc"
"fiskerinc.com/modules/kafka"
"fiskerinc.com/modules/logger"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsflate"
"github.com/pkg/errors"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
// NewTRexSession serves as the constructor for TRex sessions
func NewTRexSession(w http.ResponseWriter, r *http.Request, vin, version, iccid string) (SessionInterface, error) {
var s SessionInterface
var compressionNegotiator = wsflate.Extension{
Parameters: wsflate.DefaultParameters,
}
var websocketUpgrader = ws.HTTPUpgrader{
Negotiate: compressionNegotiator.Negotiate,
}
conn, _, _, err := websocketUpgrader.Upgrade(r, w)
if err != nil {
return s, errors.WithStack(err)
}
if _, ok := compressionNegotiator.Accepted(); !ok {
conn.Close()
return s, errors.Errorf("didn't negotiate compression for %s", conn.RemoteAddr())
}
dbc := ParseDBCFromRequest(r)
return &SessionTRex{
Session: &Session{
Websocket: conn,
Type: common.TRex,
Version: version,
ID: vin,
epoch: time.Now().UnixNano(),
},
DBC: dbc,
ICCID: iccid,
}, nil
}
// SessionTRex utilizes a specialized listener
type SessionTRex struct {
*Session
DBC string
ICCID string
}
// Authenticate returns id if proper authentication, else returns error
func (s *SessionTRex) Authenticate() error {
return nil
}
func (s *SessionTRex) Receive() ([]byte, ws.OpCode, error) {
return s.receive(s.extendDeadline)
}
// Listen to websocket session and use handler upon message received
func (s *SessionTRex) Listen(ctx context.Context, producer kafka.ProducerInterface) error {
span, _ := tracer.StartSpanFromContext(ctx, "listen")
defer span.Finish()
defer s.Close()
for {
key := s.Key()
msg, op, err := s.Receive()
if op == ws.OpClose {
logger.At(logger.Info(), key, "conn").Msg("OpClose")
return nil
} else if err != nil {
logger.At(logger.Info(), key, "conn").Err(err).Send()
return err
}
err = s.Route(producer, msg)
if err != nil {
logger.At(logger.Warn(), key, "route").Err(err).Send()
}
}
}
// Route TRex messages
func (s *SessionTRex) Route(producer kafka.ProducerInterface, data []byte) error {
// TODO Unmarshal message and extract CAN frames into Kafka
var m common.MessageRawJSON
var err error
err = m.Unmarshal(data)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("msg %s", string(data)))
}
key := s.Key()
topic, ok := kafka.TRexHandlerTopics[m.Handler]
if !ok {
return ErrInvalidHandler(m.Handler)
}
switch topic {
case kafka.VehicleData:
m.Version = models.GetShortKey(s.DBC)
car := common.CarDataBatchPayload{}
grpcCan, err := car.ToGrpc(m, s.GetID())
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to convert to GRPC")).Send()
return err
}
if s.ID == "VCF1ZBU29PG004227" {
ids := ""
event := logger.Warn()
for _, f := range grpcCan.Data.Frames{
// f.Value
ids = fmt.Sprintf("%s, %d", ids, f.ID)
event.Str(fmt.Sprintf("%d",f.ID), fmt.Sprintf("%X", f.Value))
}
event.Msg(s.ID)
}
grpcData, err := proto.Marshal(grpcCan)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal GRPC")).Send()
return err
}
err = producer.ProduceBinary(kafka.VehicleData, s.GetID(), grpcData, nil)
grpcData = nil
grpcCan = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer failed")).Send()
return err
}
case kafka.LogService:
var grpcLogs *kafka_grpc.TRexLogs_BatchPayload
switch m.Handler {
case "trex_log":
logs := common.TRexLogs{}
grpcLogs, err = logs.ToGrpc(m)
case "error":
errorr := common.TRexError{}
grpcLogs, err = errorr.ToGrpc(m)
}
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to convert trex logs to GRPC"+string(m.Data[:]))).Send()
return err
}
grpcData, err := proto.Marshal(grpcLogs)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC"+string(m.Data[:]))).Send()
return err
}
err = producer.ProduceBinary(kafka.LogServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
grpcLogs = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.LogServiceGRPCKafka: // This case should not be necessary, but just in case someone gets confused, it is in here
var grpcLogs *kafka_grpc.TRexLogs_BatchPayload
switch m.Handler {
case "trex_log":
logs := common.TRexLogs{}
grpcLogs, err = logs.ToGrpc(m)
case "error":
errorr := common.TRexError{}
grpcLogs, err = errorr.ToGrpc(m)
}
// TODO: unable to convert trex logs msg {"code":-32601,"message":"The handler does not exist or is not available"}: json: cannot unmarshal string into Go struct field TRexError.message of type []common.TRexErrorMessage
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to convert trex logs msg "+string(m.Data[:]))).Send()
return err
}
grpcData, err := proto.Marshal(grpcLogs)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC"+string(m.Data[:]))).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.LogServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
grpcLogs = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.ValetServiceGRPCKafka:
valetData := common.ValetRouteTRexPayloadGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.ValetServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.DepotServiceGRPCKafka:
valetData := common.DepotRouteTRexToGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.AttendantServiceGRPCKafka:
valetData := common.AttendantRouteTRexGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.AttendantServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
default:
err = producer.Produce(topic, key, m, nil)
if err != nil {
return err
}
}
return nil
}
func (s *SessionTRex) KafkaEndSessionMarker(producer kafka.ProducerInterface) error {
can := kafka_grpc.GRPC_BatchPayload{
Handler: "",
Data: nil,
Version: models.GetShortKey(s.DBC),
}
grpcData, _ := proto.Marshal(&can)
key := s.Key()
logger.At(logger.Info(), key, "conn").
Msgf("closing connection %s", key)
return producer.ProduceBinary(kafka.VehicleData, s.GetID(), grpcData, nil)
}
func (s *SessionTRex) Teardown(producer kafka.ProducerInterface) error {
s.KafkaEndSessionMarker(producer)
return s.Session.Teardown(producer)
}
// Load the session - distributes messages to system notifing of new connection
func (s *SessionTRex) Load(producer kafka.ProducerInterface) error {
m := &kafka_grpc.GRPC_DepotPayload_InitPayload{
InitPayload: &kafka_grpc.InitPayload{
Data: map[string]string{
"version": s.Version,
"iccid": s.ICCID,
"ip": s.GetIP(),
"dbc_version": s.DBC,
},
},
}
payload := kafka_grpc.GRPC_DepotPayload{
Handler: "init",
Data: m,
}
binaryPayload, _ := proto.Marshal(&payload)
return producer.ProduceBinary(kafka.DepotServiceGRPCKafka, s.Key(), binaryPayload, nil)
}

View File

@@ -0,0 +1,191 @@
package websocket
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/grpc/kafka_grpc"
kafka "fiskerinc.com/modules/kafka/mock"
"fiskerinc.com/modules/testhelper"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
func TestSessionTRex(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionTRex{
Session: &Session{
Websocket: ws,
},
}
if fmt.Sprintf("%T", s) != "*websocket.SessionTRex" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRex", "*websocket.SessionTRex", fmt.Sprintf("%T", s))
}
}
func TestNewTRexSession(t *testing.T) {
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewTRexSession(w, r, "1F15K3R45N1234567", "1.2.3.4", "12345678912346789123456789")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionTRex", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.SessionTRex" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionTRex", "*websocket.SessionTRex", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, "")
defer conn.Close()
}
func TestSessionTRexAuthenticate(t *testing.T) {
userAgent := "Fisker Ocean T.Rex 1.2.3.4 abc123"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexAuthenticate", nil, err)
}
defer s.Close()
err = s.Authenticate()
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexAuthenticate", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
am := common.TRexSessionMessage{
Handler: "verify",
Data: common.TRexSessionData{
VIN: "1HD1CGP134K410769",
},
}
msg, err := json.Marshal(am)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexAuthenticate", nil, err)
}
wsutil.WriteClientMessage(conn, ws.OpText, msg)
}
func TestSessionTRexListen(t *testing.T) {
userAgent := "Fisker Ocean T.Rex 1.2.3.4 abc123"
payload := "hello fisker!"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexListen", nil, err)
}
defer s.Close()
ctx := context.Background()
err = s.Listen(ctx, kafka.GetKafkaMock(nil))
if err.Error() != "EOF" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexListen", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
err := wsutil.WriteClientMessage(conn, ws.OpText, []byte(payload))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexListen", nil, err)
}
}
func TestSessionTRexRoute(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionTRex{
Session: &Session{
Websocket: ws,
},
}
msg := common.Message{
Handler: "canbus",
Data: &kafka_grpc.GRPC_CANData{
EpochUsec: 1653255445,
Dropped: 10,
Filtered: 20,
Frames: []*kafka_grpc.GRPC_CANFrame{
{
Epoch: 1642455023642165,
ID: 832,
Value: []byte("AAAAAAAAIAE="),
},
},
},
}
data, err := json.Marshal(msg)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexRoute", nil, err)
}
err = s.Route(kafka.GetKafkaMock(nil), data)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexRoute", nil, err)
}
}
func TestSessionTRexLoad(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionTRex{
Session: &Session{
Websocket: ws,
Type: common.TRex,
ID: "FISKER123",
},
}
err := s.Load(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexLoad", nil, err)
}
}
func TestSessionTRexTeardown(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionTRex{
Session: &Session{
Websocket: ws,
Type: common.TRex,
ID: "FISKER123",
},
}
err := s.Load(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexTeardown", nil, err)
}
err = s.Teardown(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexTeardown", nil, err)
}
}