shiori/internal/webserver/utils_ip_test.go
Tomi lla dde1b44e77
fix(log): record user real ip from headers (#603)
* fix(real_ip): get user real ip from headers of request

* fix(real_ip): compatible with those header with multiple IP values separated by commas

* test(real_ip): add benchmark for IPv4 and IPv6 private address check

* fix(real_ip): check empty, then remove leading and tailing comma char, finally locate first IP field

* test(real_ip): move checker logic into utils and add more unit test cases

* test(real_ip): write unit tests covering all code branches of the `util-ip` module

* refactor(real_ip): use one-line `testify.assert.Panics` to capture intended panic in test case

* chore(real_ip): add module private variable `UserRealIpHeaderCandidates`

put those headers together, make it easy to manage in one place

* doc(real_ip): write docstring for each function in the `utils-ip` module

* chore(real_ip): choose more concrete and unambiguous name for test helper function

It is to avoid polluting the module name-space with too general names.

* chore(naming): change function names according to code style

* refactor(real_ip): simplify the code indicated by 'gosimple' and `golangci`

* chore(naming): rename the `utils-ip` file to `utils_ip` to match with the rest of the file structure

---------

Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com>
2023-06-11 21:25:23 +02:00

229 lines
7.2 KiB
Go

package webserver
import (
"fmt"
"math/rand"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseCidr(t *testing.T) {
res := parseCIDR("192.168.0.0/16", "internal 192.168.x.x")
assert.Equal(t, res.IP, net.IP([]byte{192, 168, 0, 0}))
assert.Equal(t, res.Mask, net.IPMask([]byte{255, 255, 0, 0}))
}
func TestParseCidrInvalidAddr(t *testing.T) {
assert.Panics(t, func() { parseCIDR("192.168.0.0/34", "internal 192.168.x.x") })
}
func TestIsPrivateIP(t *testing.T) {
assert.True(t, IsPrivateIP(net.ParseIP("127.0.0.1")), "should be private")
assert.True(t, IsPrivateIP(net.ParseIP("192.168.254.254")), "should be private")
assert.True(t, IsPrivateIP(net.ParseIP("10.255.0.3")), "should be private")
assert.True(t, IsPrivateIP(net.ParseIP("172.16.255.255")), "should be private")
assert.True(t, IsPrivateIP(net.ParseIP("172.31.255.255")), "should be private")
assert.True(t, !IsPrivateIP(net.ParseIP("128.0.0.1")), "should be private")
assert.True(t, !IsPrivateIP(net.ParseIP("192.169.255.255")), "should not be private")
assert.True(t, !IsPrivateIP(net.ParseIP("9.255.0.255")), "should not be private")
assert.True(t, !IsPrivateIP(net.ParseIP("172.32.255.255")), "should not be private")
assert.True(t, IsPrivateIP(net.ParseIP("::0")), "should be private")
assert.True(t, IsPrivateIP(net.ParseIP("::1")), "should be private")
assert.True(t, !IsPrivateIP(net.ParseIP("::2")), "should not be private")
assert.True(t, IsPrivateIP(net.ParseIP("fe80::1")), "should be private")
assert.True(t, IsPrivateIP(net.ParseIP("febf::1")), "should be private")
assert.True(t, !IsPrivateIP(net.ParseIP("fec0::1")), "should not be private")
assert.True(t, !IsPrivateIP(net.ParseIP("feff::1")), "should not be private")
assert.True(t, IsPrivateIP(net.ParseIP("ff00::1")), "should be private")
assert.True(t, IsPrivateIP(net.ParseIP("ff10::1")), "should be private")
assert.True(t, IsPrivateIP(net.ParseIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), "should be private")
assert.True(t, IsPrivateIP(net.ParseIP("2002::")), "should be private")
assert.True(t, IsPrivateIP(net.ParseIP("2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), "should be private")
assert.True(t, IsPrivateIP(net.ParseIP("0100::")), "should be private")
assert.True(t, IsPrivateIP(net.ParseIP("0100::0000:ffff:ffff:ffff:ffff")), "should be private")
assert.True(t, !IsPrivateIP(net.ParseIP("0100::0001:0000:0000:0000:0000")), "should be private")
}
func TestIsIpValidAndPublic(t *testing.T) {
// test empty address
assert.False(t, IsIPValidAndPublic(""))
// test public address
assert.True(t, IsIPValidAndPublic("31.41.244.124"))
assert.True(t, IsIPValidAndPublic("62.233.50.248"))
// trim head or tail space
assert.True(t, IsIPValidAndPublic(" 62.233.50.249"))
assert.True(t, IsIPValidAndPublic(" 62.233.50.250 "))
assert.True(t, IsIPValidAndPublic("62.233.50.251 "))
// test private address
assert.False(t, IsIPValidAndPublic("10.1.123.52"))
assert.False(t, IsIPValidAndPublic("192.168.123.24"))
assert.False(t, IsIPValidAndPublic("172.17.0.1"))
}
func BenchmarkIsPrivateIPv4(b *testing.B) {
// range: 2-254
n1 := 2 + rand.Intn(252)
n2 := 2 + rand.Intn(252)
for i := 0; i < b.N; i++ {
IsPrivateIP(net.ParseIP(fmt.Sprintf("192.168.%d.%d", n1, n2)))
}
}
func BenchmarkIsPrivateIPv6(b *testing.B) {
n1 := 2 + rand.Intn(252)
for i := 0; i < b.N; i++ {
IsPrivateIP(net.ParseIP(fmt.Sprintf("2002::%d", n1)))
}
}
func testIsPublicHttpRequestAddressHelper(
t *testing.T, wantIP string, headers map[string]string, isPublic bool,
) {
testIsPublicHttpRequestAddressHelperWrapped(t, nil, wantIP, headers, isPublic)
}
func testIsPublicHttpRequestAddressHelperWrapped(
t *testing.T, r *http.Request, wantIP string, headers map[string]string, isPublic bool,
) {
var (
err error
userIP string
)
if r == nil {
r = httptest.NewRequest("GET", "/", nil)
}
for k, v := range headers {
r.Header.Set(k, v)
}
origVal := GetUserRealIP(r)
if strings.Contains(origVal, ":") {
userIP, _, err = net.SplitHostPort(origVal)
if err != nil {
t.Error(err)
}
} else {
userIP = origVal
}
if isPublic {
// should equal first ip in list
assert.Equal(t, wantIP, userIP)
assert.True(t, IsIPValidAndPublic(userIP))
} else {
assert.Equal(t, origVal, r.RemoteAddr)
assert.False(t, IsIPValidAndPublic(userIP))
}
}
func TestGetUserRealIPWithSetRemoteAddr(t *testing.T) {
// Test Public RemoteAddr
testIsPublicHttpRequestAddressHelper(t, "", nil, false)
r := httptest.NewRequest("GET", "/", nil)
wantIP := "34.23.123.122"
r.RemoteAddr = fmt.Sprintf("%s:1234", wantIP)
testIsPublicHttpRequestAddressHelperWrapped(t, r, wantIP, nil, true)
}
func TestGetUserRealIPWithInvalidRemoteAddr(t *testing.T) {
// Test Public RemoteAddr
testIsPublicHttpRequestAddressHelper(t, "", nil, false)
r := httptest.NewRequest("GET", "/", nil)
wantIP := "34.23.123.122"
// without port
r.RemoteAddr = wantIP
testIsPublicHttpRequestAddressHelperWrapped(t, r, wantIP, nil, true)
}
func TestGetUserRealIPWithEmptyHeader(t *testing.T) {
// Test Empty X-Real-IP
testIsPublicHttpRequestAddressHelper(t, "", nil, false)
}
func TestGetUserRealIPWithInvalidHeaderValue(t *testing.T) {
for _, name := range userRealIpHeaderCandidates {
// invalid ip
m := map[string]string{
name: "31.41.24a.12",
}
testIsPublicHttpRequestAddressHelper(t, "", m, false)
}
}
func TestGetUserRealIPWithXRealIpHeader(t *testing.T) {
// Test public Real IP
for _, name := range userRealIpHeaderCandidates {
wantIP := "31.41.242.12"
m := map[string]string{
name: wantIP,
}
testIsPublicHttpRequestAddressHelper(t, wantIP, m, true)
}
}
func TestGetUserRealIPWithPrivateXRealIpHeader(t *testing.T) {
for _, name := range userRealIpHeaderCandidates {
wantIP := "192.168.123.123"
// test private ip in header
m := map[string]string{
name: wantIP,
}
testIsPublicHttpRequestAddressHelper(t, wantIP, m, false)
}
}
func TestGetUserRealIPWithXRealIpListHeader(t *testing.T) {
// Test Real IP List
for _, name := range userRealIpHeaderCandidates {
ipList := []string{"34.23.123.122", "34.23.123.123"}
// should equal first ip in list
wantIP := ipList[0]
// test private ip in header
m := map[string]string{
name: strings.Join(ipList, ", "),
}
testIsPublicHttpRequestAddressHelper(t, wantIP, m, true)
}
}
func TestGetUserRealIPWithXRealIpHeaderIgnoreComma(t *testing.T) {
// Test Real IP List with leading or tailing comma
wantIP := "34.23.123.124"
ipVariants := []string{
",34.23.123.124", " ,34.23.123.124", "\t,34.23.123.124",
",34.23.123.124,", " ,34.23.123.124, ", "\t,34.23.123.124,\t",
"34.23.123.124,", "34.23.123.124, ", "34.23.123.124,\t"}
for _, variant := range ipVariants {
for _, name := range userRealIpHeaderCandidates {
m := map[string]string{name: variant}
testIsPublicHttpRequestAddressHelper(t, wantIP, m, true)
}
}
}
func TestGetUserRealIPWithDifferentHeaderOrder(t *testing.T) {
var m map[string]string
wantIP := "34.23.123.124"
m = map[string]string{
"X-Real-Ip": "192.168.123.122",
"X-Forwarded-For": wantIP,
}
testIsPublicHttpRequestAddressHelper(t, wantIP, m, true)
m = map[string]string{
"X-Real-Ip": wantIP,
"X-Forwarded-For": "192.168.123.122",
}
testIsPublicHttpRequestAddressHelper(t, wantIP, m, true)
}