mirror of
https://github.com/livekit/livekit.git
synced 2026-09-15 23:56:10 +00:00
Front is an http.Handler, so fallback, identity and singleKeyFallback were read on every request with no synchronization while WithFallback, WithIdentity and WithSingleKeyFallback wrote them on the live object. Each returned the same pointer, so the chain read as though it built a value. Every call site happens to run before the handler is mounted, so nothing races today, but nothing in the type prevents it, and WithSingleKeyFallback took no argument and could only be turned on. NewFront now takes FrontParams and configuration is fixed at construction. NewWorkerRegisterer takes its EndpointSettingsFunc directly, and HandshakeAgentWorker takes one in place of a variadic of raw closures that existed only to reach the setter it replaced. Registration gains NewRegistration and RegistrationParams, moving the Draining callback off an exported mutable field and folding SetSession into construction. IsDraining absorbs the nil check at both call sites. Access carried a three-state ladder as two bools, with "granted implies credentialed" documented but unenforced. It is now an ordered AccessLevel, so callers compare a rank rather than combining flags and the invariant holds by construction. Registry.Register returned an error that was always nil, with a dead branch at each call site. Both registry maps and the per-registration session are read-heavy, so they take RWMutex. CopyBody returned two errors to separate a source failure from a destination one. It returns one, wrapping a source failure in *SourceError, which is what the caller discriminates on. NewWebTransportServer took a callback to break the handler/server init cycle; the caller assigns wt.H3.Handler after construction instead. StartWebTransport reads Development off the service rather than taking a bool, and returns a nil stop function where it starts no listener. Smaller: slices.Sort for sort.Strings, for range for an unused counter, a nil slice for regs[:0:0], p2c generic over its slice so the call site passes a method expression rather than allocating a closure per request, and streamCode deduplicated into wire.StreamCode so both peers map reset codes in one place. Comments on the touched declarations drop remote behavior, migration narration and contrastive framing, keeping the constraints and invariants. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
266 lines
6.7 KiB
Go
266 lines
6.7 KiB
Go
// Copyright 2023 LiveKit, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/twitchtv/twirp"
|
|
|
|
"github.com/livekit/protocol/auth"
|
|
"github.com/livekit/protocol/livekit"
|
|
)
|
|
|
|
const (
|
|
authorizationHeader = "Authorization"
|
|
bearerPrefix = "Bearer "
|
|
accessTokenParam = "access_token"
|
|
)
|
|
|
|
type grantsKey struct{}
|
|
|
|
type grantsValue struct {
|
|
claims *auth.ClaimGrants
|
|
apiKey string
|
|
expiresAt time.Time
|
|
}
|
|
|
|
var (
|
|
ErrPermissionDenied = errors.New("permissions denied")
|
|
ErrMissingAuthorization = errors.New("invalid authorization header. Must start with " + bearerPrefix)
|
|
ErrInvalidAuthorizationToken = errors.New("invalid authorization token")
|
|
ErrInvalidAPIKey = errors.New("invalid API key")
|
|
)
|
|
|
|
// authentication middleware
|
|
type APIKeyAuthMiddleware struct {
|
|
provider auth.KeyProvider
|
|
}
|
|
|
|
func NewAPIKeyAuthMiddleware(provider auth.KeyProvider) *APIKeyAuthMiddleware {
|
|
return &APIKeyAuthMiddleware{
|
|
provider: provider,
|
|
}
|
|
}
|
|
|
|
func (m *APIKeyAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
|
if r.URL != nil && (r.URL.Path == "/rtc/validate" || r.URL.Path == "/rtc/v1/validate") {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
}
|
|
|
|
authHeader := r.Header.Get(authorizationHeader)
|
|
var authToken string
|
|
|
|
if authHeader != "" {
|
|
if !strings.HasPrefix(authHeader, bearerPrefix) {
|
|
HandleError(w, r, http.StatusUnauthorized, ErrMissingAuthorization)
|
|
return
|
|
}
|
|
|
|
authToken = authHeader[len(bearerPrefix):]
|
|
} else {
|
|
// the body must survive for requests proxied further (agent HTTP
|
|
// endpoints), so the token comes from the query string alone. URL is nil
|
|
// on hand-built requests.
|
|
if r.URL != nil {
|
|
authToken = r.URL.Query().Get(accessTokenParam)
|
|
}
|
|
}
|
|
|
|
if authToken != "" {
|
|
v, err := auth.ParseAPIToken(authToken)
|
|
if err != nil {
|
|
HandleError(w, r, http.StatusUnauthorized, ErrInvalidAuthorizationToken)
|
|
return
|
|
}
|
|
|
|
secret := m.provider.GetSecret(v.APIKey())
|
|
if secret == "" {
|
|
HandleError(w, r, http.StatusUnauthorized, ErrInvalidAPIKey, "apiKey", v.APIKey())
|
|
return
|
|
}
|
|
|
|
claims, grants, err := v.Verify(secret)
|
|
if err != nil {
|
|
HandleError(w, r, http.StatusUnauthorized, fmt.Errorf("%w: %s", ErrInvalidAuthorizationToken, err.Error()))
|
|
return
|
|
}
|
|
|
|
var expiresAt time.Time
|
|
if claims != nil && claims.ExpiresAt != nil {
|
|
expiresAt = claims.ExpiresAt.Time
|
|
}
|
|
|
|
// set grants in context
|
|
ctx := r.Context()
|
|
r = r.WithContext(context.WithValue(ctx, grantsKey{}, &grantsValue{
|
|
claims: grants,
|
|
apiKey: v.APIKey(),
|
|
expiresAt: expiresAt,
|
|
}))
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
}
|
|
|
|
func WithAPIKey(ctx context.Context, grants *auth.ClaimGrants, apiKey string) context.Context {
|
|
return context.WithValue(ctx, grantsKey{}, &grantsValue{
|
|
claims: grants,
|
|
apiKey: apiKey,
|
|
})
|
|
}
|
|
|
|
func GetGrants(ctx context.Context) *auth.ClaimGrants {
|
|
val := ctx.Value(grantsKey{})
|
|
v, ok := val.(*grantsValue)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return v.claims
|
|
}
|
|
|
|
func GetTokenExpiresAt(ctx context.Context) time.Time {
|
|
val := ctx.Value(grantsKey{})
|
|
v, ok := val.(*grantsValue)
|
|
if !ok {
|
|
return time.Time{}
|
|
}
|
|
return v.expiresAt
|
|
}
|
|
|
|
func GetAPIKey(ctx context.Context) string {
|
|
val := ctx.Value(grantsKey{})
|
|
v, ok := val.(*grantsValue)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return v.apiKey
|
|
}
|
|
|
|
func WithGrants(ctx context.Context, grants *auth.ClaimGrants, apiKey string) context.Context {
|
|
return WithGrantsExpiry(ctx, grants, apiKey, time.Time{})
|
|
}
|
|
|
|
func WithGrantsExpiry(ctx context.Context, grants *auth.ClaimGrants, apiKey string, expiresAt time.Time) context.Context {
|
|
return context.WithValue(ctx, grantsKey{}, &grantsValue{
|
|
claims: grants,
|
|
apiKey: apiKey,
|
|
expiresAt: expiresAt,
|
|
})
|
|
}
|
|
|
|
func SetAuthorizationToken(r *http.Request, token string) {
|
|
r.Header.Set(authorizationHeader, bearerPrefix+token)
|
|
}
|
|
|
|
func EnsureJoinPermission(ctx context.Context) (name livekit.RoomName, err error) {
|
|
claims := GetGrants(ctx)
|
|
if claims == nil || claims.Video == nil {
|
|
err = ErrPermissionDenied
|
|
return
|
|
}
|
|
|
|
if claims.Video.RoomJoin {
|
|
name = livekit.RoomName(claims.Video.Room)
|
|
} else {
|
|
err = ErrPermissionDenied
|
|
}
|
|
return
|
|
}
|
|
|
|
func EnsureAdminPermission(ctx context.Context, room livekit.RoomName) error {
|
|
claims := GetGrants(ctx)
|
|
if claims == nil || claims.Video == nil {
|
|
return ErrPermissionDenied
|
|
}
|
|
|
|
if !claims.Video.RoomAdmin || room != livekit.RoomName(claims.Video.Room) {
|
|
return ErrPermissionDenied
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func EnsureCreatePermission(ctx context.Context) error {
|
|
claims := GetGrants(ctx)
|
|
if claims == nil || claims.Video == nil || !claims.Video.RoomCreate {
|
|
return ErrPermissionDenied
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func EnsureListPermission(ctx context.Context) error {
|
|
claims := GetGrants(ctx)
|
|
if claims == nil || claims.Video == nil || !claims.Video.RoomList {
|
|
return ErrPermissionDenied
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func EnsureRecordPermission(ctx context.Context) error {
|
|
claims := GetGrants(ctx)
|
|
if claims == nil || claims.Video == nil || !claims.Video.RoomRecord {
|
|
return ErrPermissionDenied
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func EnsureIngressAdminPermission(ctx context.Context) error {
|
|
claims := GetGrants(ctx)
|
|
if claims == nil || claims.Video == nil || !claims.Video.IngressAdmin {
|
|
return ErrPermissionDenied
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func EnsureSIPAdminPermission(ctx context.Context) error {
|
|
claims := GetGrants(ctx)
|
|
if claims == nil || claims.SIP == nil || !claims.SIP.Admin {
|
|
return ErrPermissionDenied
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func EnsureSIPCallPermission(ctx context.Context) error {
|
|
claims := GetGrants(ctx)
|
|
if claims == nil || claims.SIP == nil || !claims.SIP.Call {
|
|
return ErrPermissionDenied
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func EnsureDestRoomPermission(ctx context.Context, source livekit.RoomName, destination livekit.RoomName) error {
|
|
claims := GetGrants(ctx)
|
|
if claims == nil || claims.Video == nil {
|
|
return ErrPermissionDenied
|
|
}
|
|
|
|
if !claims.Video.RoomAdmin || source != livekit.RoomName(claims.Video.Room) || destination != livekit.RoomName(claims.Video.DestinationRoom) {
|
|
return ErrPermissionDenied
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// wraps authentication errors around Twirp
|
|
func twirpAuthError(err error) error {
|
|
return twirp.NewError(twirp.Unauthenticated, err.Error())
|
|
}
|