mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-02 23:57:59 +00:00
fix: WebhookPing csv tags + JSON payload column; DeviceChecks struct; fix Authenticate deadlock
Co-authored-by: coolaj86 <122831+coolaj86@users.noreply.github.com>
This commit is contained in:
parent
bf3c4a48e4
commit
a708352634
@ -376,9 +376,10 @@ func (a *Auth) Authenticate(name, secret string) (auth.BasicPrinciple, error) {
|
||||
}
|
||||
|
||||
a.mux.Lock()
|
||||
defer a.mux.Unlock()
|
||||
nameID := a.nameCacheID(name)
|
||||
c, ok := a.hashedCredentials[nameID]
|
||||
a.mux.Unlock()
|
||||
|
||||
if ok {
|
||||
if err := c.Verify(name, secret); err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -4,13 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -37,23 +35,6 @@ var pingWriter jsonl.Writer
|
||||
|
||||
var smsAuth *csvauth.Auth
|
||||
|
||||
// pingRow is a flat CSV row for a ping event (x-* headers omitted, health fields inlined).
|
||||
type pingRow struct {
|
||||
DeviceID string `csv:"deviceId"`
|
||||
Event string `csv:"event"`
|
||||
ID string `csv:"id"`
|
||||
WebhookID string `csv:"webhookId"`
|
||||
PingedAt time.Time `csv:"pingedAt"`
|
||||
Status string `csv:"status"`
|
||||
Version string `csv:"version"`
|
||||
BatteryLevel float64 `csv:"batteryLevel"`
|
||||
BatteryCharging float64 `csv:"batteryCharging"`
|
||||
ConnectionStatus float64 `csv:"connectionStatus"`
|
||||
ConnectionTransport float64 `csv:"connectionTransport"`
|
||||
ConnectionCellular float64 `csv:"connectionCellular"`
|
||||
MessagesFailed float64 `csv:"messagesFailed"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
jsonf.Indent = 3
|
||||
|
||||
@ -127,68 +108,68 @@ func main() {
|
||||
mux.Handle("PUT /", LogRequest(http.HandlerFunc(HandleOK)))
|
||||
mux.Handle("DELETE /", LogRequest(http.HandlerFunc(HandleOK)))
|
||||
|
||||
// Protected routes under /api/smsgw, authenticated via csvauth with the "sms" role.
|
||||
smsgw := mwpkg.WithMux(mux, smsAuthMiddleware)
|
||||
smsgw.HandleFunc("GET /api/smsgw/received.csv", handlerReceivedCSV)
|
||||
smsgw.HandleFunc("GET /api/smsgw/received.json", handlerReceivedJSON)
|
||||
smsgw.HandleFunc("GET /api/smsgw/sent.csv", handlerSentCSV)
|
||||
smsgw.HandleFunc("GET /api/smsgw/sent.json", handlerSentJSON)
|
||||
smsgw.HandleFunc("GET /api/smsgw/ping.csv", handlerPingCSV)
|
||||
smsgw.HandleFunc("GET /api/smsgw/ping.json", handlerPingJSON)
|
||||
// Protected routes under /api/smsgw, each guarded by its specific sms:* permission.
|
||||
smsgw := mwpkg.WithMux(mux)
|
||||
smsgw.With(requireSMSPermission("sms:received")).HandleFunc("GET /api/smsgw/received.csv", handlerReceivedCSV)
|
||||
smsgw.With(requireSMSPermission("sms:received")).HandleFunc("GET /api/smsgw/received.json", handlerReceivedJSON)
|
||||
smsgw.With(requireSMSPermission("sms:sent")).HandleFunc("GET /api/smsgw/sent.csv", handlerSentCSV)
|
||||
smsgw.With(requireSMSPermission("sms:sent")).HandleFunc("GET /api/smsgw/sent.json", handlerSentJSON)
|
||||
smsgw.With(requireSMSPermission("sms:ping")).HandleFunc("GET /api/smsgw/ping.csv", handlerPingCSV)
|
||||
smsgw.With(requireSMSPermission("sms:ping")).HandleFunc("GET /api/smsgw/ping.json", handlerPingJSON)
|
||||
|
||||
addr := "localhost:8088"
|
||||
fmt.Printf("Listening on %s...\n\n", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, chiMiddleware.Logger(chiMiddleware.Compress(5)(mux))))
|
||||
}
|
||||
|
||||
// smsAuthMiddleware requires csvauth credentials with the "sms" role.
|
||||
func smsAuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if smsAuth == nil {
|
||||
w.Header().Set("WWW-Authenticate", `Basic`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
// hasSMSPermission reports whether perms includes the wildcard "sms:*" or the specific permission.
|
||||
func hasSMSPermission(perms []string, permission string) bool {
|
||||
for _, p := range perms {
|
||||
if p == "sms:*" || p == permission {
|
||||
return true
|
||||
}
|
||||
cred, err := authenticateSMS(r)
|
||||
if err != nil || !slices.Contains(cred.Permissions(), "sms") {
|
||||
w.Header().Set("WWW-Authenticate", `Basic`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// requireSMSPermission returns a middleware that authenticates the request and enforces
|
||||
// that the credential holds "sms:*" or the given specific permission.
|
||||
func requireSMSPermission(permission string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if smsAuth == nil {
|
||||
w.Header().Set("WWW-Authenticate", `Basic`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
cred, err := authenticateSMS(r)
|
||||
if err != nil || !hasSMSPermission(cred.Permissions(), permission) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// authenticateSMS extracts and verifies credentials from Basic Auth, Authorization header, or API-Key header.
|
||||
func authenticateSMS(r *http.Request) (auth.BasicPrinciple, error) {
|
||||
// 1. Try Basic Auth
|
||||
// 1. Basic Auth — Authenticate handles both login credentials and token-as-username/password.
|
||||
if username, password, ok := r.BasicAuth(); ok {
|
||||
cred, err := smsAuth.Authenticate(username, password)
|
||||
if !errors.Is(err, csvauth.ErrNotFound) {
|
||||
if err == nil {
|
||||
return cred, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// Basic Auth was used to carry a token (username is a token name like "", "api", "apikey").
|
||||
token := password
|
||||
if password == "" {
|
||||
token = username
|
||||
}
|
||||
return smsAuth.Authenticate("", token)
|
||||
return smsAuth.Authenticate(username, password)
|
||||
}
|
||||
|
||||
// 2. Try Authorization: <scheme> <token>
|
||||
// 2. Authorization: <scheme> <token>
|
||||
if authHeader := r.Header.Get("Authorization"); authHeader != "" {
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) == 2 {
|
||||
token := strings.TrimSpace(parts[1])
|
||||
return smsAuth.Authenticate("", token)
|
||||
return smsAuth.Authenticate("", strings.TrimSpace(parts[1]))
|
||||
}
|
||||
return nil, csvauth.ErrUnauthorized
|
||||
}
|
||||
|
||||
// 3. Try API-Key / X-API-Key headers
|
||||
// 3. API-Key / X-API-Key headers
|
||||
for _, h := range []string{"API-Key", "X-API-Key"} {
|
||||
if key := r.Header.Get(h); key != "" {
|
||||
return smsAuth.Authenticate("", key)
|
||||
@ -347,29 +328,16 @@ func handlerPingCSV(w http.ResponseWriter, r *http.Request) {
|
||||
since, limit := parseSinceLimit(r)
|
||||
|
||||
webhookMux.Lock()
|
||||
rows := make([]pingRow, 0, min(len(pingEvents), limit))
|
||||
rows := make([]*androidsmsgateway.WebhookPing, 0, min(len(pingEvents), limit))
|
||||
for _, ping := range pingEvents {
|
||||
pingedAt := time.UnixMilli(ping.XTimestamp).UTC()
|
||||
pingedAt := ping.PingedAt
|
||||
if pingedAt.IsZero() {
|
||||
pingedAt = time.UnixMilli(ping.XTimestamp).UTC()
|
||||
}
|
||||
if !since.IsZero() && !pingedAt.After(since) {
|
||||
continue
|
||||
}
|
||||
h := ping.Payload.Health
|
||||
row := pingRow{
|
||||
DeviceID: ping.DeviceID,
|
||||
Event: ping.Event,
|
||||
ID: ping.ID,
|
||||
WebhookID: ping.WebhookID,
|
||||
PingedAt: pingedAt,
|
||||
Status: h.Status,
|
||||
Version: h.Version,
|
||||
BatteryLevel: h.Checks["battery:level"].ObservedValue,
|
||||
BatteryCharging: h.Checks["battery:charging"].ObservedValue,
|
||||
ConnectionStatus: h.Checks["connection:status"].ObservedValue,
|
||||
ConnectionTransport: h.Checks["connection:transport"].ObservedValue,
|
||||
ConnectionCellular: h.Checks["connection:cellular"].ObservedValue,
|
||||
MessagesFailed: h.Checks["messages:failed"].ObservedValue,
|
||||
}
|
||||
rows = append(rows, row)
|
||||
rows = append(rows, ping)
|
||||
if len(rows) >= limit {
|
||||
break
|
||||
}
|
||||
@ -391,7 +359,10 @@ func handlerPingJSON(w http.ResponseWriter, r *http.Request) {
|
||||
webhookMux.Lock()
|
||||
rows := make([]*androidsmsgateway.WebhookPing, 0, min(len(pingEvents), limit))
|
||||
for _, ping := range pingEvents {
|
||||
pingedAt := time.UnixMilli(ping.XTimestamp).UTC()
|
||||
pingedAt := ping.PingedAt
|
||||
if pingedAt.IsZero() {
|
||||
pingedAt = time.UnixMilli(ping.XTimestamp).UTC()
|
||||
}
|
||||
if !since.IsZero() && !pingedAt.After(since) {
|
||||
continue
|
||||
}
|
||||
@ -599,6 +570,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
switch h.GetEvent() {
|
||||
case "system:ping":
|
||||
ping := h.(*androidsmsgateway.WebhookPing)
|
||||
ping.PingedAt = time.UnixMilli(webhook.XTimestamp).UTC()
|
||||
webhookMux.Lock()
|
||||
defer webhookMux.Unlock()
|
||||
if err := pingWriter.Write(ping); err != nil {
|
||||
|
||||
@ -96,13 +96,14 @@ type WebhookReceivedPayload struct {
|
||||
|
||||
// WebhookPing represents a system:ping webhook event.
|
||||
type WebhookPing struct {
|
||||
DeviceID string `json:"deviceId"`
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
Payload WebhookPingPayload `json:"payload"`
|
||||
WebhookID string `json:"webhookId"`
|
||||
XSignature string `json:"x-signature"`
|
||||
XTimestamp int64 `json:"x-timestamp"`
|
||||
DeviceID string `json:"deviceId" csv:"deviceId"`
|
||||
Event string `json:"event" csv:"event"`
|
||||
ID string `json:"id" csv:"id"`
|
||||
Payload WebhookPingPayload `json:"payload" csv:"payload"`
|
||||
WebhookID string `json:"webhookId" csv:"webhookId"`
|
||||
PingedAt time.Time `json:"pingedAt,omitempty" csv:"pingedAt"`
|
||||
XSignature string `json:"x-signature" csv:"-"`
|
||||
XTimestamp int64 `json:"x-timestamp" csv:"-"`
|
||||
}
|
||||
|
||||
// GetEvent marks WebhookPing as part of the WebhookEvent interface.
|
||||
@ -111,16 +112,42 @@ func (w *WebhookPing) GetEvent() string {
|
||||
}
|
||||
|
||||
// WebhookPingPayload contains the health data reported by a system:ping event.
|
||||
// MarshalText serialises the payload as a compact JSON string so that csvutil
|
||||
// stores it as a single "payload" column rather than expanding every health check.
|
||||
type WebhookPingPayload struct {
|
||||
Health DeviceHealth `json:"health"`
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler for CSV serialisation.
|
||||
func (p WebhookPingPayload) MarshalText() ([]byte, error) {
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// MarshalJSON prevents MarshalText from being used during JSON serialisation,
|
||||
// ensuring WebhookPingPayload is always encoded as a full JSON object.
|
||||
func (p WebhookPingPayload) MarshalJSON() ([]byte, error) {
|
||||
type alias WebhookPingPayload
|
||||
return json.Marshal(alias(p))
|
||||
}
|
||||
|
||||
// DeviceHealth is the top-level health object inside a system:ping payload.
|
||||
// Named fields use colon json tags matching the API key names inside "checks".
|
||||
type DeviceHealth struct {
|
||||
Checks map[string]HealthCheck `json:"checks"`
|
||||
ReleaseID int `json:"releaseId"`
|
||||
Status string `json:"status"`
|
||||
Version string `json:"version"`
|
||||
Checks DeviceChecks `json:"checks"`
|
||||
ReleaseID int `json:"releaseId"`
|
||||
Status string `json:"status"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// DeviceChecks holds the individual health checks reported by the device.
|
||||
// Go field names are camelCase; json tags carry the colon-delimited API key names.
|
||||
type DeviceChecks struct {
|
||||
BatteryCharging HealthCheck `json:"battery:charging"`
|
||||
BatteryLevel HealthCheck `json:"battery:level"`
|
||||
ConnectionCellular HealthCheck `json:"connection:cellular"`
|
||||
ConnectionStatus HealthCheck `json:"connection:status"`
|
||||
ConnectionTransport HealthCheck `json:"connection:transport"`
|
||||
MessagesFailed HealthCheck `json:"messages:failed"`
|
||||
}
|
||||
|
||||
// HealthCheck represents a single named health check result.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user