summaryrefslogtreecommitdiff
path: root/internal/server/api
diff options
context:
space:
mode:
authorKyren223 <Kyren223@proton.me>2025-07-17 18:45:43 +0300
committerKyren223 <Kyren223@proton.me>2025-07-19 18:32:12 +0300
commitf793203e043aaa2db38823c84500e82756348ac0 (patch)
tree88a8a7b78069427f11acb0f2040e9f1d391fab55 /internal/server/api
parentaaea258a21226a893025a65222e244c6b3f4454c (diff)
Added session duration metrics with device analytics along with device
analytics metrics in the DB to aggregate and DeviceID abuse prevention
Diffstat (limited to 'internal/server/api')
-rw-r--r--internal/server/api/api.go58
-rw-r--r--internal/server/api/helpers.go36
-rw-r--r--internal/server/api/migrations/20250717130820_device_analytics.sql11
3 files changed, 102 insertions, 3 deletions
diff --git a/internal/server/api/api.go b/internal/server/api/api.go
index 8b1d711..5159186 100644
--- a/internal/server/api/api.go
+++ b/internal/server/api/api.go
@@ -5,10 +5,12 @@ import (
"context"
"crypto/ed25519"
"database/sql"
+ "encoding/binary"
"fmt"
"log/slog"
"strconv"
"strings"
+ "sync"
"github.com/kyren223/eko/internal/data"
"github.com/kyren223/eko/internal/packet"
@@ -34,7 +36,7 @@ func SendMessage(ctx context.Context, sess *session.Session, request *packet.Sen
if len(request.Content) > packet.MaxMessageBytes {
return &packet.Error{Error: fmt.Sprintf(
- "message conent must not exceed %v bytes",
+ "message content must not exceed %v bytes",
packet.MaxMessageBytes,
)}
}
@@ -1647,7 +1649,57 @@ func sendInitialAuthPackets(ctx context.Context, sess *session.Session) bool {
return success
}
+var (
+ ipDeviceID map[uint32]string = map[uint32]string{}
+ deviceIdMu sync.Mutex
+)
+
func DeviceAnalytics(ctx context.Context, sess *session.Session, request *packet.DeviceAnalytics) packet.Payload {
- // TODO: implement this
- return &ErrNotImplemented
+ const DeviceIdLength = 64
+
+ if len(request.DeviceID) != DeviceIdLength {
+ return &packet.Error{Error: fmt.Sprintf(
+ "DeviceID must be exactly %v bytes", DeviceIdLength,
+ )}
+ }
+
+ for _, c := range request.DeviceID {
+ if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
+ return &packet.Error{
+ Error: "DeviceID must be all lowercase hexadecimal",
+ }
+ }
+ }
+
+ ip := binary.BigEndian.Uint32(sess.Addr().IP.To4())
+ deviceIdMu.Lock()
+ if deviceId, ok := ipDeviceID[ip]; ok {
+ if request.DeviceID != deviceId {
+ slog.WarnContext(ctx, "device ID mismatch", "existing_device_id", deviceId, "request_device_id", request.DeviceID)
+ request.DeviceID = deviceId
+ // Override the request to use the known ID
+ // This avoids abuse
+ }
+ } else {
+ ipDeviceID[ip] = request.DeviceID
+ }
+ deviceIdMu.Unlock()
+
+ if !IsValidAnalytics(ctx, request) {
+ // This is either malicious or we should actually add new variations
+ // In either case the client shouldn't need a response
+ return nil
+ }
+
+ sess.SetAnalytics(request)
+ queries := data.New(db)
+ queries.SetDeviceAnalytics(ctx, data.SetDeviceAnalyticsParams{
+ DeviceID: request.DeviceID,
+ Os: &request.OS,
+ Arch: &request.Arch,
+ Term: &request.Term,
+ Colorterm: &request.Colorterm,
+ })
+
+ return nil
}
diff --git a/internal/server/api/helpers.go b/internal/server/api/helpers.go
index ea192df..6993d0a 100644
--- a/internal/server/api/helpers.go
+++ b/internal/server/api/helpers.go
@@ -3,6 +3,7 @@ package api
import (
"context"
"log/slog"
+ "slices"
"strings"
"time"
@@ -190,3 +191,38 @@ func getNotifications(ctx context.Context, userId snowflake.ID) (packet.Notifica
}
return items, nil
}
+
+var (
+ ValidOs = []string{"linux", "darwin", "windows", "android", ""}
+ ValidArch = []string{"amd64", "arm64", "386", ""}
+ ValidTerm = []string{"xterm-256color", "tmux-256color", "xterm-ghostty", "xterm-kitty", "alacritty", "foot", "xterm", ""}
+ ValidColorterm = []string{"truecolor", "24bit", ""}
+)
+
+func IsValidAnalytics(ctx context.Context, analytics *packet.DeviceAnalytics) bool {
+ if analytics == nil {
+ return false
+ }
+
+ if !slices.Contains(ValidOs, analytics.OS) {
+ slog.WarnContext(ctx, "unrecognized analytics value", "os", analytics.OS, "analytics", analytics)
+ return false
+ }
+
+ if !slices.Contains(ValidArch, analytics.Arch) {
+ slog.WarnContext(ctx, "unrecognized analytics value", "arch", analytics.Arch, "analytics", analytics)
+ return false
+ }
+
+ if !slices.Contains(ValidTerm, analytics.Term) {
+ slog.WarnContext(ctx, "unrecognized analytics value", "term", analytics.Term, "analytics", analytics)
+ return false
+ }
+
+ if !slices.Contains(ValidColorterm, analytics.Colorterm) {
+ slog.WarnContext(ctx, "unrecognized analytics value", "colorterm", analytics.Colorterm, "analytics", analytics)
+ return false
+ }
+
+ return true
+}
diff --git a/internal/server/api/migrations/20250717130820_device_analytics.sql b/internal/server/api/migrations/20250717130820_device_analytics.sql
new file mode 100644
index 0000000..b4797a4
--- /dev/null
+++ b/internal/server/api/migrations/20250717130820_device_analytics.sql
@@ -0,0 +1,11 @@
+-- +goose Up
+CREATE TABLE IF NOT EXISTS device_analytics (
+ device_id TEXT PRIMARY KEY,
+ os TEXT,
+ arch TEXT,
+ term TEXT,
+ colorterm TEXT
+);
+
+-- +goose Down
+DROP TABLE IF EXISTS device_analytics;