diff --git a/rpc/helpers.go b/rpc/helpers.go index aec4d897..4268edab 100644 --- a/rpc/helpers.go +++ b/rpc/helpers.go @@ -247,7 +247,6 @@ func toProtoUser(u *radio.User) *User { //Password: u.Password, //Email: u.Email, //RememberToken: u.RememberToken, - //UserPermissions: u.UserPermissions, } } @@ -269,7 +268,6 @@ func fromProtoUser(u *User) *radio.User { //Password: u.Password, //Email: u.Email, //RememberToken: u.RememberToken, - //UserPermissions: u.UserPermissions, } } diff --git a/tracker/recorder.go b/tracker/recorder.go index c055c2bb..a8fd73d6 100644 --- a/tracker/recorder.go +++ b/tracker/recorder.go @@ -2,8 +2,8 @@ package tracker import ( "context" + "net" "net/http" - "net/url" "strings" "sync" "sync/atomic" @@ -20,12 +20,7 @@ import ( type Listener struct { span trace.Span - // ID is the identifier icecast is using for this client - ID radio.ListenerClientID - // Start is the time this listener started listening - Start time.Time - // Info is the information icecast sends us through the POST form values - Info url.Values + radio.Listener } func NewRecorder(ctx context.Context) *Recorder { @@ -86,10 +81,13 @@ func (r *Recorder) ListenerAdd(ctx context.Context, id radio.ListenerClientID, r ) listener := Listener{ - ID: id, - span: span, - Start: time.Now(), - Info: req.PostForm, + span: span, + Listener: radio.Listener{ + ID: id, + UserAgent: req.PostFormValue("agent"), // passed by icecast + Start: time.Now(), + IP: IcecastRealIP(req), + }, } var ok bool @@ -131,3 +129,33 @@ func requestToOtelAttributes(req *http.Request) []attribute.KeyValue { } return res } + +const prefix = "client." + +var xForwardedFor = strings.ToLower(prefix + "X-Forwarded-For") +var xRealIP = strings.ToLower(prefix + "X-Real-IP") +var trueClientIP = strings.ToLower(prefix + "True-Client-IP") + +// icecastRealIP recovers the clients real ip address from the request +// +// This looks for X-Forwarded-For, X-Real-IP and True-Client-IP +func IcecastRealIP(r *http.Request) string { + var ip string + + if tcip := r.PostForm.Get(trueClientIP); tcip != "" { + ip = tcip + } else if xrip := r.PostForm.Get(xRealIP); xrip != "" { + ip = xrip + } else if xff := r.PostForm.Get(xForwardedFor); xff != "" { + i := strings.Index(xff, ",") + if i == -1 { + i = len(xff) + } + ip = xff[:i] + } + if ip == "" || net.ParseIP(ip) == nil { + return "" + } + return ip + +} diff --git a/tracker/recorder_test.go b/tracker/recorder_test.go index 41bb9fb1..9c758f10 100644 --- a/tracker/recorder_test.go +++ b/tracker/recorder_test.go @@ -4,6 +4,8 @@ import ( "context" "net/http" "net/http/httptest" + "net/url" + "strings" "testing" "time" @@ -167,3 +169,78 @@ func TestRecorderRemoveStalePending(t *testing.T) { }, eventuallyDelay, eventuallyTick) }) } + +func TestIcecastRealIP(t *testing.T) { + t.Run(xForwardedFor, func(t *testing.T) { + ctx := testCtx(t) + r := NewRecorder(ctx) + id := radio.ListenerClientID(50) + ip := "192.168.1.1" + values := url.Values{} + values.Add(xForwardedFor, ip) + + body := strings.NewReader(values.Encode()) + req := httptest.NewRequest(http.MethodPost, "/listener_add", body) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + r.ListenerAdd(ctx, id, req) + + r.mu.Lock() + assert.Equal(t, ip, r.listeners[id].IP) + r.mu.Unlock() + }) + t.Run(xForwardedFor+"/multiple", func(t *testing.T) { + ctx := testCtx(t) + r := NewRecorder(ctx) + id := radio.ListenerClientID(50) + ip := "192.168.1.1, 203.0.113.195, 70.41.3.18, 150.172.238.178" + values := url.Values{} + values.Add(xForwardedFor, ip) + + body := strings.NewReader(values.Encode()) + req := httptest.NewRequest(http.MethodPost, "/listener_add", body) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + r.ListenerAdd(ctx, id, req) + + r.mu.Lock() + assert.Equal(t, "192.168.1.1", r.listeners[id].IP) + r.mu.Unlock() + }) + t.Run(trueClientIP, func(t *testing.T) { + ctx := testCtx(t) + r := NewRecorder(ctx) + id := radio.ListenerClientID(50) + ip := "192.168.1.1" + values := url.Values{} + values.Add(trueClientIP, ip) + + body := strings.NewReader(values.Encode()) + req := httptest.NewRequest(http.MethodPost, "/listener_add", body) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + r.ListenerAdd(ctx, id, req) + + r.mu.Lock() + assert.Equal(t, ip, r.listeners[id].IP) + r.mu.Unlock() + }) + t.Run(xRealIP, func(t *testing.T) { + ctx := testCtx(t) + r := NewRecorder(ctx) + id := radio.ListenerClientID(50) + ip := "192.168.1.1" + values := url.Values{} + values.Add(xRealIP, ip) + + body := strings.NewReader(values.Encode()) + req := httptest.NewRequest(http.MethodPost, "/listener_add", body) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + r.ListenerAdd(ctx, id, req) + + r.mu.Lock() + assert.Equal(t, ip, r.listeners[id].IP) + r.mu.Unlock() + }) +} diff --git a/website/main.go b/website/main.go index f627022a..8958b53f 100644 --- a/website/main.go +++ b/website/main.go @@ -218,7 +218,7 @@ func Execute(ctx context.Context, cfg config.Config) error { // RedirectLegacyStream redirects a request to the (new) icecast stream url func RedirectLegacyStream(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Location", "//stream.r-a-d.io/main") + w.Header().Set("Location", "//stream.r-a-d.io/main.mp3") w.WriteHeader(http.StatusMovedPermanently) } diff --git a/website/public/faves.go b/website/public/faves.go index 916535f8..3b9f2f9c 100644 --- a/website/public/faves.go +++ b/website/public/faves.go @@ -76,6 +76,7 @@ func (s State) GetFaves(w http.ResponseWriter, r *http.Request) { err := json.NewEncoder(w).Encode(NewFaveDownload(input.Faves)) if err != nil { s.errorHandler(w, r, err) + return } return }