diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/auth/README.md | 4 | ||||
| -rw-r--r-- | internal/auth/auth_test.go | 4 | ||||
| -rw-r--r-- | internal/auth/interceptor.go | 15 | ||||
| -rw-r--r-- | internal/config/config.go | 75 | ||||
| -rw-r--r-- | internal/metrics/metrics.go | 9 |
5 files changed, 80 insertions, 27 deletions
diff --git a/internal/auth/README.md b/internal/auth/README.md index de37010..98d1437 100644 --- a/internal/auth/README.md +++ b/internal/auth/README.md | |||
| @@ -142,11 +142,11 @@ import ( | |||
| 142 | 142 | ||
| 143 | // Create auth options | 143 | // Create auth options |
| 144 | authOpts := &auth.InterceptorOptions{ | 144 | authOpts := &auth.InterceptorOptions{ |
| 145 | Read: auth.OperationAuthConfig{ | 145 | Read: auth.AuthOperationConfig{ |
| 146 | Enabled: true, // Require auth for reads | 146 | Enabled: true, // Require auth for reads |
| 147 | AllowedNpubs: nil, // Accept any valid signature | 147 | AllowedNpubs: nil, // Accept any valid signature |
| 148 | }, | 148 | }, |
| 149 | Write: auth.OperationAuthConfig{ | 149 | Write: auth.AuthOperationConfig{ |
| 150 | Enabled: true, | 150 | Enabled: true, |
| 151 | AllowedNpubs: []string{"hex-pubkey-1", "hex-pubkey-2"}, // Whitelist | 151 | AllowedNpubs: []string{"hex-pubkey-1", "hex-pubkey-2"}, // Whitelist |
| 152 | }, | 152 | }, |
diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 7b0fa13..68c68f5 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go | |||
| @@ -243,11 +243,11 @@ func TestValidateAuthFromContext(t *testing.T) { | |||
| 243 | ctx := metadata.NewIncomingContext(context.Background(), md) | 243 | ctx := metadata.NewIncomingContext(context.Background(), md) |
| 244 | 244 | ||
| 245 | opts := &InterceptorOptions{ | 245 | opts := &InterceptorOptions{ |
| 246 | Read: OperationAuthConfig{ | 246 | Read: AuthOperationConfig{ |
| 247 | Enabled: true, | 247 | Enabled: true, |
| 248 | AllowedNpubs: nil, | 248 | AllowedNpubs: nil, |
| 249 | }, | 249 | }, |
| 250 | Write: OperationAuthConfig{ | 250 | Write: AuthOperationConfig{ |
| 251 | Enabled: true, | 251 | Enabled: true, |
| 252 | AllowedNpubs: nil, | 252 | AllowedNpubs: nil, |
| 253 | }, | 253 | }, |
diff --git a/internal/auth/interceptor.go b/internal/auth/interceptor.go index 42c2688..67450ce 100644 --- a/internal/auth/interceptor.go +++ b/internal/auth/interceptor.go | |||
| @@ -18,28 +18,25 @@ const ( | |||
| 18 | ) | 18 | ) |
| 19 | 19 | ||
| 20 | type InterceptorOptions struct { | 20 | type InterceptorOptions struct { |
| 21 | Read OperationAuthConfig | 21 | Read AuthOperationConfig |
| 22 | Write OperationAuthConfig | 22 | Write AuthOperationConfig |
| 23 | TimestampWindow int64 | 23 | TimestampWindow int64 |
| 24 | ValidatePayload bool | 24 | ValidatePayload bool |
| 25 | SkipMethods []string | 25 | SkipMethods []string |
| 26 | } | 26 | } |
| 27 | 27 | ||
| 28 | // OperationAuthConfig configures auth for read or write operations. | 28 | type AuthOperationConfig struct { |
| 29 | // Three states: disabled (allow all), enabled with empty list (require auth), | ||
| 30 | // enabled with npubs (whitelist only). Npubs normalized to hex at load time. | ||
| 31 | type OperationAuthConfig struct { | ||
| 32 | Enabled bool | 29 | Enabled bool |
| 33 | AllowedNpubs []string | 30 | AllowedNpubs []string |
| 34 | } | 31 | } |
| 35 | 32 | ||
| 36 | func DefaultInterceptorOptions() *InterceptorOptions { | 33 | func DefaultInterceptorOptions() *InterceptorOptions { |
| 37 | return &InterceptorOptions{ | 34 | return &InterceptorOptions{ |
| 38 | Read: OperationAuthConfig{ | 35 | Read: AuthOperationConfig{ |
| 39 | Enabled: false, | 36 | Enabled: false, |
| 40 | AllowedNpubs: nil, | 37 | AllowedNpubs: nil, |
| 41 | }, | 38 | }, |
| 42 | Write: OperationAuthConfig{ | 39 | Write: AuthOperationConfig{ |
| 43 | Enabled: false, | 40 | Enabled: false, |
| 44 | AllowedNpubs: nil, | 41 | AllowedNpubs: nil, |
| 45 | }, | 42 | }, |
| @@ -154,7 +151,7 @@ func validateAuthFromContext(ctx context.Context, method string, opts *Intercept | |||
| 154 | 151 | ||
| 155 | pubkey := ExtractPubkey(event) | 152 | pubkey := ExtractPubkey(event) |
| 156 | 153 | ||
| 157 | var opConfig OperationAuthConfig | 154 | var opConfig AuthOperationConfig |
| 158 | if isWriteMethod(method) { | 155 | if isWriteMethod(method) { |
| 159 | opConfig = opts.Write | 156 | opConfig = opts.Write |
| 160 | } else { | 157 | } else { |
diff --git a/internal/config/config.go b/internal/config/config.go index 36c8be5..dcceade 100644 --- a/internal/config/config.go +++ b/internal/config/config.go | |||
| @@ -7,6 +7,9 @@ import ( | |||
| 7 | "time" | 7 | "time" |
| 8 | 8 | ||
| 9 | "gopkg.in/yaml.v3" | 9 | "gopkg.in/yaml.v3" |
| 10 | "northwest.io/muxstr/internal/auth" | ||
| 11 | "northwest.io/muxstr/internal/metrics" | ||
| 12 | "northwest.io/muxstr/internal/ratelimit" | ||
| 10 | "northwest.io/nostr" | 13 | "northwest.io/nostr" |
| 11 | ) | 14 | ) |
| 12 | 15 | ||
| @@ -33,18 +36,10 @@ type DatabaseConfig struct { | |||
| 33 | } | 36 | } |
| 34 | 37 | ||
| 35 | type AuthConfig struct { | 38 | type AuthConfig struct { |
| 36 | Read AuthOperationConfig `yaml:"read"` | 39 | Read auth.AuthOperationConfig `yaml:"read"` |
| 37 | Write AuthOperationConfig `yaml:"write"` | 40 | Write auth.AuthOperationConfig `yaml:"write"` |
| 38 | TimestampWindow int64 `yaml:"timestamp_window"` | 41 | TimestampWindow int64 `yaml:"timestamp_window"` |
| 39 | SkipMethods []string `yaml:"skip_methods"` | 42 | SkipMethods []string `yaml:"skip_methods"` |
| 40 | } | ||
| 41 | |||
| 42 | // AuthOperationConfig configures auth for read or write operations. | ||
| 43 | // Three states: disabled (allow all), enabled with empty list (require auth), | ||
| 44 | // enabled with npubs (whitelist only). Npubs normalized to hex at load time. | ||
| 45 | type AuthOperationConfig struct { | ||
| 46 | Enabled bool `yaml:"enabled"` | ||
| 47 | AllowedNpubs []string `yaml:"allowed_npubs"` | ||
| 48 | } | 43 | } |
| 49 | 44 | ||
| 50 | type RateLimitConfig struct { | 45 | type RateLimitConfig struct { |
| @@ -105,11 +100,11 @@ func Default() *Config { | |||
| 105 | Path: "relay.db", | 100 | Path: "relay.db", |
| 106 | }, | 101 | }, |
| 107 | Auth: AuthConfig{ | 102 | Auth: AuthConfig{ |
| 108 | Read: AuthOperationConfig{ | 103 | Read: auth.AuthOperationConfig{ |
| 109 | Enabled: false, | 104 | Enabled: false, |
| 110 | AllowedNpubs: nil, | 105 | AllowedNpubs: nil, |
| 111 | }, | 106 | }, |
| 112 | Write: AuthOperationConfig{ | 107 | Write: auth.AuthOperationConfig{ |
| 113 | Enabled: false, | 108 | Enabled: false, |
| 114 | AllowedNpubs: nil, | 109 | AllowedNpubs: nil, |
| 115 | }, | 110 | }, |
| @@ -349,3 +344,55 @@ func (c *Config) Save(filename string) error { | |||
| 349 | 344 | ||
| 350 | return nil | 345 | return nil |
| 351 | } | 346 | } |
| 347 | |||
| 348 | func (r *RateLimitConfig) ToRateLimiter() *ratelimit.Config { | ||
| 349 | rlConfig := &ratelimit.Config{ | ||
| 350 | RequestsPerSecond: r.DefaultRPS, | ||
| 351 | BurstSize: r.DefaultBurst, | ||
| 352 | IPRequestsPerSecond: r.IPRPS, | ||
| 353 | IPBurstSize: r.IPBurst, | ||
| 354 | SkipMethods: r.SkipMethods, | ||
| 355 | SkipUsers: r.SkipUsers, | ||
| 356 | CleanupInterval: r.CleanupInterval, | ||
| 357 | MaxIdleTime: r.MaxIdleTime, | ||
| 358 | } | ||
| 359 | |||
| 360 | if r.Methods != nil { | ||
| 361 | rlConfig.MethodLimits = make(map[string]ratelimit.MethodLimit, len(r.Methods)) | ||
| 362 | for method, limit := range r.Methods { | ||
| 363 | rlConfig.MethodLimits[method] = ratelimit.MethodLimit{ | ||
| 364 | RequestsPerSecond: limit.RPS, | ||
| 365 | BurstSize: limit.Burst, | ||
| 366 | } | ||
| 367 | } | ||
| 368 | } | ||
| 369 | |||
| 370 | if r.Users != nil { | ||
| 371 | rlConfig.UserLimits = make(map[string]ratelimit.UserLimit, len(r.Users)) | ||
| 372 | for user, limit := range r.Users { | ||
| 373 | userLimit := ratelimit.UserLimit{ | ||
| 374 | RequestsPerSecond: limit.RPS, | ||
| 375 | BurstSize: limit.Burst, | ||
| 376 | } | ||
| 377 | if limit.Methods != nil { | ||
| 378 | userLimit.MethodLimits = make(map[string]ratelimit.MethodLimit, len(limit.Methods)) | ||
| 379 | for method, methodLimit := range limit.Methods { | ||
| 380 | userLimit.MethodLimits[method] = ratelimit.MethodLimit{ | ||
| 381 | RequestsPerSecond: methodLimit.RPS, | ||
| 382 | BurstSize: methodLimit.Burst, | ||
| 383 | } | ||
| 384 | } | ||
| 385 | } | ||
| 386 | rlConfig.UserLimits[user] = userLimit | ||
| 387 | } | ||
| 388 | } | ||
| 389 | |||
| 390 | return rlConfig | ||
| 391 | } | ||
| 392 | |||
| 393 | func (m *MetricsConfig) ToMetrics() *metrics.Config { | ||
| 394 | return &metrics.Config{ | ||
| 395 | Namespace: m.Namespace, | ||
| 396 | Subsystem: m.Subsystem, | ||
| 397 | } | ||
| 398 | } | ||
diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 3cb675f..9030d67 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go | |||
| @@ -1,8 +1,11 @@ | |||
| 1 | package metrics | 1 | package metrics |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "net/http" | ||
| 5 | |||
| 4 | "github.com/prometheus/client_golang/prometheus" | 6 | "github.com/prometheus/client_golang/prometheus" |
| 5 | "github.com/prometheus/client_golang/prometheus/promauto" | 7 | "github.com/prometheus/client_golang/prometheus/promauto" |
| 8 | "github.com/prometheus/client_golang/prometheus/promhttp" | ||
| 6 | ) | 9 | ) |
| 7 | 10 | ||
| 8 | // Metrics holds all Prometheus metrics for the relay. | 11 | // Metrics holds all Prometheus metrics for the relay. |
| @@ -280,3 +283,9 @@ const ( | |||
| 280 | StatusRateLimited RequestStatus = "rate_limited" | 283 | StatusRateLimited RequestStatus = "rate_limited" |
| 281 | StatusInvalidRequest RequestStatus = "invalid_request" | 284 | StatusInvalidRequest RequestStatus = "invalid_request" |
| 282 | ) | 285 | ) |
| 286 | |||
| 287 | func (m *Metrics) Serve(addr, path string) error { | ||
| 288 | mux := http.NewServeMux() | ||
| 289 | mux.Handle(path, promhttp.Handler()) | ||
| 290 | return http.ListenAndServe(addr, mux) | ||
| 291 | } | ||
