From 98e4f2f3d36b6d61d843c0d3a726ff85119948af Mon Sep 17 00:00:00 2001 From: martinohmann Date: Fri, 21 Jun 2024 07:45:25 +0200 Subject: [PATCH 1/2] fix: redact passwords in logs Fixes https://github.com/AlexxIT/go2rtc/issues/238 Replaces URLs of the format `rtsp://user:password@localhost:8554` with `rtsp://user:xxxxx@localhost:8554` in logs. This is best-effort for now and does not handle cases where passwords appear in query strings. It should be fairly easy to extend the `RedactPassword` function in the future in case there are other common password pattern that are worth handling. --- internal/streams/helpers.go | 8 ++++++++ internal/streams/helpers_test.go | 14 ++++++++++++++ internal/streams/producer.go | 10 +++++----- internal/streams/streams.go | 10 ++++++---- 4 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 internal/streams/helpers_test.go diff --git a/internal/streams/helpers.go b/internal/streams/helpers.go index 2ead1aa3..205dd56d 100644 --- a/internal/streams/helpers.go +++ b/internal/streams/helpers.go @@ -20,3 +20,11 @@ func ParseQuery(s string) url.Values { } return params } + +func RedactPassword(s string) string { + if u, err := url.Parse(s); err == nil { + return u.Redacted() + } + + return s +} diff --git a/internal/streams/helpers_test.go b/internal/streams/helpers_test.go new file mode 100644 index 00000000..94380c8a --- /dev/null +++ b/internal/streams/helpers_test.go @@ -0,0 +1,14 @@ +package streams + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRedactPassword(t *testing.T) { + require.Equal(t, "not_a_url", RedactPassword("not_a_url")) + require.Equal(t, "rtsp://localhost:8554", RedactPassword("rtsp://localhost:8554")) + require.Equal(t, "rtsp://user:xxxxx@localhost:8554", RedactPassword("rtsp://user:password@localhost:8554")) + require.Equal(t, "rtsp://:xxxxx@localhost:8554", RedactPassword("rtsp://:password@localhost:8554")) +} diff --git a/internal/streams/producer.go b/internal/streams/producer.go index 09e2dcc5..83d2424f 100644 --- a/internal/streams/producer.go +++ b/internal/streams/producer.go @@ -149,7 +149,7 @@ func (p *Producer) start() { return } - log.Debug().Msgf("[streams] start producer url=%s", p.url) + log.Debug().Msgf("[streams] start producer url=%s", RedactPassword(p.url)) p.state = stateStart p.workerID++ @@ -167,7 +167,7 @@ func (p *Producer) worker(conn core.Producer, workerID int) { return } - log.Warn().Err(err).Str("url", p.url).Caller().Send() + log.Warn().Err(err).Str("url", RedactPassword(p.url)).Caller().Send() } p.reconnect(workerID, 0) @@ -178,11 +178,11 @@ func (p *Producer) reconnect(workerID, retry int) { defer p.mu.Unlock() if p.workerID != workerID { - log.Trace().Msgf("[streams] stop reconnect url=%s", p.url) + log.Trace().Msgf("[streams] stop reconnect url=%s", RedactPassword(p.url)) return } - log.Debug().Msgf("[streams] retry=%d to url=%s", retry, p.url) + log.Debug().Msgf("[streams] retry=%d to url=%s", retry, RedactPassword(p.url)) conn, err := GetProducer(p.url) if err != nil { @@ -257,7 +257,7 @@ func (p *Producer) stop() { p.workerID++ } - log.Debug().Msgf("[streams] stop producer url=%s", p.url) + log.Debug().Msgf("[streams] stop producer url=%s", RedactPassword(p.url)) if p.conn != nil { _ = p.conn.Stop() diff --git a/internal/streams/streams.go b/internal/streams/streams.go index ff0f5654..8e3da458 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -119,7 +119,7 @@ func GetOrPatch(query url.Values) *Stream { // check if name param provided if name := query.Get("name"); name != "" { - log.Info().Msgf("[streams] create new stream url=%s", source) + log.Info().Msgf("[streams] create new stream url=%s", RedactPassword(source)) return Patch(name, source) } @@ -143,6 +143,8 @@ func Delete(id string) { delete(streams, id) } -var log zerolog.Logger -var streams = map[string]*Stream{} -var streamsMu sync.Mutex +var ( + log zerolog.Logger + streams = map[string]*Stream{} + streamsMu sync.Mutex +) From bf12db57471dbe4941399ffdb6986d670449f6be Mon Sep 17 00:00:00 2001 From: martinohmann Date: Fri, 21 Jun 2024 12:50:27 +0200 Subject: [PATCH 2/2] feat: add more thorough redact implementation This also deals with query parameters, stream options and is aware of potentially existing redirects. --- go.mod | 3 +- go.sum | 35 +-------- internal/echo/echo.go | 2 +- internal/exec/exec.go | 4 +- internal/hass/hass.go | 12 +-- internal/onvif/init.go | 6 +- internal/streams/helpers.go | 8 -- internal/streams/helpers_test.go | 14 ---- internal/streams/producer.go | 10 +-- internal/streams/redact.go | 124 +++++++++++++++++++++++++++++++ internal/streams/redact_test.go | 66 ++++++++++++++++ internal/streams/streams.go | 2 +- 12 files changed, 212 insertions(+), 74 deletions(-) delete mode 100644 internal/streams/helpers_test.go create mode 100644 internal/streams/redact.go create mode 100644 internal/streams/redact_test.go diff --git a/go.mod b/go.mod index d3cb791f..545f2217 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/AlexxIT/go2rtc go 1.22 require ( - github.com/asticode/go-astits v1.13.0 github.com/expr-lang/expr v1.16.9 github.com/gorilla/websocket v1.5.1 + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-isatty v0.0.20 github.com/miekg/dns v1.1.59 github.com/pion/ice/v2 v2.3.24 @@ -26,7 +26,6 @@ require ( ) require ( - github.com/asticode/go-astikit v0.30.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/kr/pretty v0.2.1 // indirect diff --git a/go.sum b/go.sum index 727787ac..c636c9c6 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,7 @@ -github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA= -github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= -github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= -github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs= -github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI= github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -16,6 +10,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -33,12 +29,8 @@ github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklr github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg= github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA= -github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks= github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/ice/v2 v2.3.19 h1:1GoMRTMnB6bCP4aGy2MjxK3w4laDkk+m7svJb/eqybc= -github.com/pion/ice/v2 v2.3.19/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= github.com/pion/ice/v2 v2.3.24 h1:RYgzhH/u5lH0XO+ABatVKCtRd+4U1GEaCXSMjNr13tI= github.com/pion/ice/v2 v2.3.24/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M= @@ -76,17 +68,12 @@ github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37 github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/webrtc/v3 v3.2.39 h1:Lf2SIMGdE3M9VNm48KpoX5pR8SJ6TsMnktzOkc/oB0o= -github.com/pion/webrtc/v3 v3.2.39/go.mod h1:AQ8p56OLbm3MjhYovYdgPuyX6oc+JcKx/HFoCGFcYzA= github.com/pion/webrtc/v3 v3.2.40 h1:Wtfi6AZMQg+624cvCXUuSmrKWepSB7zfgYDOYqsSOVU= github.com/pion/webrtc/v3 v3.2.40/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= -github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I= @@ -97,7 +84,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -115,16 +101,10 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -138,10 +118,6 @@ golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -166,10 +142,6 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -194,15 +166,12 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/echo/echo.go b/internal/echo/echo.go index fb105cec..979cbbd8 100644 --- a/internal/echo/echo.go +++ b/internal/echo/echo.go @@ -22,7 +22,7 @@ func Init() { b = bytes.TrimSpace(b) - log.Debug().Str("url", url).Msgf("[echo] %s", b) + log.Debug().Str("url", streams.Redact(url)).Msgf("[echo] %s", b) return string(b), nil }) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 035317d9..e6db2f50 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -103,7 +103,7 @@ func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, erro cl, } - log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe") + log.Debug().Strs("args", streams.RedactSlice(cmd.Args)).Msg("[exec] run pipe") ts := time.Now() @@ -144,7 +144,7 @@ func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.P waitersMu.Unlock() }() - log.Debug().Strs("args", cmd.Args).Msg("[exec] run rtsp") + log.Debug().Strs("args", streams.RedactSlice(cmd.Args)).Msg("[exec] run rtsp") ts := time.Now() diff --git a/internal/hass/hass.go b/internal/hass/hass.go index ea172b02..ca86c8ee 100644 --- a/internal/hass/hass.go +++ b/internal/hass/hass.go @@ -177,8 +177,8 @@ func importConfig(config string) error { continue } - log.Debug().Str("url", "hass:"+entrie.Title).Msg("[hass] load config") - //streams.Get("hass:" + entrie.Title) + log.Debug().Str("url", "hass:"+streams.Redact(entrie.Title)).Msg("[hass] load config") + // streams.Get("hass:" + entrie.Title) } return nil @@ -208,6 +208,8 @@ func importWebRTC(token string) error { return nil } -var entities = map[string]string{} -var log zerolog.Logger -var once sync.Once +var ( + entities = map[string]string{} + log zerolog.Logger + once sync.Once +) diff --git a/internal/onvif/init.go b/internal/onvif/init.go index 014c5e18..c152daa5 100644 --- a/internal/onvif/init.go +++ b/internal/onvif/init.go @@ -43,7 +43,7 @@ func streamOnvif(rawURL string) (core.Producer, error) { return nil, err } - log.Debug().Msgf("[onvif] new uri=%s", uri) + log.Debug().Msgf("[onvif] new uri=%s", streams.Redact(uri)) return streams.GetProducer(uri) } @@ -134,12 +134,12 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) { for _, rawURL := range urls { u, err := url.Parse(rawURL) if err != nil { - log.Warn().Str("url", rawURL).Msg("[onvif] broken") + log.Warn().Str("url", streams.Redact(rawURL)).Msg("[onvif] broken") continue } if u.Scheme != "http" { - log.Warn().Str("url", rawURL).Msg("[onvif] unsupported") + log.Warn().Str("url", streams.Redact(rawURL)).Msg("[onvif] unsupported") continue } diff --git a/internal/streams/helpers.go b/internal/streams/helpers.go index 205dd56d..2ead1aa3 100644 --- a/internal/streams/helpers.go +++ b/internal/streams/helpers.go @@ -20,11 +20,3 @@ func ParseQuery(s string) url.Values { } return params } - -func RedactPassword(s string) string { - if u, err := url.Parse(s); err == nil { - return u.Redacted() - } - - return s -} diff --git a/internal/streams/helpers_test.go b/internal/streams/helpers_test.go deleted file mode 100644 index 94380c8a..00000000 --- a/internal/streams/helpers_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package streams - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestRedactPassword(t *testing.T) { - require.Equal(t, "not_a_url", RedactPassword("not_a_url")) - require.Equal(t, "rtsp://localhost:8554", RedactPassword("rtsp://localhost:8554")) - require.Equal(t, "rtsp://user:xxxxx@localhost:8554", RedactPassword("rtsp://user:password@localhost:8554")) - require.Equal(t, "rtsp://:xxxxx@localhost:8554", RedactPassword("rtsp://:password@localhost:8554")) -} diff --git a/internal/streams/producer.go b/internal/streams/producer.go index 83d2424f..bc6ed558 100644 --- a/internal/streams/producer.go +++ b/internal/streams/producer.go @@ -149,7 +149,7 @@ func (p *Producer) start() { return } - log.Debug().Msgf("[streams] start producer url=%s", RedactPassword(p.url)) + log.Debug().Msgf("[streams] start producer url=%s", Redact(p.url)) p.state = stateStart p.workerID++ @@ -167,7 +167,7 @@ func (p *Producer) worker(conn core.Producer, workerID int) { return } - log.Warn().Err(err).Str("url", RedactPassword(p.url)).Caller().Send() + log.Warn().Err(err).Str("url", Redact(p.url)).Caller().Send() } p.reconnect(workerID, 0) @@ -178,11 +178,11 @@ func (p *Producer) reconnect(workerID, retry int) { defer p.mu.Unlock() if p.workerID != workerID { - log.Trace().Msgf("[streams] stop reconnect url=%s", RedactPassword(p.url)) + log.Trace().Msgf("[streams] stop reconnect url=%s", Redact(p.url)) return } - log.Debug().Msgf("[streams] retry=%d to url=%s", retry, RedactPassword(p.url)) + log.Debug().Msgf("[streams] retry=%d to url=%s", retry, Redact(p.url)) conn, err := GetProducer(p.url) if err != nil { @@ -257,7 +257,7 @@ func (p *Producer) stop() { p.workerID++ } - log.Debug().Msgf("[streams] stop producer url=%s", RedactPassword(p.url)) + log.Debug().Msgf("[streams] stop producer url=%s", Redact(p.url)) if p.conn != nil { _ = p.conn.Stop() diff --git a/internal/streams/redact.go b/internal/streams/redact.go new file mode 100644 index 00000000..6087f975 --- /dev/null +++ b/internal/streams/redact.go @@ -0,0 +1,124 @@ +package streams + +import ( + "fmt" + "net/url" + "regexp" + "sort" + "strings" + + "github.com/kballard/go-shellquote" +) + +// Redact takes a raw stream URL and tries to redact sensitive values. +// +// The returned string is only meant to be used for display purposes. +func Redact(rawURL string) string { + if scheme, rest, ok := strings.Cut(rawURL, ":"); ok { + if scheme == "exec" { + return fmt.Sprintf("%s:%s", scheme, redactCmd(rest)) + } + + if _, ok := redirects[scheme]; ok { + // We're not actually following the redirect here because that + // would alter the URL and might cause confusion when users try to + // reason about log entries. E.g. an `ffmpeg:` URL would expand to + // a very long ffmpeg command that the user did not configure + // explictly. + // + // Instead, we just redact the remaining URL after the redirect + // scheme and add it back afterwards. + return fmt.Sprintf("%s:%s", scheme, redact(rest)) + } + + return redact(rawURL) + } + + // Not a URL, leave as it. + return rawURL +} + +// RedactSlice takes a slice of strings and tries to redact sensitive values +// from URLs contained in them. +// +// The returned slice is only meant to be used for display purposes. +func RedactSlice(args []string) []string { + redacted := make([]string, len(args)) + for i, arg := range args { + redacted[i] = Redact(arg) + } + + return redacted +} + +func redact(rawURL string) string { + if url, options, ok := strings.Cut(rawURL, "#"); ok { + return fmt.Sprintf("%s%s", redactURL(url), redactOptions(options)) + } + + return redactURL(rawURL) +} + +func redactCmd(rawCmd string) string { + args, err := shellquote.Split(rawCmd) + if err != nil { + return rawCmd + } + + return shellquote.Join(RedactSlice(args)...) +} + +func redactURL(rawURL string) string { + if u, err := url.Parse(rawURL); err == nil { + u.RawQuery = redactSensitiveValues(u.Query()).Encode() + return u.Redacted() + } + + return rawURL +} + +func redactOptions(rawOptions string) string { + options := ParseQuery(rawOptions) + if len(options) == 0 { + return "" + } + + redactedOptions := redactSensitiveValues(options) + + // Sort keys to ensure the formatted result is stable. + keys := make([]string, 0, len(redactedOptions)) + for k := range redactedOptions { + keys = append(keys, k) + } + sort.Strings(keys) + + var sb strings.Builder + + for _, key := range keys { + for _, value := range redactedOptions[key] { + sb.WriteRune('#') + sb.WriteString(key) + sb.WriteRune('=') + sb.WriteString(value) + } + } + + return sb.String() +} + +var ( + sensitiveKeysRe = regexp.MustCompile("(?i).*(secret|pass|token).*") + sensitiveValuesRe = regexp.MustCompile("(?i)Authorization:.*") +) + +func redactSensitiveValues(values url.Values) url.Values { + for key, vals := range values { + for i, val := range vals { + if sensitiveKeysRe.MatchString(key) || sensitiveValuesRe.MatchString(val) { + values[key][i] = "xxxxx" // Same placeholder as emitted by (net/url.URL).Redacted(). + } + } + } + + return values +} diff --git a/internal/streams/redact_test.go b/internal/streams/redact_test.go new file mode 100644 index 00000000..9b2c0251 --- /dev/null +++ b/internal/streams/redact_test.go @@ -0,0 +1,66 @@ +package streams + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRedact(t *testing.T) { + assert := assert.New(t) + + // Init fake redirect. + RedirectFunc("fakeffmpeg", func(url string) (string, error) { return url[7:], nil }) + + // No sensitive information + assert.Equal("not_a_url", Redact("not_a_url")) + assert.Equal("rtsp://localhost:8554", Redact("rtsp://localhost:8554")) + + // User + password + assert.Equal( + "rtsp://user:xxxxx@localhost:8554", + Redact("rtsp://user:password@localhost:8554"), + ) + + // Only password + assert.Equal( + "rtsp://:xxxxx@localhost:8554", + Redact("rtsp://:password@localhost:8554"), + ) + + // With configured redirect + assert.Equal( + "fakeffmpeg:rtsp://:xxxxx@localhost:8554", + Redact("fakeffmpeg:rtsp://:password@localhost:8554"), + ) + + // Header option + assert.Equal( + "https://mjpeg.sanford.io/count.mjpeg#header=xxxxx", + Redact("https://mjpeg.sanford.io/count.mjpeg#header=Authorization: Bearer XXX"), + ) + + // Token query parameter + assert.Equal( + "hass://192.168.1.123:8123?entity_id=camera.nest_doorbell&token=xxxxx", + Redact("hass://192.168.1.123:8123?entity_id=camera.nest_doorbell&token=the-token"), + ) + + // OAuth credentials + assert.Equal( + "nest:?client_id=client-id&client_secret=xxxxx&refresh_token=xxxxx", + Redact("nest:?client_id=client-id&client_secret=client-secret&refresh_token=refresh-token"), + ) + + // Redirect + password + query parameters + options + assert.Equal( + "fakeffmpeg:rtsp://:xxxxx@localhost:8554?foo=bar&token=xxxxx#refresh_token=xxxxx#timeout=30", + Redact("fakeffmpeg:rtsp://:password@localhost:8554?foo=bar&token=foo#timeout=30#refresh_token=baz"), + ) + + // Exec + assert.Equal( + "exec:some-command --stream rtsp://user:xxxxx@localhost:8554 --name 'my stream'", + Redact("exec: some-command --stream rtsp://user:password@localhost:8554 --name 'my stream'"), + ) +} diff --git a/internal/streams/streams.go b/internal/streams/streams.go index 8e3da458..b13aca2a 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -119,7 +119,7 @@ func GetOrPatch(query url.Values) *Stream { // check if name param provided if name := query.Get("name"); name != "" { - log.Info().Msgf("[streams] create new stream url=%s", RedactPassword(source)) + log.Info().Msgf("[streams] create new stream url=%s", Redact(source)) return Patch(name, source) }