diff options
| author | bndw <ben@bdw.to> | 2026-02-14 12:14:19 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-14 12:14:19 -0800 |
| commit | ea4f508f5ee91b370c6912cde26b1a432380d037 (patch) | |
| tree | 79081398bc0da1db76c28de6de04ed88a5e53bc3 /cmd | |
| parent | 4fc493e6d8cc20137f920f8647e39fc5051bb245 (diff) | |
feat: integrate config system into relay main.go
Add support for loading configuration from YAML file via -config flag.
Wire up auth, rate limiting, and metrics interceptors based on config.
Changes:
- Add -config flag to relay command
- Use config types directly in auth package (AuthOperationConfig)
- Add conversion methods: RateLimitConfig.ToRateLimiter(), MetricsConfig.ToMetrics()
- Add Metrics.Serve() method for prometheus HTTP endpoint
- Update main.go to initialize interceptors from config
- Fix type naming: OperationAuthConfig -> AuthOperationConfig for consistency
Config now supports complete relay setup including auth read/write
allowlists, rate limiting, and prometheus metrics.
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/relay/main.go | 103 |
1 files changed, 75 insertions, 28 deletions
diff --git a/cmd/relay/main.go b/cmd/relay/main.go index 3a1eeef..209d758 100644 --- a/cmd/relay/main.go +++ b/cmd/relay/main.go | |||
| @@ -1,6 +1,7 @@ | |||
| 1 | package main | 1 | package main |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "context" | ||
| 4 | "flag" | 5 | "flag" |
| 5 | "log" | 6 | "log" |
| 6 | "net" | 7 | "net" |
| @@ -9,8 +10,6 @@ import ( | |||
| 9 | "os/signal" | 10 | "os/signal" |
| 10 | "syscall" | 11 | "syscall" |
| 11 | 12 | ||
| 12 | "context" | ||
| 13 | |||
| 14 | "connectrpc.com/connect" | 13 | "connectrpc.com/connect" |
| 15 | "golang.org/x/net/http2" | 14 | "golang.org/x/net/http2" |
| 16 | "golang.org/x/net/http2/h2c" | 15 | "golang.org/x/net/http2/h2c" |
| @@ -18,23 +17,27 @@ import ( | |||
| 18 | 17 | ||
| 19 | pb "northwest.io/muxstr/api/nostr/v1" | 18 | pb "northwest.io/muxstr/api/nostr/v1" |
| 20 | "northwest.io/muxstr/api/nostr/v1/nostrv1connect" | 19 | "northwest.io/muxstr/api/nostr/v1/nostrv1connect" |
| 20 | "northwest.io/muxstr/internal/auth" | ||
| 21 | "northwest.io/muxstr/internal/config" | ||
| 21 | connecthandler "northwest.io/muxstr/internal/handler/connect" | 22 | connecthandler "northwest.io/muxstr/internal/handler/connect" |
| 22 | grpchandler "northwest.io/muxstr/internal/handler/grpc" | 23 | grpchandler "northwest.io/muxstr/internal/handler/grpc" |
| 23 | wshandler "northwest.io/muxstr/internal/handler/websocket" | 24 | wshandler "northwest.io/muxstr/internal/handler/websocket" |
| 25 | "northwest.io/muxstr/internal/metrics" | ||
| 26 | "northwest.io/muxstr/internal/ratelimit" | ||
| 24 | "northwest.io/muxstr/internal/storage" | 27 | "northwest.io/muxstr/internal/storage" |
| 25 | "northwest.io/muxstr/internal/subscription" | 28 | "northwest.io/muxstr/internal/subscription" |
| 26 | ) | 29 | ) |
| 27 | 30 | ||
| 28 | func main() { | 31 | func main() { |
| 29 | var ( | 32 | configFile := flag.String("config", "", "Path to config file (optional)") |
| 30 | grpcAddr = flag.String("grpc-addr", ":50051", "gRPC server address") | ||
| 31 | wsAddr = flag.String("ws-addr", ":8080", "WebSocket server address") | ||
| 32 | dbPath = flag.String("db", "relay.db", "SQLite database path") | ||
| 33 | publicURL = flag.String("public-url", "", "Public URL for relay (e.g., nostr-grpc.x.bdw.to)") | ||
| 34 | ) | ||
| 35 | flag.Parse() | 33 | flag.Parse() |
| 36 | 34 | ||
| 37 | store, err := storage.New(*dbPath) | 35 | cfg, err := config.Load(*configFile) |
| 36 | if err != nil { | ||
| 37 | log.Fatalf("failed to load config: %v", err) | ||
| 38 | } | ||
| 39 | |||
| 40 | store, err := storage.New(cfg.Database.Path) | ||
| 38 | if err != nil { | 41 | if err != nil { |
| 39 | log.Fatalf("failed to create storage: %v", err) | 42 | log.Fatalf("failed to create storage: %v", err) |
| 40 | } | 43 | } |
| @@ -51,42 +54,86 @@ func main() { | |||
| 51 | path, handler := nostrv1connect.NewNostrRelayHandler(connectHandler, connect.WithInterceptors()) | 54 | path, handler := nostrv1connect.NewNostrRelayHandler(connectHandler, connect.WithInterceptors()) |
| 52 | mux.Handle(path, handler) | 55 | mux.Handle(path, handler) |
| 53 | 56 | ||
| 57 | var serverOpts []grpc.ServerOption | ||
| 58 | |||
| 59 | if cfg.Auth.Read.Enabled || cfg.Auth.Write.Enabled { | ||
| 60 | authOpts := &auth.InterceptorOptions{ | ||
| 61 | Read: cfg.Auth.Read, | ||
| 62 | Write: cfg.Auth.Write, | ||
| 63 | TimestampWindow: cfg.Auth.TimestampWindow, | ||
| 64 | SkipMethods: cfg.Auth.SkipMethods, | ||
| 65 | } | ||
| 66 | serverOpts = append(serverOpts, | ||
| 67 | grpc.UnaryInterceptor(auth.NostrUnaryInterceptor(authOpts)), | ||
| 68 | grpc.StreamInterceptor(auth.NostrStreamInterceptor(authOpts)), | ||
| 69 | ) | ||
| 70 | } | ||
| 71 | |||
| 72 | if cfg.RateLimit.Enabled { | ||
| 73 | limiter := ratelimit.New(cfg.RateLimit.ToRateLimiter()) | ||
| 74 | serverOpts = append(serverOpts, | ||
| 75 | grpc.ChainUnaryInterceptor(ratelimit.UnaryInterceptor(limiter)), | ||
| 76 | grpc.ChainStreamInterceptor(ratelimit.StreamInterceptor(limiter)), | ||
| 77 | ) | ||
| 78 | } | ||
| 79 | |||
| 80 | var m *metrics.Metrics | ||
| 81 | if cfg.Metrics.Enabled { | ||
| 82 | m = metrics.New(cfg.Metrics.ToMetrics()) | ||
| 83 | serverOpts = append(serverOpts, | ||
| 84 | grpc.ChainUnaryInterceptor(metrics.UnaryServerInterceptor(m)), | ||
| 85 | grpc.ChainStreamInterceptor(metrics.StreamServerInterceptor(m)), | ||
| 86 | ) | ||
| 87 | |||
| 88 | go func() { | ||
| 89 | log.Printf("Metrics server listening on %s%s", cfg.Metrics.Addr, cfg.Metrics.Path) | ||
| 90 | if err := m.Serve(cfg.Metrics.Addr, cfg.Metrics.Path); err != nil { | ||
| 91 | log.Printf("Metrics server failed: %v", err) | ||
| 92 | } | ||
| 93 | }() | ||
| 94 | } | ||
| 95 | |||
| 54 | wsHandler := wshandler.NewHandler(store, subManager) | 96 | wsHandler := wshandler.NewHandler(store, subManager) |
| 55 | 97 | ||
| 56 | // Set public URLs for index page | ||
| 57 | var grpcDisplay, httpDisplay, wsDisplay string | 98 | var grpcDisplay, httpDisplay, wsDisplay string |
| 58 | if *publicURL != "" { | 99 | if cfg.Server.PublicURL != "" { |
| 59 | // Use public URLs when behind reverse proxy (port 443) | 100 | grpcDisplay = cfg.Server.PublicURL + ":443" |
| 60 | grpcDisplay = *publicURL + ":443" | 101 | httpDisplay = "https://" + cfg.Server.PublicURL |
| 61 | httpDisplay = "https://" + *publicURL | 102 | wsDisplay = "wss://" + cfg.Server.PublicURL |
| 62 | wsDisplay = "wss://" + *publicURL | ||
| 63 | } else { | 103 | } else { |
| 64 | // Use local addresses for development | 104 | grpcDisplay = cfg.Server.GrpcAddr |
| 65 | grpcDisplay = *grpcAddr | 105 | httpDisplay = "http://" + cfg.Server.HttpAddr |
| 66 | httpDisplay = "http://" + *wsAddr | 106 | wsDisplay = "ws://" + cfg.Server.HttpAddr |
| 67 | wsDisplay = "ws://" + *wsAddr | ||
| 68 | } | 107 | } |
| 69 | wsHandler.SetIndexData(grpcDisplay, httpDisplay, wsDisplay) | 108 | wsHandler.SetIndexData(grpcDisplay, httpDisplay, wsDisplay) |
| 70 | mux.Handle("/", wsHandler) | 109 | mux.Handle("/", wsHandler) |
| 71 | 110 | ||
| 72 | grpcLis, err := net.Listen("tcp", *grpcAddr) | 111 | grpcLis, err := net.Listen("tcp", cfg.Server.GrpcAddr) |
| 73 | if err != nil { | 112 | if err != nil { |
| 74 | log.Fatalf("failed to listen on gRPC port: %v", err) | 113 | log.Fatalf("failed to listen on gRPC port: %v", err) |
| 75 | } | 114 | } |
| 76 | 115 | ||
| 77 | grpcServer := grpc.NewServer() | 116 | grpcServer := grpc.NewServer(serverOpts...) |
| 78 | pb.RegisterNostrRelayServer(grpcServer, grpcHandler) | 117 | pb.RegisterNostrRelayServer(grpcServer, grpcHandler) |
| 79 | 118 | ||
| 80 | httpServer := &http.Server{ | 119 | httpServer := &http.Server{ |
| 81 | Addr: *wsAddr, | 120 | Addr: cfg.Server.HttpAddr, |
| 82 | Handler: h2c.NewHandler(mux, &http2.Server{}), | 121 | Handler: h2c.NewHandler(mux, &http2.Server{}), |
| 122 | ReadTimeout: cfg.Server.ReadTimeout, | ||
| 123 | WriteTimeout: cfg.Server.WriteTimeout, | ||
| 83 | } | 124 | } |
| 84 | 125 | ||
| 85 | log.Printf("gRPC server listening on %s", *grpcAddr) | 126 | log.Printf("gRPC server listening on %s", cfg.Server.GrpcAddr) |
| 86 | log.Printf("HTTP server listening on %s", *wsAddr) | 127 | log.Printf("HTTP server listening on %s", cfg.Server.HttpAddr) |
| 87 | log.Printf(" - Connect (gRPC-Web) at %s/nostr.v1.NostrRelay/*", *wsAddr) | 128 | log.Printf(" - Connect (gRPC-Web) at %s/nostr.v1.NostrRelay/*", cfg.Server.HttpAddr) |
| 88 | log.Printf(" - WebSocket (Nostr) at %s/", *wsAddr) | 129 | log.Printf(" - WebSocket (Nostr) at %s/", cfg.Server.HttpAddr) |
| 89 | log.Printf("Database: %s", *dbPath) | 130 | log.Printf("Database: %s", cfg.Database.Path) |
| 131 | if cfg.Auth.Read.Enabled || cfg.Auth.Write.Enabled { | ||
| 132 | log.Printf("Auth: enabled (read=%v write=%v)", cfg.Auth.Read.Enabled, cfg.Auth.Write.Enabled) | ||
| 133 | } | ||
| 134 | if cfg.RateLimit.Enabled { | ||
| 135 | log.Printf("Rate limiting: enabled") | ||
| 136 | } | ||
| 90 | 137 | ||
| 91 | sigChan := make(chan os.Signal, 1) | 138 | sigChan := make(chan os.Signal, 1) |
| 92 | signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) | 139 | signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) |
