From 0e7af7b947ca13a6e3030a301773fcd59a4517ae Mon Sep 17 00:00:00 2001 From: David Colburn Date: Thu, 18 Jul 2024 16:56:03 -0400 Subject: [PATCH 1/4] route twitch streams to nearest ingest endpoint --- .gitignore | 2 +- pkg/config/config_test.go | 106 ------------------------------------ pkg/config/output_stream.go | 2 +- pkg/config/pipeline.go | 31 ----------- pkg/config/urls.go | 97 +++++++++++++++++++++++++++++++++ pkg/config/urls_test.go | 71 ++++++++++++++++++++++++ pkg/pipeline/controller.go | 4 +- pkg/pipeline/watch.go | 2 +- test/edge.go | 6 +- 9 files changed, 176 insertions(+), 145 deletions(-) create mode 100644 pkg/config/urls.go create mode 100644 pkg/config/urls_test.go diff --git a/.gitignore b/.gitignore index 697af852..97b271c8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ .DS_Store .github/workflows/config.yaml -build/gstcefsrc/*.tar.bz2 +build/plugins/ test/output/* test/*.yaml !test/config-sample.yaml diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 444fabdc..2adc254f 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -22,114 +22,8 @@ import ( "github.com/livekit/egress/pkg/info" "github.com/livekit/protocol/livekit" - "github.com/livekit/protocol/rpc" ) -func TestRedactUpload(t *testing.T) { - t.Cleanup(func() { - _ = os.Remove("test_upload/") - }) - - conf := &ServiceConfig{ - BaseConfig: BaseConfig{ - NodeID: "server", - }, - } - - fileReq := &rpc.StartEgressRequest{ - EgressId: "test_upload", - Request: &rpc.StartEgressRequest_RoomComposite{ - RoomComposite: &livekit.RoomCompositeEgressRequest{ - RoomName: "room", - Layout: "layout", - Output: &livekit.RoomCompositeEgressRequest_File{ - File: &livekit.EncodedFileOutput{ - Filepath: "filepath", - Output: &livekit.EncodedFileOutput_S3{ - S3: &livekit.S3Upload{ - AccessKey: "access", - Secret: "secret", - SessionToken: "", - Bucket: "bucket", - }, - }, - }, - }, - }, - }, - Token: "token", - WsUrl: "wss://egress.com", - } - - p, err := GetValidatedPipelineConfig(conf, fileReq) - require.NoError(t, err) - - require.Equal(t, "******", (*livekit.EgressInfo)(p.Info).GetRoomComposite().GetFile().GetS3().AccessKey) - - require.Len(t, p.Outputs, 1) - output := p.GetFileConfig() - require.NotNil(t, output.UploadConfig) - require.Equal(t, "access", output.UploadConfig.(*livekit.S3Upload).AccessKey) -} - -func TestRedactStreamKeys(t *testing.T) { - t.Cleanup(func() { - _ = os.Remove("test_stream/") - }) - - var ( - streamUrl1 = "rtmp://sfo.contribute.live-video.net/app/stream_key" - redactedUrl1 = "rtmp://sfo.contribute.live-video.net/app/**********" - streamUrl2 = "rtmps://live-api-s.facebook.com:443/rtmp/stream_key" - redactedUrl2 = "rtmps://live-api-s.facebook.com:443/rtmp/**********" - ) - - conf := &ServiceConfig{ - BaseConfig: BaseConfig{ - NodeID: "server", - }, - } - - streamReq := &rpc.StartEgressRequest{ - EgressId: "test_stream", - Request: &rpc.StartEgressRequest_RoomComposite{ - RoomComposite: &livekit.RoomCompositeEgressRequest{ - RoomName: "room", - Layout: "layout", - Output: &livekit.RoomCompositeEgressRequest_Stream{ - Stream: &livekit.StreamOutput{ - Urls: []string{ - streamUrl1, - streamUrl2, - }, - }, - }, - }, - }, - Token: "token", - WsUrl: "wss://egress.com", - } - - p, err := GetValidatedPipelineConfig(conf, streamReq) - require.NoError(t, err) - - urls := (*livekit.EgressInfo)(p.Info).GetRoomComposite().GetStream().GetUrls() - require.Len(t, urls, 2) - require.Equal(t, redactedUrl1, urls[0]) - require.Equal(t, redactedUrl2, urls[1]) - - streamInfo := (*livekit.EgressInfo)(p.Info).GetStream() - require.Len(t, streamInfo.Info, 2) - require.Equal(t, redactedUrl1, streamInfo.Info[0].Url) - require.Equal(t, redactedUrl2, streamInfo.Info[1].Url) - - require.Len(t, p.Outputs, 1) - output := p.GetStreamConfig() - require.Len(t, output.Urls, 2) - require.Equal(t, streamUrl1, output.Urls[0]) - require.Equal(t, streamUrl2, output.Urls[1]) -} - func TestSegmentNaming(t *testing.T) { t.Cleanup(func() { _ = os.RemoveAll("conf_test/") diff --git a/pkg/config/output_stream.go b/pkg/config/output_stream.go index 29e8dfc5..dcd11419 100644 --- a/pkg/config/output_stream.go +++ b/pkg/config/output_stream.go @@ -50,7 +50,7 @@ func (p *PipelineConfig) getStreamConfig(outputType types.OutputType, urls []str conf.StreamInfo = make(map[string]*livekit.StreamInfo) var streamInfoList []*livekit.StreamInfo for _, rawUrl := range urls { - url, redacted, err := p.ValidateUrl(rawUrl, outputType) + url, redacted, err := ValidateUrl(rawUrl, outputType) if err != nil { return nil, err } diff --git a/pkg/config/pipeline.go b/pkg/config/pipeline.go index 3be43cf1..ab800ed9 100644 --- a/pkg/config/pipeline.go +++ b/pkg/config/pipeline.go @@ -16,7 +16,6 @@ package config import ( "context" - "fmt" "net/url" "strings" "time" @@ -34,7 +33,6 @@ import ( "github.com/livekit/protocol/logger" "github.com/livekit/protocol/rpc" "github.com/livekit/protocol/tracer" - "github.com/livekit/protocol/utils" lksdk "github.com/livekit/server-sdk-go/v2" ) @@ -598,35 +596,6 @@ func (p *PipelineConfig) UpdateInfoFromSDK(identifier string, replacements map[s return nil } -func (p *PipelineConfig) ValidateUrl(rawUrl string, outputType types.OutputType) (string, string, error) { - parsed, err := url.Parse(rawUrl) - if err != nil { - return "", "", errors.ErrInvalidUrl(rawUrl, err.Error()) - } - - switch outputType { - case types.OutputTypeRTMP: - if parsed.Scheme == "mux" { - rawUrl = fmt.Sprintf("rtmps://global-live.mux.com:443/app/%s", parsed.Host) - } - - redacted, ok := utils.RedactStreamKey(rawUrl) - if !ok { - return "", "", errors.ErrInvalidUrl(rawUrl, "rtmp urls must be of format rtmp(s)://{host}(/{path})/{app}/{stream_key}( live=1)") - } - return rawUrl, redacted, nil - - case types.OutputTypeRaw: - if parsed.Scheme != "ws" && parsed.Scheme != "wss" { - return "", "", errors.ErrInvalidUrl(rawUrl, "invalid scheme") - } - return rawUrl, rawUrl, nil - - default: - return "", "", errors.ErrInvalidInput("stream output type") - } -} - func (p *PipelineConfig) GetEncodedOutputs() []OutputConfig { ret := make([]OutputConfig, 0) diff --git a/pkg/config/urls.go b/pkg/config/urls.go new file mode 100644 index 00000000..e4f84449 --- /dev/null +++ b/pkg/config/urls.go @@ -0,0 +1,97 @@ +// 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 config + +import ( + "fmt" + "net/http" + "net/url" + "regexp" + "strings" + + "github.com/go-jose/go-jose/v3/json" + + "github.com/livekit/egress/pkg/errors" + "github.com/livekit/egress/pkg/types" + "github.com/livekit/protocol/utils" +) + +var twitchEndpoint = regexp.MustCompile("^rtmps?://.*\\.contribute\\.live-video\\.net/app/(.*)( live=1)?$") + +func ValidateUrl(rawUrl string, outputType types.OutputType) (string, string, error) { + parsed, err := url.Parse(rawUrl) + if err != nil { + return "", "", errors.ErrInvalidUrl(rawUrl, err.Error()) + } + + switch outputType { + case types.OutputTypeRTMP: + if parsed.Scheme == "mux" { + rawUrl = fmt.Sprintf("rtmps://global-live.mux.com:443/app/%s", parsed.Host) + } else if parsed.Scheme == "twitch" { + rawUrl, err = updateTwitchURL(parsed.Host) + if err != nil { + return "", "", errors.ErrInvalidUrl(rawUrl, err.Error()) + } + } else if match := twitchEndpoint.FindStringSubmatch(rawUrl); len(match) > 0 { + updated, err := updateTwitchURL(match[1]) + if err == nil { + rawUrl = updated + } + } + + redacted, ok := utils.RedactStreamKey(rawUrl) + if !ok { + return "", "", errors.ErrInvalidUrl(rawUrl, "rtmp urls must be of format rtmp(s)://{host}(/{path})/{app}/{stream_key}( live=1)") + } + return rawUrl, redacted, nil + + case types.OutputTypeRaw: + if parsed.Scheme != "ws" && parsed.Scheme != "wss" { + return "", "", errors.ErrInvalidUrl(rawUrl, "invalid scheme") + } + return rawUrl, rawUrl, nil + + default: + return "", "", errors.ErrInvalidInput("stream output type") + } +} + +func updateTwitchURL(key string) (string, error) { + resp, err := http.Get("https://ingest.twitch.tv/ingests") + if err != nil { + return "", err + } + defer resp.Body.Close() + var body struct { + Ingests []struct { + Name string `json:"name"` + URLTemplate string `json:"url_template"` + URLTemplateSecure string `json:"url_template_secure"` + Priority int `json:"priority"` + } `json:"ingests"` + } + if err = json.NewDecoder(resp.Body).Decode(&body); err != nil { + return "", err + } + for _, ingest := range body.Ingests { + if ingest.URLTemplateSecure != "" { + return strings.ReplaceAll(ingest.URLTemplateSecure, "{stream_key}", key), nil + } else if ingest.URLTemplate != "" { + return strings.ReplaceAll(ingest.URLTemplate, "{stream_key}", key), nil + } + } + return "", errors.New("no ingest found") +} diff --git a/pkg/config/urls_test.go b/pkg/config/urls_test.go new file mode 100644 index 00000000..a93579c2 --- /dev/null +++ b/pkg/config/urls_test.go @@ -0,0 +1,71 @@ +// 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 config + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/livekit/egress/pkg/types" +) + +func TestValidateUrl(t *testing.T) { + var twitchUpdated = regexp.MustCompile("rtmps://(.*).contribute.live-video.net/app/streamkey") + var twitchRedacted = regexp.MustCompile("rtmps://(.*).contribute.live-video.net/app/\\{str\\.\\.\\.key}") + + for _, test := range []struct { + url string + twitch bool + updated string + redacted string + }{ + { + url: "mux://streamkey", + updated: "rtmps://global-live.mux.com:443/app/streamkey", + redacted: "rtmps://global-live.mux.com:443/app/{str...key}", + }, + { + url: "twitch://streamkey", + twitch: true, + }, + { + url: "rtmp://fake.contribute.live-video.net/app/streamkey", + twitch: true, + }, + { + url: "rtmp://localhost:1935/live/streamkey", + updated: "rtmp://localhost:1935/live/streamkey", + redacted: "rtmp://localhost:1935/live/{str...key}", + }, + { + url: "rtmps://localhost:1935/live/streamkey", + updated: "rtmps://localhost:1935/live/streamkey", + redacted: "rtmps://localhost:1935/live/{str...key}", + }, + } { + updated, redacted, err := ValidateUrl(test.url, types.OutputTypeRTMP) + require.NoError(t, err) + + if test.twitch { + require.NotEmpty(t, twitchUpdated.FindString(updated), updated) + require.NotEmpty(t, twitchRedacted.FindString(redacted), redacted) + } else { + require.Equal(t, test.updated, updated) + require.Equal(t, test.redacted, redacted) + } + } +} diff --git a/pkg/pipeline/controller.go b/pkg/pipeline/controller.go index 8a1b9dcf..c024fc9c 100644 --- a/pkg/pipeline/controller.go +++ b/pkg/pipeline/controller.go @@ -264,7 +264,7 @@ func (c *Controller) UpdateStream(ctx context.Context, req *livekit.UpdateStream // add stream outputs first for _, rawUrl := range req.AddOutputUrls { // validate and redact url - url, redacted, err := c.ValidateUrl(rawUrl, types.OutputTypeRTMP) + url, redacted, err := config.ValidateUrl(rawUrl, types.OutputTypeRTMP) if err != nil { errs.AppendErr(err) continue @@ -297,7 +297,7 @@ func (c *Controller) UpdateStream(ctx context.Context, req *livekit.UpdateStream // remove stream outputs for _, rawUrl := range req.RemoveOutputUrls { - url, _, err := c.ValidateUrl(rawUrl, types.OutputTypeRTMP) + url, _, err := config.ValidateUrl(rawUrl, types.OutputTypeRTMP) if err != nil { errs.AppendErr(err) continue diff --git a/pkg/pipeline/watch.go b/pkg/pipeline/watch.go index 7ab2e062..b72637b2 100644 --- a/pkg/pipeline/watch.go +++ b/pkg/pipeline/watch.go @@ -213,7 +213,7 @@ func (c *Controller) handleMessageError(gErr *gst.GError) error { // Debug info comes in the following format: // file.c(line): method_name (): /GstPipeline:pipeline/GstBin:bin_name/GstElement:element_name:\nError message -var regExp = regexp.MustCompile("(?s)(.*?)GstPipeline:pipeline\\/GstBin:(.*?)\\/(.*?):([^:]*)(:\n)?(.*)") +var regExp = regexp.MustCompile("(?s)(.*?)GstPipeline:pipeline/GstBin:(.*?)/(.*?):([^:]*)(:\n)?(.*)") func parseDebugInfo(gErr *gst.GError) (element, name, message string) { match := regExp.FindStringSubmatch(gErr.DebugString()) diff --git a/test/edge.go b/test/edge.go index d808a880..bac5e150 100644 --- a/test/edge.go +++ b/test/edge.go @@ -35,7 +35,7 @@ func (r *Runner) testEdgeCases(t *testing.T) { } // ParticipantComposite where the participant does not publish a track - r.runParticipantTest(t, "6B/Edge/ParticipantNoPublish", &testCase{}, + r.runParticipantTest(t, "6A/Edge/ParticipantNoPublish", &testCase{}, func(t *testing.T, identity string) { req := &rpc.StartEgressRequest{ EgressId: utils.NewGuid(utils.EgressPrefix), @@ -72,7 +72,7 @@ func (r *Runner) testEdgeCases(t *testing.T) { ) // Stream output with a bad rtmp url or stream key - r.runRoomTest(t, "6A/Edge/RtmpFailure", types.MimeTypeOpus, types.MimeTypeVP8, func(t *testing.T) { + r.runRoomTest(t, "6B/Edge/RtmpFailure", types.MimeTypeOpus, types.MimeTypeVP8, func(t *testing.T) { req := &rpc.StartEgressRequest{ EgressId: utils.NewGuid(utils.EgressPrefix), Request: &rpc.StartEgressRequest_RoomComposite{ @@ -105,7 +105,7 @@ func (r *Runner) testEdgeCases(t *testing.T) { }) // Track composite with data loss due to a disconnection - t.Run("6B/Edge/TrackDisconnection", func(t *testing.T) { + t.Run("6C/Edge/TrackDisconnection", func(t *testing.T) { r.awaitIdle(t) test := &testCase{ From 93dd6a6e2e3bba0482fb5f97eba8af248d7112e3 Mon Sep 17 00:00:00 2001 From: David Colburn Date: Thu, 18 Jul 2024 18:08:39 -0400 Subject: [PATCH 2/4] twitch endpoint may not always be consistent --- pkg/config/urls.go | 27 +++++++++++++++++++++++++++ pkg/config/urls_test.go | 29 +++++++++++++++++++++++++++++ pkg/pipeline/controller.go | 3 ++- 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/pkg/config/urls.go b/pkg/config/urls.go index e4f84449..30aa9ae9 100644 --- a/pkg/config/urls.go +++ b/pkg/config/urls.go @@ -69,6 +69,33 @@ func ValidateUrl(rawUrl string, outputType types.OutputType) (string, string, er } } +func (o *StreamConfig) GetStreamUrl(rawUrl string) (string, error) { + parsed, err := url.Parse(rawUrl) + if err != nil { + return "", errors.ErrInvalidUrl(rawUrl, err.Error()) + } + + var twitchKey string + if parsed.Scheme == "mux" { + return fmt.Sprintf("rtmps://global-live.mux.com:443/app/%s", parsed.Host), nil + } else if parsed.Scheme == "twitch" { + twitchKey = parsed.Host + } else if match := twitchEndpoint.FindStringSubmatch(rawUrl); len(match) > 0 { + twitchKey = match[1] + } else { + return rawUrl, nil + } + + // find twitch url by stream key because we can't rely on the ingest endpoint returning consistent results + for u := range o.StreamInfo { + if match := twitchEndpoint.FindStringSubmatch(u); len(match) > 0 && match[1] == twitchKey { + return u, nil + } + } + + return "", errors.ErrStreamNotFound(rawUrl) +} + func updateTwitchURL(key string) (string, error) { resp, err := http.Get("https://ingest.twitch.tv/ingests") if err != nil { diff --git a/pkg/config/urls_test.go b/pkg/config/urls_test.go index a93579c2..d8b41d55 100644 --- a/pkg/config/urls_test.go +++ b/pkg/config/urls_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/require" "github.com/livekit/egress/pkg/types" + "github.com/livekit/protocol/livekit" ) func TestValidateUrl(t *testing.T) { @@ -69,3 +70,31 @@ func TestValidateUrl(t *testing.T) { } } } + +func TestGetUrl(t *testing.T) { + urls := []string{ + "rtmps://global-live.mux.com:443/app/streamkey", + "rtmp://sfo.contribute.live-video.net/app/streamkey", + "rtmp://sfo.contribute.live-video.net/app/streamkey", + "rtmp://localhost:1935/live/streamkey", + } + + o := &StreamConfig{ + StreamInfo: map[string]*livekit.StreamInfo{ + urls[0]: {Url: urls[0]}, + urls[1]: {Url: urls[1]}, + urls[3]: {Url: urls[3]}, + }, + } + + for i, rawUrl := range []string{ + "mux://streamkey", + "twitch://streamkey", + "rtmp://jfk.contribute.live-video.net/app/streamkey", + "rtmp://localhost:1935/live/streamkey", + } { + url, err := o.GetStreamUrl(rawUrl) + require.NoError(t, err) + require.Equal(t, urls[i], url) + } +} diff --git a/pkg/pipeline/controller.go b/pkg/pipeline/controller.go index c024fc9c..78e6e261 100644 --- a/pkg/pipeline/controller.go +++ b/pkg/pipeline/controller.go @@ -287,6 +287,7 @@ func (c *Controller) UpdateStream(ctx context.Context, req *livekit.UpdateStream Status: livekit.StreamInfo_ACTIVE, } o.StreamInfo[url] = streamInfo + c.Info.StreamResults = append(c.Info.StreamResults, streamInfo) if list := (*livekit.EgressInfo)(c.Info).GetStream(); list != nil { list.Info = append(list.Info, streamInfo) @@ -297,7 +298,7 @@ func (c *Controller) UpdateStream(ctx context.Context, req *livekit.UpdateStream // remove stream outputs for _, rawUrl := range req.RemoveOutputUrls { - url, _, err := config.ValidateUrl(rawUrl, types.OutputTypeRTMP) + url, err := o.GetStreamUrl(rawUrl) if err != nil { errs.AppendErr(err) continue From 35dea12f26ddb6f37c8fa4e174ec30971928d996 Mon Sep 17 00:00:00 2001 From: David Colburn Date: Thu, 18 Jul 2024 18:35:07 -0400 Subject: [PATCH 3/4] fix tests --- test/integration.go | 10 +++++++--- test/stream.go | 5 +---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/integration.go b/test/integration.go index 5505d40a..7b40c9e7 100644 --- a/test/integration.go +++ b/test/integration.go @@ -40,8 +40,8 @@ const ( redactedUrl1 = "rtmp://localhost:1935/live/{st...am}" streamUrl2 = "rtmp://localhost:1935/live/stream_key" redactedUrl2 = "rtmp://localhost:1935/live/{str...key}" - badStreamUrl1 = "rtmp://sfo.contribute.live-video.net/app/fake1" - redactedBadUrl1 = "rtmp://sfo.contribute.live-video.net/app/{f...1}" + badStreamUrl1 = "rtmp://xxx.contribute.live-video.net/app/fake1" + redactedBadUrl1 = "rtmp://xxx.contribute.live-video.net/app/{f...1}" badStreamUrl2 = "rtmp://localhost:1936/live/stream" redactedBadUrl2 = "rtmp://localhost:1936/live/{st...am}" webUrl = "https://videoplayer-2k23.vercel.app/videos/eminem" @@ -270,7 +270,11 @@ func (r *Runner) checkStreamUpdate(t *testing.T, egressID string, expected map[s require.Equal(t, len(expected), len(info.StreamResults)) for _, s := range info.StreamResults { - require.Equal(t, expected[s.Url], s.Status) + if strings.HasSuffix(s.Url, ".contribute.live-video.net/app/{f...1}") { + require.Equal(t, expected[redactedBadUrl1], s.Status) + } else { + require.Equal(t, expected[s.Url], s.Status) + } require.Equal(t, s.Status == livekit.StreamInfo_FAILED, s.Error != "") } } diff --git a/test/stream.go b/test/stream.go index 1f512a69..9223b6ab 100644 --- a/test/stream.go +++ b/test/stream.go @@ -119,11 +119,8 @@ func (r *Runner) runStreamTest(t *testing.T, req *rpc.StartEgressRequest, test * require.Equal(t, livekit.StreamInfo_FINISHED.String(), info.Status.String()) require.Greater(t, float64(info.Duration)/1e9, 10.0) - case redactedBadUrl1, redactedBadUrl2: - require.Equal(t, livekit.StreamInfo_FAILED.String(), info.Status.String()) - default: - t.Fatal("invalid stream url in result") + require.Equal(t, livekit.StreamInfo_FAILED.String(), info.Status.String()) } } } From c355670d2ef6f5f828f026c72e9590693645c1d5 Mon Sep 17 00:00:00 2001 From: David Colburn Date: Fri, 19 Jul 2024 00:13:12 -0400 Subject: [PATCH 4/4] tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 313a30f3..b48c88cd 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/frostbyte73/core v0.0.10 github.com/go-gst/go-glib v1.0.1 github.com/go-gst/go-gst v1.0.0 + github.com/go-jose/go-jose/v3 v3.0.3 github.com/go-logr/logr v1.4.2 github.com/googleapis/gax-go/v2 v2.12.4 github.com/gorilla/websocket v1.5.2 @@ -62,7 +63,6 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gammazero/deque v0.2.1 // indirect - github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect