From c250e34075ee491eb78605d93ac646969770a970 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Wed, 11 Feb 2026 21:47:02 +0100 Subject: [PATCH] support X-Forwarded-Proto in playback server (#4970) (#5445) allow reverse proxies to change the schema of URLs returned by the server through the X-Forwarded-Proto header. --- api/openapi.yaml | 4 +- internal/playback/on_list.go | 23 ++++++++--- internal/playback/on_list_test.go | 65 +++++++++++++++++++++++++++++++ mediamtx.yml | 16 ++++---- 4 files changed, 92 insertions(+), 16 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index c547110b..e677b2c6 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -2623,7 +2623,7 @@ paths: get: operationId: recordingsList tags: [Recordings] - summary: returns all recordings. + summary: returns all recordings, splitted by path. description: '' parameters: - name: page @@ -2662,7 +2662,7 @@ paths: get: operationId: recordingsGet tags: [Recordings] - summary: returns recordings for a path. + summary: returns recordings of a path. description: '' parameters: - name: name diff --git a/internal/playback/on_list.go b/internal/playback/on_list.go index 4d8714a8..0a4eb8de 100644 --- a/internal/playback/on_list.go +++ b/internal/playback/on_list.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "net" "net/http" "net/url" "os" @@ -83,6 +84,21 @@ func parseSegments(segments []*recordstore.Segment) ([]*parsedSegment, error) { return parsed, err } +func urlScheme(ctx *gin.Context, trustedProxies conf.IPNetworks, encryption bool) string { + if trustedProxies.Contains(net.ParseIP(ctx.RemoteIP())) { + xForwardedProto := ctx.Request.Header.Get("X-Forwarded-Proto") + if xForwardedProto != "" { + return xForwardedProto + } + } + + if encryption { + return "https" + } + + return "http" +} + type listEntry struct { Start time.Time `json:"start"` Duration listEntryDuration `json:"duration"` @@ -212,12 +228,7 @@ func (s *Server) onList(ctx *gin.Context) { } } - var scheme string - if s.Encryption { - scheme = "https" - } else { - scheme = "http" - } + scheme := urlScheme(ctx, s.TrustedProxies, s.Encryption) for i := range entries { v := url.Values{} diff --git a/internal/playback/on_list_test.go b/internal/playback/on_list_test.go index ddf532f0..ca90e623 100644 --- a/internal/playback/on_list_test.go +++ b/internal/playback/on_list_test.go @@ -336,3 +336,68 @@ func TestOnListCachedDuration(t *testing.T) { }, }, out) } + +func TestOnListXForwardedProto(t *testing.T) { + dir, err := os.MkdirTemp("", "mediamtx-playback") + require.NoError(t, err) + defer os.RemoveAll(dir) + + err = os.Mkdir(filepath.Join(dir, "mypath"), 0o755) + require.NoError(t, err) + + writeSegment1(t, filepath.Join(dir, "mypath", "2008-11-07_11-22-00-500000.mp4")) + + var trustedProxies conf.IPNetworks + err = json.Unmarshal([]byte(`["127.0.0.0/8"]`), &trustedProxies) + require.NoError(t, err) + + s := &Server{ + Address: "127.0.0.1:9996", + ReadTimeout: conf.Duration(10 * time.Second), + WriteTimeout: conf.Duration(10 * time.Second), + TrustedProxies: trustedProxies, + PathConfs: map[string]*conf.Path{ + "mypath": { + Name: "mypath", + RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"), + RecordFormat: conf.RecordFormatFMP4, + }, + }, + AuthManager: test.NilAuthManager, + Parent: test.NilLogger, + } + err = s.Initialize() + require.NoError(t, err) + defer s.Close() + + u, err := url.Parse("http://localhost:9996/list") + require.NoError(t, err) + + v := url.Values{} + v.Set("path", "mypath") + u.RawQuery = v.Encode() + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + require.NoError(t, err) + + req.Header.Set("X-Forwarded-Proto", "https") + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + require.Equal(t, http.StatusOK, res.StatusCode) + + var out any + err = json.NewDecoder(res.Body).Decode(&out) + require.NoError(t, err) + + require.Equal(t, []any{ + map[string]any{ + "duration": float64(62), + "start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano), + "url": "https://localhost:9996/get?duration=62&path=mypath&start=" + + url.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)), + }, + }, out) +} diff --git a/mediamtx.yml b/mediamtx.yml index 61e403fd..03363a91 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -171,8 +171,8 @@ apiServerCert: server.crt # Supports wildcards: ['http://*.example.com'] apiAllowOrigins: ['*'] # IPs or CIDRs of proxies placed before the HTTP server. -# If the server receives a request from one of these entries, IP in logs -# will be taken from the X-Forwarded-For header. +# These proxies can use the X-Forwarded-For header to set the real IP of clients, +# and the X-Forwarded-Proto header to set the original protocol. apiTrustedProxies: [] ############################################### @@ -195,8 +195,8 @@ metricsServerCert: server.crt # Supports wildcards: ['http://*.example.com'] metricsAllowOrigins: ['*'] # IPs or CIDRs of proxies placed before the HTTP server. -# If the server receives a request from one of these entries, IP in logs -# will be taken from the X-Forwarded-For header. +# These proxies can use the X-Forwarded-For header to set the real IP of clients, +# and the X-Forwarded-Proto header to set the original protocol. metricsTrustedProxies: [] ############################################### @@ -219,8 +219,8 @@ pprofServerCert: server.crt # Supports wildcards: ['http://*.example.com'] pprofAllowOrigins: ['*'] # IPs or CIDRs of proxies placed before the HTTP server. -# If the server receives a request from one of these entries, IP in logs -# will be taken from the X-Forwarded-For header. +# These proxies can use the X-Forwarded-For header to set the real IP of clients, +# and the X-Forwarded-Proto header to set the original protocol. pprofTrustedProxies: [] ############################################### @@ -243,8 +243,8 @@ playbackServerCert: server.crt # Supports wildcards: ['http://*.example.com'] playbackAllowOrigins: ['*'] # IPs or CIDRs of proxies placed before the HTTP server. -# If the server receives a request from one of these entries, IP in logs -# will be taken from the X-Forwarded-For header. +# These proxies can use the X-Forwarded-For header to set the real IP of clients, +# and the X-Forwarded-Proto header to set the original protocol. playbackTrustedProxies: [] ###############################################