- lighthouse: Service/From with realtime.driver selection, RegisterDriver registry, null/log/memory drivers, Route/Surface/Mount, users and actors - centrifugo: HTTP API client (apikey header, 2xx success, no request without a key), five-generator HS256 TokenIssuer, TokenHandler with the WinterCMS 401/503 bodies - module README and root modules table row
9.6 KiB
lighthouse
Transport-neutral realtime: a publisher interface with pluggable drivers, subscribe-time channel authorization, and model broadcasts enqueued in the write transaction.
import "git.golem15.com/golem15/summercms/modules/lighthouse"
import _ "git.golem15.com/golem15/summercms/modules/lighthouse/centrifugo"
Overview
lighthouse is the SummerCMS counterpart of the WinterCMS websockets plugin. The package itself knows no transport. It owns the interfaces application code writes against, and a driver package supplies the transport. A driver registers itself from its init function, the way database/sql drivers do, and the application picks one with realtime.driver.
lighthouse.From builds the app-scoped lighthouse.Service on first use and publishes it on the app. The service holds the selected driver, the application's user lookup, and the broadcast settings.
A driver may need HTTP endpoints, such as a token route for signed-in users or a callback the realtime server calls. It declares them as lighthouse.Route values, each tagged with a lighthouse.Surface. The application mounts them once with lighthouse.Mount and decides the guard, group and rate-limit bucket per surface. Switching drivers never edits the application's route file.
The centrifugo sub-package is the Centrifugo driver. It has a hand-rolled net/http client for the Centrifugo HTTP API, a token issuer with the claims of the WinterCMS JwtTokenGenerator, and the token route handler.
Features
- Driver selection by
realtime.driver. The built-in drivers arenull(the default; it discards everything),log(logs channel names and the event, never the payload) andmemory(lighthouse.MemoryDriverrecords everylighthouse.Publicationfor tests). An unknown name is a boot error that lists the registered drivers. - Third-party drivers:
lighthouse.RegisterDriverwith alighthouse.DriverFactory. A duplicate name panics at init. - The
lighthouse.Publisherinterface (Publishfor one channel,Broadcastfor several) and thelighthouse.Driverinterface, which addsNameandRoutes. - Route mounting by surface:
lighthouse.UserAuth,lighthouse.ServerToServerandlighthouse.Public.lighthouse.Mountputs user and public routes inGroupand server-to-server routes inGroupRaw, each with the surface middleware followed bylighthouse.Surfaces.Middleware. Alighthouse.UserAuthroute with no user middleware is refused, so a token route can never be mounted without a guard. Every route is validated before any is registered. - Users and actors: the application installs a
lighthouse.UserLookupwithlighthouse.Service.SetUserLookup.lighthouse.Service.Userloads alighthouse.User(id and display name).lighthouse.Service.Actorreturns thelighthouse.Actorof a request, andlighthouse.SystemActorwhen there is no signed-in user or the principal is a backend admin. - Centrifugo driver (
centrifugo.Driver, driver namecentrifugo):centrifugo.ClientPOSTspublish,broadcast,presenceandunsubscribecalls withAuthorization: apikey <key>and a 5 s timeout. The publish body is{"channel":…,"data":{"event":…,"payload":…,"timestamp":"…+00:00"}}, with an empty payload sent as[]. Any 2xx status counts as success. With an empty API key nothing is sent and the call returnscentrifugo.ErrNotConfigured. The key never appears in logs or errors.centrifugo.TokenIssuersigns HS256 tokens with five generators, the same as the WinterCMS generator:ForUser(claimssub,exp,infowith onlyname),Subscription,Anonymous(sub"" and a 5-minute lifetime),ForIdentifier(an emptyinfois encoded as[]) andSubscriptionForIdentifier. It refuses to sign with an empty secret.centrifugo.TokenHandlerserves the token route. It answers 401{"error":"Unauthorized"}when no user is signed in, 503{"error":"WebSocket not configured"}when the token secret is empty, and otherwise 200{"token":"…"}. It sendsCache-Control: no-cache, privateand no trailing newline.
Usage
An application selects the driver in config/realtime.yaml:
driver: centrifugo
centrifugo:
token_secret: "" # set with SUMMER_REALTIME__CENTRIFUGO__TOKEN_SECRET
A plugin imports the driver package for its side effect, builds the service at Boot and installs a user lookup:
package acme
import (
"context"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/lighthouse"
_ "git.golem15.com/golem15/summercms/modules/lighthouse/centrifugo"
)
func (p *Plugin) Boot(app *backpack.App) error {
svc, err := lighthouse.From(app)
if err != nil {
return err
}
p.realtime = svc
svc.SetUserLookup(func(ctx context.Context, id uint) (lighthouse.User, bool, error) {
return lookupAcmeUser(ctx, id) // the application's own user model
})
return nil
}
and mounts the driver's routes once:
func (p *Plugin) Routes(r pact.Router) error {
return lighthouse.Mount(r, p.realtime.Driver(), lighthouse.Surfaces{
UserAuth: surf.Use("jwt.auth"),
ServerToServer: surf.Use(),
Middleware: surf.Use("throttle:acme-realtime"),
})
}
Tests select the memory driver and read what was published:
mem := svc.Driver().(*lighthouse.MemoryDriver)
for _, pub := range mem.Publications() {
fmt.Println(pub.Method, pub.Channels, pub.Event)
}
API reference
lighthouse
| Identifier | Description |
|---|---|
lighthouse.From(app) |
The app's *lighthouse.Service, built and published on first use. |
lighthouse.Service |
The realtime service: Driver, Logger, Namespace, Queue, Timeout, SetUserLookup, User, Actor. |
lighthouse.Publisher |
Publish(ctx, channel, event, payload) and Broadcast(ctx, channels, event, payload). |
lighthouse.Driver |
lighthouse.Publisher plus Name() and Routes(). |
lighthouse.DriverFactory |
func(app, svc) (lighthouse.Driver, error). |
lighthouse.RegisterDriver(name, factory) |
Registers a driver from an init function. |
lighthouse.MemoryDriver, lighthouse.NewMemoryDriver, lighthouse.Publication |
The recording driver and its records (Method, Channels, Event, Payload, Timestamp). |
lighthouse.Route |
Name, Method, Path, Surface, Handler. |
lighthouse.Surface, lighthouse.UserAuth, lighthouse.ServerToServer, lighthouse.Public |
Who calls a route. |
lighthouse.Surfaces |
Application middleware per surface plus Middleware for every route. |
lighthouse.Mount(r, driver, surfaces) |
Registers a driver's routes. |
lighthouse.User, lighthouse.UserLookup |
A user id with a display name, and the application's lookup. |
lighthouse.Actor, lighthouse.SystemActor |
Who caused a broadcast: {"user_id":…,"name":…}. |
lighthouse.DurationSetting(cfg, path) |
Reads a duration string or an integer number of seconds. |
lighthouse.DefaultDriver, lighthouse.DefaultQueue, lighthouse.DefaultTimeout |
Defaults of realtime.driver, realtime.broadcast_queue and realtime.broadcast_timeout. |
lighthouse/centrifugo
| Identifier | Description |
|---|---|
centrifugo.Config, centrifugo.LoadConfig |
The realtime.centrifugo.* settings with their defaults. |
centrifugo.Client, centrifugo.NewClient |
HTTP API client: Publish, Broadcast, Presence, Unsubscribe, Enabled, DebugInfo. |
centrifugo.DebugInfo |
api_url, enabled, api_key_set. |
centrifugo.TokenIssuer, centrifugo.NewTokenIssuer |
HS256 token generators: ForUser, Subscription, Anonymous, ForIdentifier, SubscriptionForIdentifier, Configured. |
centrifugo.TokenHandler(svc, issuer) |
The token route handler. |
centrifugo.Driver, centrifugo.NewDriver, centrifugo.DriverName |
The lighthouse.Driver, with Client, Issuer and Config accessors. |
centrifugo.ErrNotConfigured |
Returned when the API key or token secret an operation needs is empty. |
Configuration
| Key | Default | Description |
|---|---|---|
realtime.driver |
null |
null, log, memory, or a registered driver such as centrifugo. |
realtime.broadcast_namespace |
"" |
Prefix applied to broadcast channel names. |
realtime.broadcast_queue |
broadcasts |
River queue of broadcast jobs. |
realtime.broadcast_timeout |
5 |
Broadcast job timeout, in seconds or as a duration string. |
realtime.centrifugo.api_url |
http://127.0.0.1:8001/api |
Centrifugo HTTP API base. |
realtime.centrifugo.api_key |
"" |
HTTP API key; empty disables publishing. |
realtime.centrifugo.token_secret |
"" |
HS256 token secret; empty makes the token route answer 503. |
realtime.centrifugo.token_ttl |
3600 |
Token lifetime, in seconds or as a duration string. |
realtime.centrifugo.ws_url |
/ws |
WebSocket URL of the Centrifugo server. |
realtime.centrifugo.proxy_secret |
"" |
Expected X-Centrifugo-Secret of subscribe proxy calls. |
realtime.centrifugo.token_path |
/api/realtime/token |
Path of the token route. |
realtime.centrifugo.subscribe_path |
/api/realtime/subscribe |
Path of the subscribe proxy route. |
Dependencies
backpack,bouncer,compass,pactandwirefrom this repository.github.com/golang-jwt/jwt/v5(centrifugo token signing).- The Centrifugo client is plain
net/http; no Centrifugo SDK is used.
Testing
go test ./modules/lighthouse/...
A test selects realtime.driver: memory and reads lighthouse.MemoryDriver.Publications, or points realtime.centrifugo.api_url at an httptest server to see the exact Centrifugo requests.