oficina / meet Public

relay: parse CARLOS_RELAY and mint TURN credentials (platform vector)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzvnuKhLzudvq7jZ7SvVt2
Paul Campbell pushed by paul@keymail.dev 4e711796e49dd66602c6e45e3dcb2b2113aa8ecc parent 0690a7a
2 files changed, +252 −0
  • internal/meet/relay/relay.go +133 −0
  • internal/meet/relay/relay_test.go +119 −0

CI passed — run details

diff --git a/internal/meet/relay/relay.go b/internal/meet/relay/relay.go
new file mode 100644
index 0000000..75b528d
--- /dev/null
+++ b/internal/meet/relay/relay.go
@@ -0,0 +1,133 @@
+// Package relay reads the platform's CARLOS_RELAY contract and mints
+// per-call TURN credentials from it (platform relay design §2, frozen
+// at revision 6). It is pure: no logging, no clock, no network. The
+// decoded keys live only in memory; nothing here ever formats one.
+package relay
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Region is one element of the contract: the relay hosts for one AWS
+// region, the 32-byte key the platform issued for this app and
+// incarnation, and the username suffix that names account, app,
+// incarnation and key id.
+type Region struct {
+ Region string
+ URLs []string
+ Key []byte
+ UsernameSuffix string
+}
+
+// ICEServer is one RTCIceServer as the browser wants it. A stun entry
+// carries no username; a turn entry carries the minted pair.
+type ICEServer struct {
+ URLs []string `json:"urls"`
+ Username string `json:"username,omitempty"`
+ Credential string `json:"credential,omitempty"`
+}
+
+type wire struct {
+ Region string `json:"region"`
+ URLs []string `json:"urls"`
+ Key string `json:"key"`
+ UsernameSuffix string `json:"username_suffix"`
+}
+
+var (
+ accountRe = regexp.MustCompile(`^[a-z0-9]+$`)
+ appRe = regexp.MustCompile(`^[a-z][a-z0-9-]{0,31}$`)
+ incarnationRe = regexp.MustCompile(`^[0-9a-f]{16}$`)
+ kidRe = regexp.MustCompile(`^[a-z0-9]{1,8}$`)
+)
+
+// Parse decodes $CARLOS_RELAY. An empty value is no relay (nil, nil).
+// Errors name the element index and the field, never the value, so a
+// startup log line built from one cannot leak a key or a suffix.
+func Parse(raw string) ([]Region, error) {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return nil, nil
+ }
+ b, err := base64.StdEncoding.DecodeString(raw)
+ if err != nil {
+ return nil, errors.New("relay: CARLOS_RELAY is not standard base64")
+ }
+ var elems []wire
+ if err := json.Unmarshal(b, &elems); err != nil {
+ return nil, errors.New("relay: CARLOS_RELAY does not decode to a JSON array of regions")
+ }
+ out := make([]Region, 0, len(elems))
+ for i, e := range elems {
+ if e.Region == "" {
+ return nil, fmt.Errorf("relay: element %d has no region", i)
+ }
+ if len(e.URLs) == 0 {
+ return nil, fmt.Errorf("relay: element %d (%s) has no urls", i, e.Region)
+ }
+ key, err := base64.StdEncoding.DecodeString(e.Key)
+ if err != nil || len(key) != 32 {
+ return nil, fmt.Errorf("relay: element %d (%s) key is not 32 base64 bytes", i, e.Region)
+ }
+ fields := strings.Split(e.UsernameSuffix, ":")
+ if len(fields) != 4 || !accountRe.MatchString(fields[0]) || !appRe.MatchString(fields[1]) || !incarnationRe.MatchString(fields[2]) || !kidRe.MatchString(fields[3]) {
+ return nil, fmt.Errorf("relay: element %d (%s) username_suffix is not account:app:incarnation:kid", i, e.Region)
+ }
+ out = append(out, Region{Region: e.Region, URLs: append([]string(nil), e.URLs...), Key: key, UsernameSuffix: e.UsernameSuffix})
+ }
+ return out, nil
+}
+
+// Credential is base64std(HMAC-SHA256(key, username)), the platform's
+// TURN REST scheme.
+func Credential(key []byte, username string) string {
+ mac := hmac.New(sha256.New, key)
+ mac.Write([]byte(username))
+ return base64.StdEncoding.EncodeToString(mac.Sum(nil))
+}
+
+// Mint builds the RTCIceServer list for one call. Per region, in the
+// contract's order: one bare entry for every URL whose scheme is not
+// turn or turns (stun, stuns, or a scheme the contract adds later),
+// then one credentialed entry for the turn and turns URLs. Either may
+// be absent. Expiry is the caller's; §5.2 uses three hours ahead.
+func Mint(regions []Region, expiry time.Time) []ICEServer {
+ out := []ICEServer{}
+ username := strconv.FormatInt(expiry.Unix(), 10)
+ for _, r := range regions {
+ var bare, turn []string
+ for _, u := range r.URLs {
+ switch scheme(u) {
+ case "turn", "turns":
+ turn = append(turn, u)
+ default:
+ bare = append(bare, u)
+ }
+ }
+ if len(bare) > 0 {
+ out = append(out, ICEServer{URLs: bare})
+ }
+ if len(turn) > 0 {
+ name := username + ":" + r.UsernameSuffix
+ out = append(out, ICEServer{URLs: turn, Username: name, Credential: Credential(r.Key, name)})
+ }
+ }
+ return out
+}
+
+func scheme(u string) string {
+ i := strings.IndexByte(u, ':')
+ if i < 0 {
+ return ""
+ }
+ return strings.ToLower(u[:i])
+}
diff --git a/internal/meet/relay/relay_test.go b/internal/meet/relay/relay_test.go
new file mode 100644
index 0000000..ce60a34
--- /dev/null
+++ b/internal/meet/relay/relay_test.go
@@ -0,0 +1,119 @@
+package relay
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "strings"
+ "testing"
+ "time"
+)
+
+const vectorKey = "uMhwW4P/9O5dQUIs91t2CCHGhwJbeiM0xQMQ2G0CJLU="
+
+func encode(t *testing.T, v any) string {
+ t.Helper()
+ b, err := json.Marshal(v)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return base64.StdEncoding.EncodeToString(b)
+}
+
+func oneRegion() []map[string]any {
+ return []map[string]any{{
+ "region": "eu-west-1",
+ "urls": []string{"stun:relay.eu-west-1.oncarlos.com:3478", "turn:relay.eu-west-1.oncarlos.com:3478?transport=udp", "turn:relay.eu-west-1.oncarlos.com:3478?transport=tcp"},
+ "key": vectorKey,
+ "username_suffix": "abc:meet:0123456789abcdef:k1",
+ }}
+}
+
+func TestThePlatformVectorMints(t *testing.T) {
+ key, _ := base64.StdEncoding.DecodeString(vectorKey)
+ got := Credential(key, "1900000000:abc:meet:0123456789abcdef:k1")
+ if got != "jRU479Y3GO45nrRHqxBf3BMeBHE6Fip1cwAccNv28MQ=" {
+ t.Fatalf("credential %q does not match the platform vector", got)
+ }
+}
+
+func TestParseAcceptsTheContractAndDecodesTheKey(t *testing.T) {
+ regions, err := Parse(encode(t, oneRegion()))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(regions) != 1 || regions[0].Region != "eu-west-1" || len(regions[0].Key) != 32 || regions[0].UsernameSuffix != "abc:meet:0123456789abcdef:k1" || len(regions[0].URLs) != 3 {
+ t.Fatalf("parsed %+v", regions)
+ }
+}
+
+func TestParseOfNothingIsNoRelay(t *testing.T) {
+ regions, err := Parse("")
+ if err != nil || regions != nil {
+ t.Fatalf("empty must mean no relay: %v %v", regions, err)
+ }
+}
+
+func TestParseRefusesWithoutEchoingTheValue(t *testing.T) {
+ cases := map[string]string{
+ "not base64": "%%%",
+ "not json": base64.StdEncoding.EncodeToString([]byte("nope")),
+ "not an array": base64.StdEncoding.EncodeToString([]byte(`{"region":"x"}`)),
+ "short key": encode(t, []map[string]any{{"region": "eu-west-1", "urls": []string{"stun:a:1"}, "key": base64.StdEncoding.EncodeToString([]byte("short")), "username_suffix": "abc:meet:0123456789abcdef:k1"}}),
+ "suffix wrong fields": encode(t, []map[string]any{{"region": "eu-west-1", "urls": []string{"stun:a:1"}, "key": vectorKey, "username_suffix": "abc:meet:k1"}}),
+ "suffix bad app": encode(t, []map[string]any{{"region": "eu-west-1", "urls": []string{"stun:a:1"}, "key": vectorKey, "username_suffix": "abc:Meet!:0123456789abcdef:k1"}}),
+ "no urls": encode(t, []map[string]any{{"region": "eu-west-1", "urls": []string{}, "key": vectorKey, "username_suffix": "abc:meet:0123456789abcdef:k1"}}),
+ "empty region": encode(t, []map[string]any{{"region": "", "urls": []string{"stun:a:1"}, "key": vectorKey, "username_suffix": "abc:meet:0123456789abcdef:k1"}}),
+ }
+ for name, raw := range cases {
+ t.Run(name, func(t *testing.T) {
+ _, err := Parse(raw)
+ if err == nil {
+ t.Fatal("accepted")
+ }
+ if strings.Contains(err.Error(), vectorKey) || strings.Contains(err.Error(), "0123456789abcdef") || strings.Contains(err.Error(), raw) {
+ t.Fatalf("the error echoes the value: %v", err)
+ }
+ })
+ }
+}
+
+func TestMintCarriesCredentialsOnTurnOnlyAndPassesURLsThrough(t *testing.T) {
+ regions, err := Parse(encode(t, oneRegion()))
+ if err != nil {
+ t.Fatal(err)
+ }
+ expiry := time.Unix(1900000000, 0)
+ servers := Mint(regions, expiry)
+ if len(servers) != 2 {
+ t.Fatalf("want a bare stun entry and a credentialed turn entry, got %+v", servers)
+ }
+ stun, turn := servers[0], servers[1]
+ if len(stun.URLs) != 1 || stun.URLs[0] != "stun:relay.eu-west-1.oncarlos.com:3478" || stun.Username != "" || stun.Credential != "" {
+ t.Fatalf("stun entry %+v", stun)
+ }
+ if len(turn.URLs) != 2 || turn.Username != "1900000000:abc:meet:0123456789abcdef:k1" || turn.Credential != "jRU479Y3GO45nrRHqxBf3BMeBHE6Fip1cwAccNv28MQ=" {
+ t.Fatalf("turn entry %+v", turn)
+ }
+}
+
+func TestMintKeepsConsoleOrderAndUnknownSchemesBare(t *testing.T) {
+ two := append(oneRegion(), map[string]any{
+ "region": "us-east-1", "urls": []string{"stuns:relay.us-east-1.oncarlos.com:5349", "turns:relay.us-east-1.oncarlos.com:5349", "future:relay.us-east-1.oncarlos.com:1"},
+ "key": vectorKey, "username_suffix": "abc:meet:0123456789abcdef:k1",
+ })
+ regions, err := Parse(encode(t, two))
+ if err != nil {
+ t.Fatal(err)
+ }
+ servers := Mint(regions, time.Unix(1900000000, 0))
+ // eu-west-1 first (stun, turn), then us-east-1 (bare: stuns+future, credentialed: turns).
+ if len(servers) != 4 || servers[0].URLs[0] != "stun:relay.eu-west-1.oncarlos.com:3478" || servers[2].Username != "" || len(servers[2].URLs) != 2 || servers[3].Username == "" || servers[3].URLs[0] != "turns:relay.us-east-1.oncarlos.com:5349" {
+ t.Fatalf("order or scheme handling wrong: %+v", servers)
+ }
+}
+
+func TestMintOfNoRegionsIsAnEmptyListNotNil(t *testing.T) {
+ if got := Mint(nil, time.Now()); got == nil || len(got) != 0 {
+ t.Fatalf("want [] so the JSON is [] not null, got %#v", got)
+ }
+}