mirror of
https://github.com/livekit/livekit.git
synced 2026-07-10 16:32:06 +00:00
147 lines
6.4 KiB
Go
147 lines
6.4 KiB
Go
// Copyright 2026 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 main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// headerMock carries the whole mock control config as a JSON object; see
|
|
// mockConfig and the README. Sent by the SDK on API calls (and forwarded onto
|
|
// the /settings/regions fetch and every failover retry).
|
|
headerMock = "X-Lk-Mock"
|
|
// headerRegion is set on responses to the index of the region that served it.
|
|
headerRegion = "X-Lk-Mock-Region"
|
|
)
|
|
|
|
// Deprecated: the individual X-Lk-Mock-* control headers predate the unified
|
|
// X-Lk-Mock JSON header. They are still honored for existing clients; new
|
|
// clients should send X-Lk-Mock instead. When X-Lk-Mock is present its fields
|
|
// take precedence over any legacy header.
|
|
const (
|
|
legacyHeaderFailRegions = "X-Lk-Mock-Fail-Regions"
|
|
legacyHeaderFailMode = "X-Lk-Mock-Fail-Mode"
|
|
legacyHeaderFailStatus = "X-Lk-Mock-Fail-Status"
|
|
legacyHeaderFailTwirpCode = "X-Lk-Mock-Fail-Twirp-Code"
|
|
legacyHeaderDelayMs = "X-Lk-Mock-Delay-Ms"
|
|
legacyHeaderRegionsStatus = "X-Lk-Mock-Regions-Status"
|
|
legacyHeaderResponse = "X-Lk-Mock-Response"
|
|
legacyHeaderSkipAuth = "X-Lk-Mock-Skip-Auth"
|
|
)
|
|
|
|
// legacyDefaultDelayMs is the sleep used by the deprecated "delay" fail mode when
|
|
// no X-Lk-Mock-Delay-Ms is given (long enough to trip client timeouts).
|
|
const legacyDefaultDelayMs = 30_000
|
|
|
|
// mockConfig is the JSON value of the X-Lk-Mock request header. Every field is
|
|
// optional; the zero value means "behave normally". A single object keeps the
|
|
// control protocol simple — the SDK serializes one struct instead of juggling a
|
|
// header per knob.
|
|
type mockConfig struct {
|
|
// FailRegions lists region indices that should fail this request. A listener
|
|
// fails only if its own region index appears here, so one config can make the
|
|
// primary fail while a fallback succeeds.
|
|
FailRegions []int `json:"failRegions,omitempty"`
|
|
// FailMode selects how a failing region fails: "status" (default) writes a
|
|
// Twirp error; "drop" closes the connection to force a transport error.
|
|
// ("delay" is a deprecated legacy mode; new clients use DelayMs instead.)
|
|
FailMode string `json:"failMode,omitempty"`
|
|
// FailStatus is the HTTP status for a "status"-mode failure (default 503).
|
|
FailStatus int `json:"failStatus,omitempty"`
|
|
// FailTwirpCode overrides the Twirp error code string in the failure body
|
|
// (default derived from FailStatus).
|
|
FailTwirpCode string `json:"failTwirpCode,omitempty"`
|
|
// DelayMs delays the response by this many milliseconds before returning,
|
|
// whether the region succeeds or fails. It overrides a method's natural
|
|
// latency (see methodLatency): set it high for timeout tests, or to 0 to skip
|
|
// a SIP method's built-in wait. Nil means "use the natural latency".
|
|
DelayMs *int `json:"delayMs,omitempty"`
|
|
// RegionsStatus overrides the HTTP status of GET /settings/regions (default 200).
|
|
RegionsStatus int `json:"regionsStatus,omitempty"`
|
|
// Response is the protojson of the response message for the called method; it
|
|
// replaces the populated default, giving full control over the payload.
|
|
Response json.RawMessage `json:"response,omitempty"`
|
|
// SkipAuth disables permission enforcement for this request (for tests that
|
|
// aren't about authz, e.g. failover tests with a placeholder token).
|
|
SkipAuth bool `json:"skipAuth,omitempty"`
|
|
// SIPStatus, when set on a SIP dial method (CreateSIPParticipant /
|
|
// TransferSIPParticipant), fails the call with this SIP status. The Twirp
|
|
// error code and metadata (sip_status_code, sip_status, error_details) are
|
|
// derived from it exactly as the real server does, so the SDK sees an
|
|
// identical error. Composes with DelayMs to simulate "ring, then fail".
|
|
SIPStatus *sipStatusConfig `json:"sipStatus,omitempty"`
|
|
|
|
// legacyDelayMs is the sleep used by the deprecated "delay" fail mode. It is
|
|
// populated only from the legacy X-Lk-Mock-Delay-Ms header, never from JSON.
|
|
legacyDelayMs int
|
|
}
|
|
|
|
// sipStatusConfig is a SIP response to inject; see mockConfig.SIPStatus.
|
|
type sipStatusConfig struct {
|
|
// Code is the SIP response code, e.g. 486 (Busy Here) or 603 (Decline).
|
|
Code int `json:"code"`
|
|
// Status is the SIP reason phrase; defaults to the code's canonical name.
|
|
Status string `json:"status,omitempty"`
|
|
}
|
|
|
|
// parseMockConfig builds the request's config. Deprecated individual X-Lk-Mock-*
|
|
// headers form the base; the unified X-Lk-Mock JSON header (if present) is
|
|
// overlaid on top, so its fields win per-field while absent fields keep the
|
|
// legacy value.
|
|
func parseMockConfig(r *http.Request) mockConfig {
|
|
cfg := parseLegacyConfig(r)
|
|
if v := r.Header.Get(headerMock); v != "" {
|
|
// Unmarshal overwrites only the fields present in the JSON; the unexported
|
|
// legacyDelayMs is untouched.
|
|
_ = json.Unmarshal([]byte(v), &cfg)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// parseLegacyConfig reads the deprecated per-setting headers into a config.
|
|
func parseLegacyConfig(r *http.Request) mockConfig {
|
|
var cfg mockConfig
|
|
for _, part := range strings.Split(r.Header.Get(legacyHeaderFailRegions), ",") {
|
|
if idx, err := strconv.Atoi(strings.TrimSpace(part)); err == nil {
|
|
cfg.FailRegions = append(cfg.FailRegions, idx)
|
|
}
|
|
}
|
|
cfg.FailMode = r.Header.Get(legacyHeaderFailMode)
|
|
cfg.FailStatus = parseStatus(r.Header.Get(legacyHeaderFailStatus))
|
|
cfg.FailTwirpCode = r.Header.Get(legacyHeaderFailTwirpCode)
|
|
cfg.RegionsStatus = parseStatus(r.Header.Get(legacyHeaderRegionsStatus))
|
|
if resp := r.Header.Get(legacyHeaderResponse); resp != "" {
|
|
cfg.Response = json.RawMessage(resp)
|
|
}
|
|
cfg.SkipAuth = strings.EqualFold(r.Header.Get(legacyHeaderSkipAuth), "true")
|
|
cfg.legacyDelayMs = legacyDefaultDelayMs
|
|
if ms, err := strconv.Atoi(r.Header.Get(legacyHeaderDelayMs)); err == nil && ms >= 0 {
|
|
cfg.legacyDelayMs = ms
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// parseStatus returns a valid HTTP status from s, or 0 if absent/invalid.
|
|
func parseStatus(s string) int {
|
|
if v, err := strconv.Atoi(strings.TrimSpace(s)); err == nil && v >= 100 && v <= 599 {
|
|
return v
|
|
}
|
|
return 0
|
|
}
|