From e04b9e0cfa02711c97c599d1b7648b3a527f0587 Mon Sep 17 00:00:00 2001 From: "Michael Peters Jr." Date: Mon, 30 Mar 2026 23:09:21 -0700 Subject: [PATCH] fix: minor overhaul for ip checker page --- .claude/settings.json | 6 + go.mod | 4 + go.sum | 9 + internal/handlers/ip.go | 20 +- internal/handlers/ip_test.go | 159 ++++ internal/handlers/templates/ip.gotmpl.html | 868 ++++++++++++++++++--- 6 files changed, 955 insertions(+), 111 deletions(-) create mode 100644 .claude/settings.json create mode 100644 internal/handlers/ip_test.go diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..81aa16a --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,6 @@ +{ + "enabledPlugins": { + "frontend-design@claude-plugins-official": true, + "playwright@claude-plugins-official": true + } +} diff --git a/go.mod b/go.mod index af6022e..0a7a568 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/caarlos0/env/v11 v11.3.1 github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 + github.com/stretchr/testify v1.10.0 go.opentelemetry.io/contrib/instrumentation/host v0.59.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 go.opentelemetry.io/contrib/instrumentation/runtime v0.59.0 @@ -18,6 +19,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/ebitengine/purego v0.8.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -28,6 +30,7 @@ require ( github.com/klauspost/compress v1.17.11 // indirect github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect @@ -56,4 +59,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect google.golang.org/grpc v1.70.0 // indirect google.golang.org/protobuf v1.36.4 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 29da751..c98dddb 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,10 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0 h1:VD1gqscl4nYs1YxVuSdemTrSgTK github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0/go.mod h1:4EgsQoS4TOhJizV+JTFg40qx1Ofh3XmXEQNBpgvNT40= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= @@ -55,6 +59,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/shirou/gopsutil/v4 v4.24.12 h1:qvePBOk20e0IKA1QXrIIU+jmk+zEiYVVx06WjBRlZo4= github.com/shirou/gopsutil/v4 v4.24.12/go.mod h1:DCtMPAad2XceTeIAbGyVfycbYQNBGk2P8cvDi7/VN9o= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= @@ -114,5 +120,8 @@ google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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/handlers/ip.go b/internal/handlers/ip.go index 7a521ac..c4d83ac 100644 --- a/internal/handlers/ip.go +++ b/internal/handlers/ip.go @@ -25,9 +25,25 @@ func init() { }) } +// clientIP extracts the real client IP from the request, checking +// proxy headers before falling back to the direct connection address. +func clientIP(r *http.Request) string { + if ip := r.Header.Get("X-Real-Ip"); ip != "" { + return strings.TrimSpace(ip) + } + if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { + // The first entry is the original client IP. + if ip, _, _ := strings.Cut(fwd, ","); ip != "" { + return strings.TrimSpace(ip) + } + } + host, _, _ := strings.Cut(r.RemoteAddr, ":") + return host +} + func RawIPHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ip := strings.Split(r.RemoteAddr, ":")[0] + ip := clientIP(r) w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Incoming-IP", ip) @@ -37,7 +53,7 @@ func RawIPHandler() http.HandlerFunc { func IPHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ip := strings.Split(r.RemoteAddr, ":")[0] + ip := clientIP(r) w.Header().Set("X-Incoming-IP", ip) diff --git a/internal/handlers/ip_test.go b/internal/handlers/ip_test.go new file mode 100644 index 0000000..3bdb383 --- /dev/null +++ b/internal/handlers/ip_test.go @@ -0,0 +1,159 @@ +package handlers + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClientIP(t *testing.T) { + tests := []struct { + name string + remoteAddr string + headers map[string]string + wantIP string + }{ + { + name: "remote addr only", + remoteAddr: "203.0.113.1:12345", + wantIP: "203.0.113.1", + }, + { + name: "x-real-ip takes priority", + remoteAddr: "10.0.0.1:12345", + headers: map[string]string{ + "X-Real-Ip": "203.0.113.50", + "X-Forwarded-For": "198.51.100.1", + }, + wantIP: "203.0.113.50", + }, + { + name: "x-forwarded-for single entry", + remoteAddr: "10.0.0.1:12345", + headers: map[string]string{ + "X-Forwarded-For": "203.0.113.10", + }, + wantIP: "203.0.113.10", + }, + { + name: "x-forwarded-for multiple entries uses first", + remoteAddr: "10.0.0.1:12345", + headers: map[string]string{ + "X-Forwarded-For": "203.0.113.10, 10.0.0.2, 10.0.0.3", + }, + wantIP: "203.0.113.10", + }, + { + name: "x-forwarded-for with whitespace", + remoteAddr: "10.0.0.1:12345", + headers: map[string]string{ + "X-Forwarded-For": " 203.0.113.10 , 10.0.0.2", + }, + wantIP: "203.0.113.10", + }, + { + name: "x-real-ip with whitespace", + remoteAddr: "10.0.0.1:12345", + headers: map[string]string{ + "X-Real-Ip": " 203.0.113.50 ", + }, + wantIP: "203.0.113.50", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.RemoteAddr = tt.remoteAddr + for k, v := range tt.headers { + r.Header.Set(k, v) + } + + got := clientIP(r) + assert.Equal(t, tt.wantIP, got) + }) + } +} + +func TestRawIPHandler(t *testing.T) { + tests := []struct { + name string + remoteAddr string + headers map[string]string + wantIP string + }{ + { + name: "returns ip as plain text", + remoteAddr: "203.0.113.1:12345", + wantIP: "203.0.113.1", + }, + { + name: "respects x-forwarded-for", + remoteAddr: "10.0.0.1:12345", + headers: map[string]string{ + "X-Forwarded-For": "203.0.113.10", + }, + wantIP: "203.0.113.10", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.RemoteAddr = tt.remoteAddr + for k, v := range tt.headers { + r.Header.Set(k, v) + } + w := httptest.NewRecorder() + + RawIPHandler().ServeHTTP(w, r) + + assert.Equal(t, tt.wantIP, w.Body.String()) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + assert.Equal(t, tt.wantIP, w.Header().Get("X-Incoming-IP")) + }) + } +} + +func TestIPHandler(t *testing.T) { + tests := []struct { + name string + remoteAddr string + headers map[string]string + wantIP string + }{ + { + name: "renders template with remote addr", + remoteAddr: "203.0.113.1:12345", + wantIP: "203.0.113.1", + }, + { + name: "renders template with x-real-ip", + remoteAddr: "10.0.0.1:12345", + headers: map[string]string{ + "X-Real-Ip": "203.0.113.50", + }, + wantIP: "203.0.113.50", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.RemoteAddr = tt.remoteAddr + for k, v := range tt.headers { + r.Header.Set(k, v) + } + w := httptest.NewRecorder() + + IPHandler().ServeHTTP(w, r) + + require.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, tt.wantIP, w.Header().Get("X-Incoming-IP")) + assert.Contains(t, w.Body.String(), tt.wantIP) + }) + } +} diff --git a/internal/handlers/templates/ip.gotmpl.html b/internal/handlers/templates/ip.gotmpl.html index 2fa8138..e9f0ef9 100644 --- a/internal/handlers/templates/ip.gotmpl.html +++ b/internal/handlers/templates/ip.gotmpl.html @@ -1,198 +1,848 @@ - + - ip by alpineworks.io + + + ip — alpineworks.io + + + + + + + +
+
+
+
+ + +
-
-
copied!
-
{{.IP}}
+
+
copied
+
your ip address
+
{{.IP}}
+
+ + + + + click to copy +
-
headers
+
copied
+
+
+ + + + Request Headers +
+
+
{{range $key, $value := .Headers}}
-
{{$key}}:
+
{{$key}}
{{$value}}
{{end}}
- + +