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:
142
modules/lighthouse/route.go
Normal file
142
modules/lighthouse/route.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package lighthouse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
// Surface says who calls a driver route, so the application can put it
|
||||
// behind the right guard and group.
|
||||
type Surface int
|
||||
|
||||
const (
|
||||
// UserAuth routes are called by a signed-in browser user; they mount
|
||||
// behind the application's user guard.
|
||||
UserAuth Surface = iota + 1
|
||||
// ServerToServer routes are called by the realtime server itself; they
|
||||
// mount in a raw group without the house envelope.
|
||||
ServerToServer
|
||||
// Public routes need no authentication.
|
||||
Public
|
||||
)
|
||||
|
||||
// String returns the surface name.
|
||||
func (s Surface) String() string {
|
||||
switch s {
|
||||
case UserAuth:
|
||||
return "UserAuth"
|
||||
case ServerToServer:
|
||||
return "ServerToServer"
|
||||
case Public:
|
||||
return "Public"
|
||||
default:
|
||||
return fmt.Sprintf("Surface(%d)", int(s))
|
||||
}
|
||||
}
|
||||
|
||||
// Route is an HTTP endpoint declared by a driver.
|
||||
type Route struct {
|
||||
// Name identifies the route inside the driver (for example "token").
|
||||
Name string
|
||||
// Method is GET, POST, PUT, PATCH or DELETE.
|
||||
Method string
|
||||
// Path is the absolute request path.
|
||||
Path string
|
||||
Surface Surface
|
||||
Handler http.HandlerFunc
|
||||
}
|
||||
|
||||
// Surfaces holds the application's middleware for each surface. Middleware
|
||||
// is appended after the surface middleware on every route, so a guard in
|
||||
// UserAuth runs before a throttle in Middleware.
|
||||
type Surfaces struct {
|
||||
UserAuth []string
|
||||
ServerToServer []string
|
||||
Public []string
|
||||
Middleware []string
|
||||
}
|
||||
|
||||
// Mount registers the driver's routes on r. UserAuth and Public routes go
|
||||
// through r.Group and ServerToServer routes through r.GroupRaw, each with
|
||||
// the surface middleware followed by s.Middleware. A nil driver, or one
|
||||
// without routes, mounts nothing. A UserAuth route with an empty
|
||||
// s.UserAuth is an error: the application must never expose a user route
|
||||
// without a guard. Every route is validated before any is registered.
|
||||
func Mount(r pact.Router, d Driver, s Surfaces) error {
|
||||
if r == nil {
|
||||
return fmt.Errorf("lighthouse: router is nil")
|
||||
}
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
routes := d.Routes()
|
||||
for _, rt := range routes {
|
||||
if err := validateRoute(d, rt, s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, rt := range routes {
|
||||
mw := append(append([]string{}, surfaceMiddleware(rt.Surface, s)...), s.Middleware...)
|
||||
add := func(g pact.Router) { addRoute(g, rt) }
|
||||
if rt.Surface == ServerToServer {
|
||||
r.GroupRaw("/", mw, add)
|
||||
} else {
|
||||
r.Group("/", mw, add)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRoute(d Driver, rt Route, s Surfaces) error {
|
||||
where := fmt.Sprintf("lighthouse: driver %s route %q", d.Name(), rt.Name)
|
||||
if rt.Handler == nil {
|
||||
return fmt.Errorf("%s has no handler", where)
|
||||
}
|
||||
if !strings.HasPrefix(rt.Path, "/") {
|
||||
return fmt.Errorf("%s path %q must be absolute", where, rt.Path)
|
||||
}
|
||||
switch strings.ToUpper(rt.Method) {
|
||||
case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
|
||||
default:
|
||||
return fmt.Errorf("%s has unsupported method %q", where, rt.Method)
|
||||
}
|
||||
switch rt.Surface {
|
||||
case UserAuth:
|
||||
if len(s.UserAuth) == 0 {
|
||||
return fmt.Errorf("%s is a UserAuth route but Surfaces.UserAuth is empty; mount it behind a user guard", where)
|
||||
}
|
||||
case ServerToServer, Public:
|
||||
default:
|
||||
return fmt.Errorf("%s has unknown surface %v", where, rt.Surface)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func surfaceMiddleware(surface Surface, s Surfaces) []string {
|
||||
switch surface {
|
||||
case UserAuth:
|
||||
return s.UserAuth
|
||||
case ServerToServer:
|
||||
return s.ServerToServer
|
||||
default:
|
||||
return s.Public
|
||||
}
|
||||
}
|
||||
|
||||
func addRoute(g pact.Router, rt Route) {
|
||||
switch strings.ToUpper(rt.Method) {
|
||||
case http.MethodGet:
|
||||
g.Get(rt.Path, rt.Handler)
|
||||
case http.MethodPost:
|
||||
g.Post(rt.Path, rt.Handler)
|
||||
case http.MethodPut:
|
||||
g.Put(rt.Path, rt.Handler)
|
||||
case http.MethodPatch:
|
||||
g.Patch(rt.Path, rt.Handler)
|
||||
case http.MethodDelete:
|
||||
g.Delete(rt.Path, rt.Handler)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user