feat(11-03): add the lighthouse realtime package and its Centrifugo driver
- 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
This commit is contained in:
151
modules/lighthouse/README.md
Normal file
151
modules/lighthouse/README.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# 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 are `null` (the default; it discards everything), `log` (logs channel names and the event, never the payload) and `memory` (`lighthouse.MemoryDriver` records every `lighthouse.Publication` for tests). An unknown name is a boot error that lists the registered drivers.
|
||||
- Third-party drivers: `lighthouse.RegisterDriver` with a `lighthouse.DriverFactory`. A duplicate name panics at init.
|
||||
- The `lighthouse.Publisher` interface (`Publish` for one channel, `Broadcast` for several) and the `lighthouse.Driver` interface, which adds `Name` and `Routes`.
|
||||
- Route mounting by surface: `lighthouse.UserAuth`, `lighthouse.ServerToServer` and `lighthouse.Public`. `lighthouse.Mount` puts user and public routes in `Group` and server-to-server routes in `GroupRaw`, each with the surface middleware followed by `lighthouse.Surfaces.Middleware`. A `lighthouse.UserAuth` route 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.UserLookup` with `lighthouse.Service.SetUserLookup`. `lighthouse.Service.User` loads a `lighthouse.User` (id and display name). `lighthouse.Service.Actor` returns the `lighthouse.Actor` of a request, and `lighthouse.SystemActor` when there is no signed-in user or the principal is a backend admin.
|
||||
- Centrifugo driver (`centrifugo.Driver`, driver name `centrifugo`):
|
||||
- `centrifugo.Client` POSTs `publish`, `broadcast`, `presence` and `unsubscribe` calls with `Authorization: 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 returns `centrifugo.ErrNotConfigured`. The key never appears in logs or errors.
|
||||
- `centrifugo.TokenIssuer` signs HS256 tokens with five generators, the same as the WinterCMS generator: `ForUser` (claims `sub`, `exp`, `info` with only `name`), `Subscription`, `Anonymous` (`sub` "" and a 5-minute lifetime), `ForIdentifier` (an empty `info` is encoded as `[]`) and `SubscriptionForIdentifier`. It refuses to sign with an empty secret.
|
||||
- `centrifugo.TokenHandler` serves 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 sends `Cache-Control: no-cache, private` and no trailing newline.
|
||||
|
||||
## Usage
|
||||
|
||||
An application selects the driver in `config/realtime.yaml`:
|
||||
|
||||
```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:
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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`, `pact` and `wire` from this repository.
|
||||
- `github.com/golang-jwt/jwt/v5` (centrifugo token signing).
|
||||
- The Centrifugo client is plain `net/http`; no Centrifugo SDK is used.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
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.
|
||||
Reference in New Issue
Block a user