diff --git a/egress/redact.go b/egress/redact.go index 96ffe66c0..944e84c69 100644 --- a/egress/redact.go +++ b/egress/redact.go @@ -35,7 +35,7 @@ func RedactUpload(req UploadRequest) { s3.AccessKey = utils.Redact(s3.AccessKey, "{access_key}") s3.Secret = utils.Redact(s3.Secret, "{secret}") s3.AssumeRoleExternalId = utils.Redact(s3.AssumeRoleExternalId, "{external_id}") - s3.SessionToken = utils.Redact(s3.AssumeRoleExternalId, "{session_token}") + s3.SessionToken = utils.Redact(s3.SessionToken, "{session_token}") return } @@ -91,6 +91,11 @@ func RedactDirectOutputs(out DirectOutput) { if f := out.GetFile(); f != nil { RedactUpload(f) } + if track, ok := out.(*livekit.TrackEgressRequest); ok { + if ws, ok := track.Output.(*livekit.TrackEgressRequest_WebsocketUrl); ok { + ws.WebsocketUrl = utils.RedactUrlQueryValues(ws.WebsocketUrl) + } + } } func RedactStreamKeys(stream *livekit.StreamOutput) { diff --git a/egress/redact_test.go b/egress/redact_test.go index 561e67276..d0ee4e2f0 100644 --- a/egress/redact_test.go +++ b/egress/redact_test.go @@ -71,6 +71,20 @@ func TestRedactUpload(t *testing.T) { require.Equal(t, "{external_id}", cl.(*livekit.EncodedFileOutput).Output.(*livekit.EncodedFileOutput_S3).S3.AssumeRoleExternalId) require.Equal(t, "{session_token}", cl.(*livekit.EncodedFileOutput).Output.(*livekit.EncodedFileOutput_S3).S3.SessionToken) + sessionTokenOnly := &livekit.EncodedFileOutput{ + Output: &livekit.EncodedFileOutput_S3{ + S3: &livekit.S3Upload{ + AccessKey: "ACCESS_KEY", + Secret: "LONG_SECRET_STRING", + SessionToken: "SESSION_TOKEN", + }, + }, + } + cl = proto.Clone(sessionTokenOnly) + RedactUpload(cl.(UploadRequest)) + + require.Equal(t, "{session_token}", cl.(*livekit.EncodedFileOutput).Output.(*livekit.EncodedFileOutput_S3).S3.SessionToken) + cl = proto.Clone(image) RedactUpload(cl.(UploadRequest)) @@ -93,11 +107,19 @@ func TestRedactStreamOutput(t *testing.T) { so := &livekit.StreamOutput{ Urls: []string{ "rtmps://foo.bar.com/app/secret_stream_key", + "mux://8e0b1b9c-50a2-d893-ec0a-102056d112ae", + "twitch://live_12345678_abcdefghijklmnop", + "srt://foo.bar.com:9999", + "srt://foo.bar.com:9999?streamid=124939da-5244&passphrase=WnxknzJTUbwYl9SpdqAudX", }, } RedactStreamKeys(so) require.Equal(t, "rtmps://foo.bar.com/app/{sec...key}", so.Urls[0]) + require.Equal(t, "mux://{8e0...2ae}", so.Urls[1]) + require.Equal(t, "twitch://{liv...nop}", so.Urls[2]) + require.Equal(t, "srt://foo.bar.com:9999", so.Urls[3]) + require.Equal(t, "srt://foo.bar.com:9999?streamid={...}&passphrase={...}", so.Urls[4]) } func TestRedactEncodedOutputs(t *testing.T) { @@ -154,4 +176,15 @@ func TestRedactDirectOutput(t *testing.T) { RedactDirectOutputs(track) require.Equal(t, "{access_key}", track.Output.(*livekit.TrackEgressRequest_File).File.Output.(*livekit.DirectFileOutput_S3).S3.AccessKey) require.Equal(t, "{secret}", track.Output.(*livekit.TrackEgressRequest_File).File.Output.(*livekit.DirectFileOutput_S3).S3.Secret) + + websocket := &livekit.TrackEgressRequest{ + Output: &livekit.TrackEgressRequest_WebsocketUrl{ + WebsocketUrl: "wss://foo.bar.com/audio?callId=12145&token=7df13dab0d4437602d6f7056b97e72b9", + }, + } + + RedactDirectOutputs(websocket) + require.Equal(t, + "wss://foo.bar.com/audio?callId={...}&token={...}", + websocket.Output.(*livekit.TrackEgressRequest_WebsocketUrl).WebsocketUrl) } diff --git a/livekit/livekit_room.pb.go b/livekit/livekit_room.pb.go index b228265cb..1c2535d4e 100644 --- a/livekit/livekit_room.pb.go +++ b/livekit/livekit_room.pb.go @@ -1174,7 +1174,7 @@ type RoomConfiguration struct { EmptyTimeout uint32 `protobuf:"varint,2,opt,name=empty_timeout,json=emptyTimeout,proto3" json:"empty_timeout,omitempty"` // number of seconds to keep the room open after everyone leaves DepartureTimeout uint32 `protobuf:"varint,3,opt,name=departure_timeout,json=departureTimeout,proto3" json:"departure_timeout,omitempty"` - // limit number of participants that can be in a room, excluding Egress and Ingress participants + // limit number of participants that can be in a room, excluding Egress and Agent participants MaxParticipants uint32 `protobuf:"varint,4,opt,name=max_participants,json=maxParticipants,proto3" json:"max_participants,omitempty"` // metadata of room Metadata string `protobuf:"bytes,11,opt,name=metadata,proto3" json:"metadata,omitempty"` diff --git a/protobufs/livekit_room.proto b/protobufs/livekit_room.proto index 6505837c4..3232438d6 100644 --- a/protobufs/livekit_room.proto +++ b/protobufs/livekit_room.proto @@ -61,7 +61,7 @@ service RoomService { rpc SendData(SendDataRequest) returns (SendDataResponse); // Update room metadata, will cause updates to be broadcasted to everyone in the room, Requires `roomAdmin` - rpc UpdateRoomMetadata (UpdateRoomMetadataRequest) returns (Room); + rpc UpdateRoomMetadata(UpdateRoomMetadataRequest) returns (Room); // Cloud-only // Forward a connected participant's track(s) to another room. Requires `roomAdmin` and `destinationRoom`. The forwarding will @@ -125,7 +125,6 @@ message RoomAgent { repeated RoomAgentDispatch dispatches = 1; } - message ListRoomsRequest { // when set, will only return rooms with name match repeated string names = 1; @@ -222,7 +221,7 @@ message SendDataRequest { bytes data = 2; DataPacket.Kind kind = 3; // mark deprecated - repeated string destination_sids = 4 [deprecated=true]; + repeated string destination_sids = 4 [deprecated = true]; // when set, only forward to these identities repeated string destination_identities = 6; optional string topic = 5; @@ -246,12 +245,12 @@ message UpdateRoomMetadataRequest { } message RoomConfiguration { - string name = 1; // Used as ID, must be unique + string name = 1; // Used as ID, must be unique // number of seconds to keep the room open if no one joins uint32 empty_timeout = 2; // number of seconds to keep the room open after everyone leaves uint32 departure_timeout = 3; - // limit number of participants that can be in a room, excluding Egress and Ingress participants + // limit number of participants that can be in a room, excluding Egress and Agent participants uint32 max_participants = 4; // metadata of room string metadata = 11 [ diff --git a/utils/redact.go b/utils/redact.go index d894f6efa..7aa5e90bb 100644 --- a/utils/redact.go +++ b/utils/redact.go @@ -23,9 +23,19 @@ import ( // rtmp urls must be of format rtmp(s)://{host}(/{path})/{app}/{stream_key}( live=1) var rtmpRegexp = regexp.MustCompile(`^(rtmps?://)(.*/)(.*/)(\S*)( live=1)?$`) +// mux and twitch shorthand urls carry the stream key as the entire authority +var shorthandStreamRegexp = regexp.MustCompile(`^(mux|twitch)://(\S+)$`) + func RedactStreamKey(url string) (string, bool) { + if match := shorthandStreamRegexp.FindStringSubmatch(url); len(match) == 3 { + return match[1] + "://" + RedactIdentifier(match[2]), true + } + match := rtmpRegexp.FindStringSubmatch(url) if len(match) != 6 { + if redacted := RedactUrlQueryValues(url); redacted != url { + return redacted, true + } return url, false } @@ -33,6 +43,22 @@ func RedactStreamKey(url string) (string, bool) { return strings.Join(match[1:], ""), true } +// RedactUrlQueryValues fully redacts every query parameter value in rawUrl, keeping parameter names intact. +func RedactUrlQueryValues(rawUrl string) string { + base, query, found := strings.Cut(rawUrl, "?") + if !found || query == "" { + return rawUrl + } + + params := strings.Split(query, "&") + for i, p := range params { + if k, v, ok := strings.Cut(p, "="); ok && v != "" { + params[i] = k + "={...}" + } + } + return base + "?" + strings.Join(params, "&") +} + func RedactIdentifier(identifier string) string { var prefix, suffix string for i := 3; i > 0; i-- {