- lighthouse: Registry of namespace authorizers (Result, Allowed, Denied), ParseChannel, ChannelID with PHP (int)-cast semantics (PHPInt, pinned by a php -r table test), FormatChannels, WithClientID/ClientID - centrifugo: ProxyHandler (constant-time X-Centrifugo-Secret, HTTP 200 generic deny, info [] on allow, presence allow/override merge, 64 KiB body cap) mounted as the ServerToServer subscribe route - README: proxy contract, registry and channel rules
112 lines
3.2 KiB
Go
112 lines
3.2 KiB
Go
package lighthouse
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Result is an authorizer's subscribe decision. Info, Capabilities and
|
|
// Overrides are passed to the realtime server on an allow; nil means "use
|
|
// the driver default". The deny reason is for logs only and never reaches
|
|
// the client.
|
|
type Result struct {
|
|
Allowed bool
|
|
Info map[string]any
|
|
Capabilities []string
|
|
Overrides map[string]any
|
|
reason string
|
|
}
|
|
|
|
// Reason returns the internal deny reason ("" for an allow).
|
|
func (r Result) Reason() string { return r.reason }
|
|
|
|
// Allowed returns an allow with info (nil for none).
|
|
func Allowed(info map[string]any) Result {
|
|
return Result{Allowed: true, Info: info}
|
|
}
|
|
|
|
// Denied returns a deny with an internal reason for the logs.
|
|
func Denied(reason string) Result {
|
|
return Result{reason: reason}
|
|
}
|
|
|
|
// Authorizer decides whether userID may subscribe to channel. It is called
|
|
// on every subscribe with the full original channel, including any
|
|
// "presence:" prefix, and must check current state (no caching). The
|
|
// driver's client id is available through ClientID(ctx).
|
|
type Authorizer interface {
|
|
Authorize(ctx context.Context, userID uint, channel string) Result
|
|
}
|
|
|
|
// AuthorizerFunc adapts a function to Authorizer.
|
|
type AuthorizerFunc func(ctx context.Context, userID uint, channel string) Result
|
|
|
|
// Authorize calls f.
|
|
func (f AuthorizerFunc) Authorize(ctx context.Context, userID uint, channel string) Result {
|
|
return f(ctx, userID, channel)
|
|
}
|
|
|
|
// Registry maps channel namespaces to authorizers. It is safe for
|
|
// concurrent use.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
authorizers map[string]Authorizer
|
|
}
|
|
|
|
// NewRegistry returns an empty registry.
|
|
func NewRegistry() *Registry {
|
|
return &Registry{authorizers: map[string]Authorizer{}}
|
|
}
|
|
|
|
// Register adds the authorizer of namespace. An empty namespace, one that
|
|
// contains ":", a nil authorizer, or a namespace registered twice is an
|
|
// error (unlike the WinterCMS registry, a second registration does not
|
|
// silently replace the first).
|
|
func (r *Registry) Register(namespace string, a Authorizer) error {
|
|
if namespace == "" {
|
|
return fmt.Errorf("lighthouse: authorizer namespace is empty")
|
|
}
|
|
if strings.Contains(namespace, ":") {
|
|
return fmt.Errorf("lighthouse: authorizer namespace %q contains \":\"", namespace)
|
|
}
|
|
if a == nil {
|
|
return fmt.Errorf("lighthouse: authorizer for namespace %q is nil", namespace)
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, dup := r.authorizers[namespace]; dup {
|
|
return fmt.Errorf("lighthouse: authorizer namespace %q is already registered", namespace)
|
|
}
|
|
r.authorizers[namespace] = a
|
|
return nil
|
|
}
|
|
|
|
// Get returns the authorizer of namespace. The lookup is byte-exact.
|
|
func (r *Registry) Get(namespace string) (Authorizer, bool) {
|
|
if r == nil {
|
|
return nil, false
|
|
}
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
a, ok := r.authorizers[namespace]
|
|
return a, ok
|
|
}
|
|
|
|
// Namespaces returns the registered namespaces, sorted.
|
|
func (r *Registry) Namespaces() []string {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]string, 0, len(r.authorizers))
|
|
for ns := range r.authorizers {
|
|
out = append(out, ns)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|