diff --git a/acmetxt.go b/acmetxt.go index e65c842..ad3ab82 100644 --- a/acmetxt.go +++ b/acmetxt.go @@ -57,6 +57,17 @@ func (a ACMETxt) allowedFrom(ip string) bool { return false } +// Go through list (most likely from headers) to check for the IP. +// Reason for this is that some setups use reverse proxy in front of acme-dns +func (a ACMETxt) allowedFromList(ips []string) bool { + for _, v := range ips { + if a.allowedFrom(v) { + return true + } + } + return false +} + func newACMETxt() ACMETxt { var a = ACMETxt{} password := generatePassword(40) diff --git a/api.go b/api.go index dfb9e2d..1b6d07c 100644 --- a/api.go +++ b/api.go @@ -9,6 +9,7 @@ import ( // Serve is an authentication middlware function used to authenticate update requests func (a authMiddleware) Serve(ctx *iris.Context) { + allowUpdate := false usernameStr := ctx.RequestHeader("X-Api-User") password := ctx.RequestHeader("X-Api-Key") postData := ACMETxt{} @@ -23,7 +24,16 @@ func (a authMiddleware) Serve(ctx *iris.Context) { } else { if correctPassword(password, au.Password) { // Password ok - if au.allowedFrom(ctx.RequestIP()) { + + // Now test for the possibly limited ranges + if DNSConf.API.UseHeader { + ips := getIPListFromHeader(ctx.RequestHeader(DNSConf.API.HeaderName)) + allowUpdate = au.allowedFromList(ips) + } else { + allowUpdate = au.allowedFrom(ctx.RequestIP()) + } + + if allowUpdate { // Update is allowed from remote addr if err := ctx.ReadJSON(&postData); err == nil { if au.Subdomain == postData.Subdomain { diff --git a/api_test.go b/api_test.go index 87c7e2a..454edf5 100644 --- a/api_test.go +++ b/api_test.go @@ -19,6 +19,8 @@ func setupIris(t *testing.T, debug bool, noauth bool) *httpexpect.Expect { Port: "8080", TLS: "none", CorsOrigins: []string{"*"}, + UseHeader: false, + HeaderName: "X-Forwarded-For", } var dnscfg = DNSConfig{ API: httpapicfg, @@ -206,3 +208,48 @@ func TestApiManyUpdateWithCredentials(t *testing.T) { Status(test.status) } } + +func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) { + + updateJSON := map[string]interface{}{ + "subdomain": "", + "txt": ""} + + e := setupIris(t, false, false) + // Use header checks from default header (X-Forwarded-For) + DNSConf.API.UseHeader = true + // User without defined CIDR masks + newUser, err := DB.Register(cidrslice{}) + if err != nil { + t.Errorf("Could not create new user, got error [%v]", err) + } + + newUserWithCIDR, err := DB.Register(cidrslice{"192.168.1.2/32", "invalid"}) + if err != nil { + t.Errorf("Could not create new user with CIDR, got error [%v]", err) + } + + for _, test := range []struct { + user ACMETxt + headerValue string + status int + }{ + {newUser, "whatever goes", 200}, + {newUser, "10.0.0.1, 1.2.3.4 ,3.4.5.6", 200}, + {newUserWithCIDR, "127.0.0.1", 401}, + {newUserWithCIDR, "10.0.0.1, 10.0.0.2, 192.168.1.3", 401}, + {newUserWithCIDR, "10.1.1.1 ,192.168.1.2, 8.8.8.8", 200}, + } { + updateJSON = map[string]interface{}{ + "subdomain": test.user.Subdomain, + "txt": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} + e.POST("/update"). + WithJSON(updateJSON). + WithHeader("X-Api-User", test.user.Username.String()). + WithHeader("X-Api-Key", test.user.Password). + WithHeader("X-Forwarded-For", test.headerValue). + Expect(). + Status(test.status) + } + DNSConf.API.UseHeader = false +} diff --git a/config.cfg b/config.cfg index b0d593f..dbe82f9 100644 --- a/config.cfg +++ b/config.cfg @@ -44,6 +44,10 @@ tls_cert_fullchain = "/etc/tls/example.org/fullchain.pem" corsorigins = [ "*" ] +# use HTTP header to get the client ip +use_header = false +# header name to pull the ip address / list of ip addresses from +header_name = "X-Forwarded-For" [logconfig] # logging level: "error", "warning", "info" or "debug" diff --git a/main_test.go b/main_test.go index 980d0d1..c417816 100644 --- a/main_test.go +++ b/main_test.go @@ -68,6 +68,8 @@ func setupConfig() { Port: "8080", TLS: "none", CorsOrigins: []string{"*"}, + UseHeader: false, + HeaderName: "X-Forwarded-For", } var dnscfg = DNSConfig{ diff --git a/types.go b/types.go index 0dc3c79..0a41a3e 100644 --- a/types.go +++ b/types.go @@ -56,6 +56,8 @@ type httpapi struct { TLSCertPrivkey string `toml:"tls_cert_privkey"` TLSCertFullchain string `toml:"tls_cert_fullchain"` CorsOrigins []string + UseHeader bool `toml:"use_header"` + HeaderName string `toml:"header_name"` } // Logging config diff --git a/util.go b/util.go index e3d76c0..44a5f3e 100644 --- a/util.go +++ b/util.go @@ -2,12 +2,13 @@ package main import ( "crypto/rand" - "github.com/BurntSushi/toml" - log "github.com/Sirupsen/logrus" - "github.com/miekg/dns" "math/big" "regexp" "strings" + + "github.com/BurntSushi/toml" + log "github.com/Sirupsen/logrus" + "github.com/miekg/dns" ) func readConfig(fname string) DNSConfig { @@ -68,3 +69,14 @@ func startDNS(listen string, proto string) *dns.Server { go server.ListenAndServe() return server } + +func getIPListFromHeader(header string) []string { + iplist := []string{} + for _, v := range strings.Split(header, ",") { + if len(v) > 0 { + // Ignore empty values + iplist = append(iplist, strings.TrimSpace(v)) + } + } + return iplist +} diff --git a/util_test.go b/util_test.go index 3d8d9fd..621918c 100644 --- a/util_test.go +++ b/util_test.go @@ -71,3 +71,27 @@ func TestReadConfig(t *testing.T) { } } } + +func TestGetIPListFromHeader(t *testing.T) { + for i, test := range []struct { + input string + output []string + }{ + {"1.1.1.1, 2.2.2.2", []string{"1.1.1.1", "2.2.2.2"}}, + {" 1.1.1.1 , 2.2.2.2", []string{"1.1.1.1", "2.2.2.2"}}, + {",1.1.1.1 ,2.2.2.2", []string{"1.1.1.1", "2.2.2.2"}}, + } { + res := getIPListFromHeader(test.input) + if len(res) != len(test.output) { + t.Errorf("Test %d: Expected [%d] items in return list, but got [%d]", i, len(test.output), len(res)) + } else { + + for j, vv := range test.output { + if res[j] != vv { + t.Errorf("Test %d: Expected return value [%v] but got [%v]", j, test.output, res) + } + + } + } + } +}