Skip to content

Commit

Permalink
feat(ui): return the client address (ip and port)
Browse files Browse the repository at this point in the history
  • Loading branch information
qdm12 committed Nov 19, 2024
1 parent ee6fe6f commit d6c661d
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 29 deletions.
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<body>
Listening address: {{ .ListeningAddress }}
<br>
Client IP: {{ .ClientIP }}
Client address: {{ .ClientAddress }}
<br>
Browser: {{ .Browser }}
<br>
Expand Down
12 changes: 6 additions & 6 deletions internal/clientip/clientip.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (

var ErrRequestIsNil = errors.New("request is nil")

func ParseHTTPRequest(request *http.Request) (ip netip.Addr, err error) {
func ParseHTTPRequest(request *http.Request) (addrPort netip.AddrPort, err error) {
if request == nil {
return ip, fmt.Errorf("%w", ErrRequestIsNil)
return addrPort, fmt.Errorf("%w", ErrRequestIsNil)
}

remoteAddress := removeSpaces(request.RemoteAddr)
Expand All @@ -24,7 +24,7 @@ func ParseHTTPRequest(request *http.Request) (ip netip.Addr, err error) {

// No header so it can only be remoteAddress
if xRealIP == "" && len(xForwardedFor) == 0 {
return getIPFromHostPort(remoteAddress)
return addrStringToAddrPort(remoteAddress)
}

// remoteAddress is the last proxy server forwarding the traffic
Expand All @@ -33,17 +33,17 @@ func ParseHTTPRequest(request *http.Request) (ip netip.Addr, err error) {
publicXForwardedIPs := extractPublicIPs(xForwardedIPs)
if len(publicXForwardedIPs) > 0 {
// first public XForwardedIP should be the client IP
return publicXForwardedIPs[0], nil
return netip.AddrPortFrom(publicXForwardedIPs[0], 0), nil
}

// If all forwarded IP addresses are private we use the x-real-ip
// address if it exists
if xRealIP != "" {
return getIPFromHostPort(xRealIP)
return addrStringToAddrPort(xRealIP)
}

// Client IP is the first private IP address in the chain
return xForwardedIPs[0], nil
return netip.AddrPortFrom(xForwardedIPs[0], 0), nil
}

func removeSpaces(header string) string {
Expand Down
24 changes: 12 additions & 12 deletions internal/clientip/clientip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func Test_ParseHTTPRequest(t *testing.T) {

testCases := map[string]struct {
request *http.Request
ip netip.Addr
addrPort netip.AddrPort
errWrapped error
errMessage string
}{
Expand All @@ -35,13 +35,13 @@ func Test_ParseHTTPRequest(t *testing.T) {
request: &http.Request{
RemoteAddr: "99.99.99.99",
},
ip: netip.AddrFrom4([4]byte{99, 99, 99, 99}),
addrPort: netip.AddrPortFrom(netip.AddrFrom4([4]byte{99, 99, 99, 99}), 0),
},
"request with remote address": {
request: &http.Request{
RemoteAddr: "99.99.99.99",
},
ip: netip.AddrFrom4([4]byte{99, 99, 99, 99}),
addrPort: netip.AddrPortFrom(netip.AddrFrom4([4]byte{99, 99, 99, 99}), 0),
},
"request with xRealIP header": {
request: &http.Request{
Expand All @@ -50,7 +50,7 @@ func Test_ParseHTTPRequest(t *testing.T) {
"X-Real-IP": {"88.88.88.88"},
}),
},
ip: netip.AddrFrom4([4]byte{88, 88, 88, 88}),
addrPort: netip.AddrPortFrom(netip.AddrFrom4([4]byte{88, 88, 88, 88}), 0),
},
"request with xRealIP header and public XForwardedFor IP": {
request: &http.Request{
Expand All @@ -60,7 +60,7 @@ func Test_ParseHTTPRequest(t *testing.T) {
"X-Forwarded-For": {"88.88.88.88"},
}),
},
ip: netip.AddrFrom4([4]byte{88, 88, 88, 88}),
addrPort: netip.AddrPortFrom(netip.AddrFrom4([4]byte{88, 88, 88, 88}), 0),
},
"request with xRealIP header and private XForwardedFor IP": {
request: &http.Request{
Expand All @@ -70,7 +70,7 @@ func Test_ParseHTTPRequest(t *testing.T) {
"X-Forwarded-For": {"10.0.0.5"},
}),
},
ip: netip.AddrFrom4([4]byte{88, 88, 88, 88}),
addrPort: netip.AddrPortFrom(netip.AddrFrom4([4]byte{88, 88, 88, 88}), 0),
},
"request with single public IP in xForwardedFor header": {
request: &http.Request{
Expand All @@ -79,7 +79,7 @@ func Test_ParseHTTPRequest(t *testing.T) {
"X-Forwarded-For": {"88.88.88.88"},
}),
},
ip: netip.AddrFrom4([4]byte{88, 88, 88, 88}),
addrPort: netip.AddrPortFrom(netip.AddrFrom4([4]byte{88, 88, 88, 88}), 0),
},
"request with two public IPs in xForwardedFor header": {
request: &http.Request{
Expand All @@ -88,7 +88,7 @@ func Test_ParseHTTPRequest(t *testing.T) {
"X-Forwarded-For": {"88.88.88.88", "77.77.77.77"},
}),
},
ip: netip.AddrFrom4([4]byte{88, 88, 88, 88}),
addrPort: netip.AddrPortFrom(netip.AddrFrom4([4]byte{88, 88, 88, 88}), 0),
},
"request with private and public IPs in xForwardedFor header": {
request: &http.Request{
Expand All @@ -97,7 +97,7 @@ func Test_ParseHTTPRequest(t *testing.T) {
"X-Forwarded-For": {"192.168.1.5", "88.88.88.88", "10.0.0.1", "77.77.77.77"},
}),
},
ip: netip.AddrFrom4([4]byte{88, 88, 88, 88}),
addrPort: netip.AddrPortFrom(netip.AddrFrom4([4]byte{88, 88, 88, 88}), 0),
},
"request with single private IP in xForwardedFor header": {
request: &http.Request{
Expand All @@ -106,7 +106,7 @@ func Test_ParseHTTPRequest(t *testing.T) {
"X-Forwarded-For": {"192.168.1.5"},
}),
},
ip: netip.AddrFrom4([4]byte{192, 168, 1, 5}),
addrPort: netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 1, 5}), 0),
},
"request with private IPs in xForwardedFor header": {
request: &http.Request{
Expand All @@ -115,14 +115,14 @@ func Test_ParseHTTPRequest(t *testing.T) {
"X-Forwarded-For": {"192.168.1.5", "10.0.0.17"},
}),
},
ip: netip.AddrFrom4([4]byte{192, 168, 1, 5}),
addrPort: netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 1, 5}), 0),
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ip, err := ParseHTTPRequest(testCase.request)
assert.Equal(t, testCase.ip, ip)
assert.Equal(t, testCase.addrPort, ip)
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
Expand Down
36 changes: 32 additions & 4 deletions internal/clientip/split.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
package clientip

