731b60863a
Tunnel method now requires Bearer JWT (HMAC-HS256) in gRPC metadata.
- internal/auth: pure-stdlib verifier + issuer + revoked-jti list
- internal/tunnel: per-target rate limiter + egress allowlist with
wildcard support + crypto/rand frame padding to {1KB, 4KB, 16KB}
- cmd/server reads /etc/stats-gateway/{jwt.key,tunnel.yaml,revoked.txt}
via --config-dir flag
- cmd/issue-token Go binary wraps auth.Issue
- configs/tunnel.yaml example with projectshitpost wildcard
Tests: 13 PASS across 5 packages.
117 lines
3.3 KiB
Go
117 lines
3.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
statsv1 "github.com/bergabruh/stats-gateway/gen/stats/v1"
|
|
"github.com/bergabruh/stats-gateway/internal/auth"
|
|
"github.com/bergabruh/stats-gateway/internal/storage"
|
|
"github.com/bergabruh/stats-gateway/internal/tunnel"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// Service implements statsv1.SpeedStatusServer.
|
|
type Service struct {
|
|
statsv1.UnimplementedSpeedStatusServer
|
|
store *storage.Store
|
|
verifier *auth.Verifier
|
|
tunnel *tunnel.Handler
|
|
}
|
|
|
|
// New creates a Service with the given storage, JWT verifier, and tunnel handler.
|
|
func New(st *storage.Store, v *auth.Verifier, t *tunnel.Handler) *Service {
|
|
return &Service{store: st, verifier: v, tunnel: t}
|
|
}
|
|
|
|
func (s *Service) RecentResults(f *statsv1.Filter, stream statsv1.SpeedStatus_RecentResultsServer) error {
|
|
limit := int(f.Limit)
|
|
if limit <= 0 || limit > 1000 {
|
|
limit = 100
|
|
}
|
|
var since time.Time
|
|
if f.Since != "" {
|
|
t, err := time.Parse(time.RFC3339, f.Since)
|
|
if err != nil {
|
|
return status.Errorf(codes.InvalidArgument, "since: %v", err)
|
|
}
|
|
since = t
|
|
}
|
|
samples, err := s.store.QueryRecent(stream.Context(), f.Node, limit, since)
|
|
if err != nil {
|
|
return status.Errorf(codes.Internal, "query: %v", err)
|
|
}
|
|
for _, r := range samples {
|
|
err := stream.Send(&statsv1.Sample{
|
|
Node: r.Node,
|
|
Timestamp: r.Timestamp.Format(time.RFC3339Nano),
|
|
HttpLatencyMs: r.HTTPLatencyMs,
|
|
TcpRttMs: r.TCPRttMs,
|
|
DnsResolveMs: r.DNSResolveMs,
|
|
TlsHandshakeMs: r.TLSHandshakeMs,
|
|
ThroughputKbps: r.ThroughputKbps,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// aggregateBounds defines bucket upper bounds (ms) used by Aggregate.
|
|
var aggregateBounds = []float64{25, 50, 100, 250, 500, 1000, 5000}
|
|
|
|
func (s *Service) Aggregate(ctx context.Context, r *statsv1.Range) (*statsv1.Histogram, error) {
|
|
since, _ := time.Parse(time.RFC3339, r.Since)
|
|
until, _ := time.Parse(time.RFC3339, r.Until)
|
|
if until.IsZero() {
|
|
until = time.Now().UTC()
|
|
}
|
|
if since.IsZero() {
|
|
return nil, status.Error(codes.InvalidArgument, "since required")
|
|
}
|
|
|
|
samples, err := s.store.QueryRange(ctx, r.Node, since, until)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "query: %v", err)
|
|
}
|
|
|
|
counts := make([]int64, len(aggregateBounds))
|
|
for _, sm := range samples {
|
|
for i, b := range aggregateBounds {
|
|
if sm.HTTPLatencyMs <= b {
|
|
counts[i]++
|
|
break
|
|
}
|
|
}
|
|
}
|
|
h := &statsv1.Histogram{}
|
|
for i, b := range aggregateBounds {
|
|
h.Buckets = append(h.Buckets, &statsv1.Bucket{UpperMs: b, Count: counts[i]})
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
// Tunnel authenticates the caller via Bearer JWT then delegates to the tunnel handler.
|
|
func (s *Service) Tunnel(stream statsv1.SpeedStatus_TunnelServer) error {
|
|
md, ok := metadata.FromIncomingContext(stream.Context())
|
|
if !ok {
|
|
return status.Error(codes.Unauthenticated, "no metadata")
|
|
}
|
|
authH := md.Get("authorization")
|
|
if len(authH) == 0 {
|
|
return status.Error(codes.Unauthenticated, "no auth")
|
|
}
|
|
tok := strings.TrimPrefix(authH[0], "Bearer ")
|
|
if tok == authH[0] {
|
|
return status.Error(codes.Unauthenticated, "expected Bearer")
|
|
}
|
|
if _, err := s.verifier.Verify(tok); err != nil {
|
|
return status.Errorf(codes.Unauthenticated, "auth: %v", err)
|
|
}
|
|
return s.tunnel.Serve(stream)
|
|
}
|