From 9070e9a2c296f28ac7a2b60d8020c6560909e209 Mon Sep 17 00:00:00 2001 From: Daniel Carrillo Date: Sat, 30 Apr 2022 17:10:22 +0200 Subject: [PATCH] Use Accept request header instead of user agent to figure out non-browser clients. Funny fact, I borrowed the idea from a fork ('https://github.com/hachmeister/whatismyip/blob/8a3e142cf3ebf54ed3a4b6de2173cc5b13b40e18/router/generic.go#L33') --- README.md | 2 +- integration-tests/integration_test.go | 10 +- router/generic.go | 13 +-- router/generic_test.go | 152 ++++++++++++++++++-------- router/setup_test.go | 4 + 5 files changed, 124 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 713bd9c..a527132 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ curl -6 ifconfig.es ## Endpoints - https://ifconfig.es/ -- https://ifconfig.es/json +- https://ifconfig.es/json (this is the same as `curl -H "Accept: application/json" https://ifconfig.es/`) - https://ifconfig.es/geo - https://ifconfig.es/geo/city - https://ifconfig.es/geo/country diff --git a/integration-tests/integration_test.go b/integration-tests/integration_test.go index 024e988..1ae4b1c 100644 --- a/integration-tests/integration_test.go +++ b/integration-tests/integration_test.go @@ -70,16 +70,18 @@ func TestContainerIntegration(t *testing.T) { }() http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - for _, url := range []string{"http://localhost:8000/json", "https://localhost:8001/json"} { - resp, _ := http.Get(url) + for _, url := range []string{"http://localhost:8000", "https://localhost:8001"} { + client := &http.Client{} + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Accept", "application/json") + resp, _ := client.Do(req) assert.Equal(t, 200, resp.StatusCode) - var dat router.JSONResponse body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatal(err) } - assert.NoError(t, json.Unmarshal(body, &dat)) + assert.NoError(t, json.Unmarshal(body, &router.JSONResponse{})) } } diff --git a/router/generic.go b/router/generic.go index 5b36cf6..8f912f7 100644 --- a/router/generic.go +++ b/router/generic.go @@ -4,7 +4,6 @@ import ( "net" "net/http" "path/filepath" - "regexp" "github.com/dcarrillo/whatismyip/internal/httputils" "github.com/dcarrillo/whatismyip/internal/setting" @@ -12,8 +11,6 @@ import ( "github.com/gin-gonic/gin" ) -const userAgentPattern = `curl|wget|libwww-perl|python|ansible-httpget|HTTPie|WindowsPowerShell|http_request|Go-http-client|^$` - // JSONResponse maps data as json type JSONResponse struct { IP string `json:"ip"` @@ -33,15 +30,17 @@ type JSONResponse struct { } func getRoot(ctx *gin.Context) { - reg := regexp.MustCompile(userAgentPattern) - if reg.Match([]byte(ctx.Request.UserAgent())) { - ctx.String(http.StatusOK, ctx.ClientIP()) - } else { + switch ctx.NegotiateFormat(gin.MIMEPlain, gin.MIMEHTML, gin.MIMEJSON) { + case gin.MIMEHTML: name := "home" if setting.App.TemplatePath != "" { name = filepath.Base(setting.App.TemplatePath) } ctx.HTML(http.StatusOK, name, jsonOutput(ctx)) + case gin.MIMEJSON: + getJSON(ctx) + default: + ctx.String(http.StatusOK, ctx.ClientIP()+"\n") } } diff --git a/router/generic_test.go b/router/generic_test.go index d0d375b..3cd53e9 100644 --- a/router/generic_test.go +++ b/router/generic_test.go @@ -9,31 +9,80 @@ import ( "github.com/stretchr/testify/assert" ) -func TestIP4RootFromCli(t *testing.T) { - uas := []string{ - "", - "curl", - "wget", - "libwww-perl", - "python", - "ansible-httpget", - "HTTPie", - "WindowsPowerShell", - "http_request", - "Go-http-client", +func TestRootContentType(t *testing.T) { + tests := []struct { + name string + accepted string + expected string + }{ + { + name: "Accept wildcard", + accepted: "*/*", + expected: contentType.text, + }, + { + name: "Bogus accept", + accepted: "bogus/plain", + expected: contentType.text, + }, + { + name: "Accept plain text", + accepted: "text/plain", + expected: contentType.text, + }, + { + name: "Accept json", + accepted: "application/json", + expected: contentType.json, + }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "/", nil) + req.Header.Set(trustedHeader, testIP.ipv4) + req.Header.Set("Accept", tt.accepted) - req, _ := http.NewRequest("GET", "/", nil) - req.Header.Set("X-Real-IP", testIP.ipv4) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) - for _, ua := range uas { - req.Header.Set("User-Agent", ua) + assert.Equal(t, 200, w.Code) + assert.Equal(t, tt.expected, w.Header().Get("Content-Type")) + }) + } +} - w := httptest.NewRecorder() - app.ServeHTTP(w, req) +func TestGetIP(t *testing.T) { + expected := testIP.ipv4 + "\n" + tests := []struct { + name string + accepted string + }{ + { + name: "No browser", + accepted: "*/*", + }, + { + name: "Bogus accept", + accepted: "bogus/plain", + }, + { + name: "Plain accept", + accepted: "text/plain", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "/", nil) + req.Header.Set(trustedHeader, testIP.ipv4) + req.Header.Set("Accept", tt.accepted) - assert.Equal(t, 200, w.Code) - assert.Equal(t, testIP.ipv4, w.Body.String()) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + assert.Equal(t, 200, w.Code) + assert.Equal(t, expected, w.Body.String()) + assert.Equal(t, contentType.text, w.Header().Get("Content-Type")) + }) } } @@ -50,7 +99,7 @@ func TestHost(t *testing.T) { func TestClientPort(t *testing.T) { req, _ := http.NewRequest("GET", "/client-port", nil) req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000") - req.Header.Set("X-Real-IP", testIP.ipv4) + req.Header.Set(trustedHeader, testIP.ipv4) w := httptest.NewRecorder() app.ServeHTTP(w, req) @@ -71,31 +120,44 @@ func TestNotFound(t *testing.T) { } func TestJSON(t *testing.T) { - expectedIPv4 := `{"client_port":"1000","ip":"81.2.69.192","ip_version":4,"country":"United Kingdom","country_code":"GB","city":"London","latitude":51.5142,"longitude":-0.0931,"postal_code":"","time_zone":"Europe/London","asn":0,"asn_organization":"","host":"test","headers":{"X-Real-Ip":["81.2.69.192"]}}` - expectedIPv6 := `{"asn":3352, "asn_organization":"TELEFONICA DE ESPANA", "city":"", "client_port":"1000", "country":"", "country_code":"", "headers":{"X-Real-Ip":["2a02:9000::1"]}, "host":"test", "ip":"2a02:9000::1", "ip_version":6, "latitude":0, "longitude":0, "postal_code":"", "time_zone":""}` + type args struct { + ip string + } + tests := []struct { + name string + args args + expected string + }{ + { + name: "IPv4", + args: args{ + ip: testIP.ipv4, + }, + expected: jsonIPv4, + }, + { + name: "IPv6", + args: args{ + ip: testIP.ipv6, + }, + expected: jsonIPv6, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", "/json", nil) + req.RemoteAddr = net.JoinHostPort(tt.args.ip, "1000") + req.Host = "test" + req.Header.Set(trustedHeader, tt.args.ip) - req, _ := http.NewRequest("GET", "/json", nil) - req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000") - req.Host = "test" - req.Header.Set("X-Real-IP", testIP.ipv4) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) - w := httptest.NewRecorder() - app.ServeHTTP(w, req) - - assert.Equal(t, 200, w.Code) - assert.Equal(t, contentType.json, w.Header().Get("Content-Type")) - assert.JSONEq(t, expectedIPv4, w.Body.String()) - - req.RemoteAddr = net.JoinHostPort(testIP.ipv6, "1000") - req.Host = "test" - req.Header.Set("X-Real-IP", testIP.ipv6) - - w = httptest.NewRecorder() - app.ServeHTTP(w, req) - - assert.Equal(t, 200, w.Code) - assert.Equal(t, contentType.json, w.Header().Get("Content-Type")) - assert.JSONEq(t, expectedIPv6, w.Body.String()) + assert.Equal(t, 200, w.Code) + assert.Equal(t, contentType.json, w.Header().Get("Content-Type")) + assert.JSONEq(t, tt.expected, w.Body.String()) + }) + } } func TestAll(t *testing.T) { @@ -120,7 +182,7 @@ X-Real-Ip: 81.2.69.192 req, _ := http.NewRequest("GET", "/all", nil) req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000") req.Host = "test" - req.Header.Set("X-Real-IP", testIP.ipv4) + req.Header.Set(trustedHeader, testIP.ipv4) req.Header.Set("Header1", "one") w := httptest.NewRecorder() diff --git a/router/setup_test.go b/router/setup_test.go index a6a4411..fe9720e 100644 --- a/router/setup_test.go +++ b/router/setup_test.go @@ -16,6 +16,7 @@ type testIPs struct { } type contentTypes struct { + html string text string json string } @@ -29,9 +30,12 @@ var ( ipv6ASN: "2a02:a800::1", } contentType = contentTypes{ + html: "content-type: text/html; charset=utf-8", text: "text/plain; charset=utf-8", json: "application/json; charset=utf-8", } + jsonIPv4 = `{"client_port":"1000","ip":"81.2.69.192","ip_version":4,"country":"United Kingdom","country_code":"GB","city":"London","latitude":51.5142,"longitude":-0.0931,"postal_code":"","time_zone":"Europe/London","asn":0,"asn_organization":"","host":"test","headers":{"X-Real-Ip":["81.2.69.192"]}}` + jsonIPv6 = `{"asn":3352, "asn_organization":"TELEFONICA DE ESPANA", "city":"", "client_port":"1000", "country":"", "country_code":"", "headers":{"X-Real-Ip":["2a02:9000::1"]}, "host":"test", "ip":"2a02:9000::1", "ip_version":6, "latitude":0, "longitude":0, "postal_code":"", "time_zone":""}` ) const trustedHeader = "X-Real-IP"