import (
"errors"
"fmt"
"math"
"net"
"net/netip"
"strconv"
"strings"
)

func getIPFromHostPort(address string) (ip netip.Addr, err error) {
// address can be in the form ipv4:port, ipv6:port, ipv4 or ipv6
host, _, err := splitHostPort(address)
func addrStringToAddrPort(address string) (addrPort netip.AddrPort, err error) {
// address can be in the form ipv4:portStr, ipv6:portStr, ipv4 or ipv6
host, portStr, err := splitHostPort(address)
if err != nil {
host = address
}
return netip.ParseAddr(host)
ip, err := netip.ParseAddr(host)
if err != nil {
return addrPort, fmt.Errorf("parsing IP address: %w", err)
}
var port uint16
if portStr != "" {
port, err = parsePort(portStr)
if err != nil {
return addrPort, fmt.Errorf("parsing port: %w", err)
}
}
return netip.AddrPortFrom(ip, port), nil
}

func splitHostPort(address string) (ip, port string, err error) {
Expand All @@ -35,3 +50,16 @@ func splitHostPort(address string) (ip, port string, err error) {
// IPv4 address
return net.SplitHostPort(address)
}

var ErrPortOutOfRange = errors.New("port is out of range")

func parsePort(s string) (port uint16, err error) {
const base, bitSize = 10, 16
portUint, err := strconv.ParseUint(s, base, bitSize)
if err != nil {
return 0, fmt.Errorf("parsing port: %w", err)
} else if portUint > math.MaxUint16 {
return 0, fmt.Errorf("%w: %d", ErrPortOutOfRange, portUint)
}
return uint16(portUint), nil
}
19 changes: 13 additions & 6 deletions internal/server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,26 +52,33 @@ func (h *handlers) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

browser, device, os := getUserAgentDetails(r.Header.Get("User-Agent"))
ip, err := clientip.ParseHTTPRequest(r)
addrPort, err := clientip.ParseHTTPRequest(r)
if err != nil {
h.logger.Errorf("cannot parse IP address: %s", err)
http.Error(w, "cannot parse IP address", http.StatusInternalServerError)
h.logger.Errorf("cannot parse ip address port: %s", err)
http.Error(w, "cannot parse ip address port", http.StatusInternalServerError)
return
}

var clientAddress string
if addrPort.Port() == 0 {
clientAddress = addrPort.Addr().String()
} else {
clientAddress = addrPort.String()
}

h.logger.Infof("received request from IP %s (device: %s | %s | %s)",
ip, device, os, browser,
clientAddress, device, os, browser,
)

htmlData := struct {
ListeningAddress string
ClientIP string
ClientAddress string
Browser string
Device string
OS string
}{
ListeningAddress: h.listeningAddress,
ClientIP: ip.String(),
ClientAddress: clientAddress,
Browser: browser,
Device: device,
OS: os,
Expand Down

0 comments on commit d6c661d

Please sign in to comment.