Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow getting IP address from "X-Real-Ip", "X-Forwarded-For" headers #45

Merged
merged 7 commits into from
Feb 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM debian:stretch as builder
FROM debian:buster as builder
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -q -y golang git-core && \
apt-get clean
Expand All @@ -8,7 +8,7 @@ RUN mkdir -p /root/go/src
COPY rest-api /root/go/src/dyndns
RUN cd /root/go/src/dyndns && go get && go test -v

FROM debian:stretch
FROM debian:buster-slim
MAINTAINER David Prandzioch <[email protected]>

RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
Expand Down
91 changes: 88 additions & 3 deletions rest-api/request_handler.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"bytes"
"errors"
"fmt"
"log"
"net"
Expand Down Expand Up @@ -56,7 +58,13 @@ func BuildWebserviceResponseFromRequest(r *http.Request, appConfig *Config, extr
} else if ipparser.ValidIP6(response.Address) {
response.AddrType = "AAAA"
} else {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
var ip string
var err error

ip, err = getUserIP(r)
if ip == "" {
ip, _, err = net.SplitHostPort(r.RemoteAddr)
}

if err != nil {
response.Success = false
Expand All @@ -68,19 +76,96 @@ func BuildWebserviceResponseFromRequest(r *http.Request, appConfig *Config, extr
// @todo refactor this code to remove duplication
if ipparser.ValidIP4(ip) {
response.AddrType = "A"
response.Address = ip
} else if ipparser.ValidIP6(ip) {
response.AddrType = "AAAA"
response.Address = ip
} else {
response.Success = false
response.Message = fmt.Sprintf("%s is neither a valid IPv4 nor IPv6 address", response.Address)
log.Println(fmt.Sprintf("Invalid address: %s", response.Address))
return response
}

response.Address = ip
}

response.Success = true

return response
}

func getUserIP(r *http.Request) (string, error) {
for _, h := range []string{"X-Real-Ip", "X-Forwarded-For"} {
addresses := strings.Split(r.Header.Get(h), ",")
// march from right to left until we get a public address
// that will be the address right before our proxy.
for i := len(addresses) - 1; i >= 0; i-- {
ip := strings.TrimSpace(addresses[i])
// header can contain spaces too, strip those out.
realIP := net.ParseIP(ip)
if !realIP.IsGlobalUnicast() || isPrivateSubnet(realIP) {
// bad address, go to next
continue
}
return ip, nil
}
}
return "", errors.New("no match")
}

//ipRange - a structure that holds the start and end of a range of ip addresses
type ipRange struct {
start net.IP
end net.IP
}

// inRange - check to see if a given ip address is within a range given
func inRange(r ipRange, ipAddress net.IP) bool {
// strcmp type byte comparison
if bytes.Compare(ipAddress, r.start) >= 0 && bytes.Compare(ipAddress, r.end) < 0 {
return true
}
return false
}

var privateRanges = []ipRange{
ipRange{
start: net.ParseIP("10.0.0.0"),
end: net.ParseIP("10.255.255.255"),
},
ipRange{
start: net.ParseIP("100.64.0.0"),
end: net.ParseIP("100.127.255.255"),
},
ipRange{
start: net.ParseIP("172.16.0.0"),
end: net.ParseIP("172.31.255.255"),
},
ipRange{
start: net.ParseIP("192.0.0.0"),
end: net.ParseIP("192.0.0.255"),
},
ipRange{
start: net.ParseIP("192.168.0.0"),
end: net.ParseIP("192.168.255.255"),
},
ipRange{
start: net.ParseIP("198.18.0.0"),
end: net.ParseIP("198.19.255.255"),
},
}

// isPrivateSubnet - check to see if this ip is in a private subnet
func isPrivateSubnet(ipAddress net.IP) bool {
// my use case is only concerned with ipv4 atm
if ipCheck := ipAddress.To4(); ipCheck != nil {
// iterate over all our ranges
for _, r := range privateRanges {
// check if this ip is in a private range
if inRange(r, ipAddress) {
return true
}
}
}
return false
}

50 changes: 50 additions & 0 deletions rest-api/request_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,56 @@ func TestBuildWebserviceResponseFromRequestToReturnValidObject(t *testing.T) {
}
}

func TestBuildWebserviceResponseFromRequestWithXRealIPHeaderToReturnValidObject(t *testing.T) {
var appConfig = &Config{}
appConfig.SharedSecret = "changeme"

req, _ := http.NewRequest("GET", "/update?secret=changeme&domain=foo", nil)
req.Header.Add("X-Real-Ip", "1.2.3.4")
result := BuildWebserviceResponseFromRequest(req, appConfig, defaultExtractor)

if result.Success != true {
t.Fatalf("Expected WebserviceResponse.Success to be true")
}

if result.Domain != "foo" {
t.Fatalf("Expected WebserviceResponse.Domain to be foo")
}

if result.Address != "1.2.3.4" {
t.Fatalf("Expected WebserviceResponse.Address to be 1.2.3.4")
}

if result.AddrType != "A" {
t.Fatalf("Expected WebserviceResponse.AddrType to be A")
}
}

func TestBuildWebserviceResponseFromRequestWithXForwardedForHeaderToReturnValidObject(t *testing.T) {
var appConfig = &Config{}
appConfig.SharedSecret = "changeme"

req, _ := http.NewRequest("GET", "/update?secret=changeme&domain=foo", nil)
req.Header.Add("X-Forwarded-For", "1.2.3.4")
result := BuildWebserviceResponseFromRequest(req, appConfig, defaultExtractor)

if result.Success != true {
t.Fatalf("Expected WebserviceResponse.Success to be true")
}

if result.Domain != "foo" {
t.Fatalf("Expected WebserviceResponse.Domain to be foo")
}

if result.Address != "1.2.3.4" {
t.Fatalf("Expected WebserviceResponse.Address to be 1.2.3.4")
}

if result.AddrType != "A" {
t.Fatalf("Expected WebserviceResponse.AddrType to be A")
}
}

func TestBuildWebserviceResponseFromRequestToReturnInvalidObjectWhenNoSecretIsGiven(t *testing.T) {
var appConfig = &Config{}
appConfig.SharedSecret = "changeme"
Expand Down