diff --git a/README.md b/README.md index 6e8a845..c528f71 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,24 @@ A simplified DNS server with a RESTful HTTP API to provide a simple way to autom Many DNS servers do not provide an API to enable automation for the ACME DNS challenges. Those which do, give the keys way too much power. Leaving the keys laying around your random boxes is too often a requirement to have a meaningful process automation. +Acme-dns provides a simple API exclusively for TXT record updates and should be used with ACME magic "\_acme-challenge" - subdomain CNAME records. This way, in the unfortunate exposure of API keys, the effetcs are limited to the subdomain TXT record in question. + So basically it boils down to **accessibility** and **security** ## Features - Simplified DNS server, serving your ACME DNS challenges (TXT) - Custom records (have your required A, AAAA, NS, etc. records served) - HTTP API automatically acquires and uses Let's Encrypt TLS certificate -- Simple deployment (it's Go after all) +- Limit /update API endpoint access to specific CIDR mask(s), defined in the /register request - Supports SQLite & PostgreSQL as DB backends +- Simple deployment (it's Go after all) ## Usage - -[![asciicast](https://asciinema.org/a/94462.png)](https://asciinema.org/a/94462) +[![asciicast](https://asciinema.org/a/94903.png)](https://asciinema.org/a/94903) Using acme-dns is a three-step process (provided you already have the self-hosted server set up, or are using a service like acme-dns.io): -- Get credentials and unique subdomain (simple GET request to https://auth.exmaple.org/register) +- Get credentials and unique subdomain (simple POST request to eg. https://auth.acme-dns.io/register) - Create a (ACME magic) CNAME record to your existing zone, pointing to the subdomain you got from the registration. (eg. `_acme-challenge.domainiwantcertfor.tld. CNAME a097455b-52cc-4569-90c8-7a4b97c6eba8.auth.example.org` ) - Use your credentials to POST a new DNS challenge values to an acme-dns server for the CA to validate them off of. - Crontab and forget. @@ -33,18 +35,31 @@ Using acme-dns is a three-step process (provided you already have the self-hoste ### Register endpoint The method returns a new unique subdomain and credentials needed to update your record. -Subdomain is where you can point your own `_acme-challenge` subdomain CNAME record to. -With the credentials, you can update the TXT response in the service to match the challenge token, later referred as ______my_43_char_dns_validation_token______, given out by the Certificate Authority. +Fulldomain is where you can point your own `_acme-challenge` subdomain CNAME record to. +With the credentials, you can update the TXT response in the service to match the challenge token, later referred as \_\_\_validation\_token\_recieved\_from\_the\_ca\_\_\_, given out by the Certificate Authority. -```GET /register``` +**Optional:**: You can POST JSON data to limit the /update requests to predefined source networks using CIDR notation. -#### Parameters +```POST /register``` + +#### OPTIONAL Example input +```json +{ + "allowfrom": [ + "192.168.100.1/24", + "1.2.3.4/32", + "2002:c0a8:2a00::0/40", +} +``` -None ```Status: 201 Created``` -``` +```json { + "allowfrom": [ + "192.168.100.1/24", + "1.2.3.4/32" + ], "fulldomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a.auth.acme-dns.io", "password": "htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z", "subdomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a", @@ -65,10 +80,10 @@ The method allows you to update the TXT answer contents of your unique subdomain | X-Api-Key | Password recieved from registration | `X-Api-Key: htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z` | #### Example input -``` +```json { "subdomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a", - "txt": "______my_43_char_dns_validation_token______" + "txt": "___validation_token_recieved_from_the_ca___", } ``` @@ -77,7 +92,7 @@ The method allows you to update the TXT answer contents of your unique subdomain ```Status: 200 OK``` ```json { - "txt": "______my_43_char_dns_validation_token______" + "txt": "___validation_token_recieved_from_the_ca___", } ``` @@ -90,7 +105,7 @@ Check out how in the INSTALL section. ## As a service Acme-dns instance is running as a service for everyone wanting to get on in fast. You can find it at `auth.acme-dns.io`, so to get started, try: -```curl -X GET https://auth.acme-dns.io/register``` +```curl -X POST https://auth.acme-dns.io/register``` ## Installation @@ -169,11 +184,15 @@ logtype = "stdout" # logfile = "./acme-dns.log" # format, either "json" or "text" logformat = "text" +# 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" ``` ## TODO -- Ability to define the CIDR mask in POST request to /register endpoint which is authorized to make /update requests with the created user-key-pair. +- Logging to a file - Want to see something implemented, make a feature request! ## Contributing diff --git a/acmetxt.go b/acmetxt.go new file mode 100644 index 0000000..ad3ab82 --- /dev/null +++ b/acmetxt.go @@ -0,0 +1,78 @@ +package main + +import ( + "encoding/json" + "net" + + "github.com/satori/go.uuid" +) + +// ACMETxt is the default structure for the user controlled record +type ACMETxt struct { + Username uuid.UUID + Password string + ACMETxtPost + LastActive int64 + AllowFrom cidrslice +} + +// ACMETxtPost holds the DNS part of the ACMETxt struct +type ACMETxtPost struct { + Subdomain string `json:"subdomain"` + Value string `json:"txt"` +} + +// cidrslice is a list of allowed cidr ranges +type cidrslice []string + +func (c *cidrslice) JSON() string { + ret, _ := json.Marshal(c.ValidEntries()) + return string(ret) +} + +func (c *cidrslice) ValidEntries() []string { + valid := []string{} + for _, v := range *c { + _, _, err := net.ParseCIDR(v) + if err == nil { + valid = append(valid, v) + } + } + return valid +} + +// Check if IP belongs to an allowed net +func (a ACMETxt) allowedFrom(ip string) bool { + remoteIP := net.ParseIP(ip) + // Range not limited + if len(a.AllowFrom.ValidEntries()) == 0 { + return true + } + for _, v := range a.AllowFrom.ValidEntries() { + _, vnet, _ := net.ParseCIDR(v) + if vnet.Contains(remoteIP) { + return true + } + } + 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) + a.Username = uuid.NewV4() + a.Password = password + a.Subdomain = uuid.NewV4().String() + return a +} diff --git a/api.go b/api.go index 7226ef9..7ed5b3b 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{} @@ -16,37 +17,58 @@ func (a authMiddleware) Serve(ctx *iris.Context) { username, err := getValidUsername(usernameStr) if err == nil && validKey(password) { au, err := DB.GetByUsername(username) - if err == nil && correctPassword(password, au.Password) { - // Password ok - if err := ctx.ReadJSON(&postData); err == nil { - // Check that the subdomain belongs to the user - if au.Subdomain == postData.Subdomain { - ctx.Next() - return + if err != nil { + log.WithFields(log.Fields{"error": err.Error()}).Error("Error while trying to get user") + // To protect against timed side channel (never gonna give you up) + correctPassword(password, "$2a$10$8JEFVNYYhLoBysjAxe2yBuXrkDojBQBkVpXEQgyQyjn43SvJ4vL36") + } else { + if correctPassword(password, au.Password) { + // Password ok + + // 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 { + ctx.Next() + return + } + } else { + // JSON error + ctx.JSON(iris.StatusBadRequest, iris.Map{"error": "bad data"}) + return + } } } else { - ctx.JSON(iris.StatusBadRequest, iris.Map{"error": "bad data"}) - return + // Wrong password + log.WithFields(log.Fields{"username": username}).Warning("Failed password check") } } - // To protect against timed side channel (never gonna give you up) - correctPassword(password, "$2a$10$8JEFVNYYhLoBysjAxe2yBuXrkDojBQBkVpXEQgyQyjn43SvJ4vL36") } ctx.JSON(iris.StatusUnauthorized, iris.Map{"error": "unauthorized"}) } func webRegisterPost(ctx *iris.Context) { - // Create new user - nu, err := DB.Register() var regJSON iris.Map var regStatus int + aTXT := ACMETxt{} + _ = ctx.ReadJSON(&aTXT) + // Create new user + nu, err := DB.Register(aTXT.AllowFrom) if err != nil { errstr := fmt.Sprintf("%v", err) regJSON = iris.Map{"error": errstr} regStatus = iris.StatusInternalServerError log.WithFields(log.Fields{"error": err.Error()}).Debug("Error in registration") } else { - regJSON = iris.Map{"username": nu.Username, "password": nu.Password, "fulldomain": nu.Subdomain + "." + DNSConf.General.Domain, "subdomain": nu.Subdomain} + regJSON = iris.Map{"username": nu.Username, "password": nu.Password, "fulldomain": nu.Subdomain + "." + DNSConf.General.Domain, "subdomain": nu.Subdomain, "allowfrom": nu.AllowFrom.ValidEntries()} regStatus = iris.StatusCreated log.WithFields(log.Fields{"user": nu.Username.String()}).Debug("Created new user") @@ -54,11 +76,6 @@ func webRegisterPost(ctx *iris.Context) { ctx.JSON(regStatus, regJSON) } -func webRegisterGet(ctx *iris.Context) { - // This is placeholder for now - webRegisterPost(ctx) -} - func webUpdatePost(ctx *iris.Context) { // User auth done in middleware a := ACMETxt{} diff --git a/api_test.go b/api_test.go index b067f14..99fe655 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, @@ -26,7 +28,6 @@ func setupIris(t *testing.T, debug bool, noauth bool) *httpexpect.Expect { } DNSConf = dnscfg var ForceAuth = authMiddleware{} - iris.Get("/register", webRegisterGet) iris.Post("/register", webRegisterPost) if noauth { iris.Post("/update", webUpdatePost) @@ -40,14 +41,6 @@ func setupIris(t *testing.T, debug bool, noauth bool) *httpexpect.Expect { func TestApiRegister(t *testing.T) { e := setupIris(t, false, false) - e.GET("/register").Expect(). - Status(iris.StatusCreated). - JSON().Object(). - ContainsKey("fulldomain"). - ContainsKey("subdomain"). - ContainsKey("username"). - ContainsKey("password"). - NotContainsKey("error") e.POST("/register").Expect(). Status(iris.StatusCreated). JSON().Object(). @@ -56,6 +49,26 @@ func TestApiRegister(t *testing.T) { ContainsKey("username"). ContainsKey("password"). NotContainsKey("error") + + allowfrom := map[string][]interface{}{ + "allowfrom": []interface{}{"123.123.123.123/32", + "1010.10.10.10/24", + "invalid"}, + } + + response := e.POST("/register"). + WithJSON(allowfrom). + Expect(). + Status(iris.StatusCreated). + JSON().Object(). + ContainsKey("fulldomain"). + ContainsKey("subdomain"). + ContainsKey("username"). + ContainsKey("password"). + ContainsKey("allowfrom"). + NotContainsKey("error") + + response.Value("allowfrom").Array().Elements("123.123.123.123/32") } func TestApiRegisterWithMockDB(t *testing.T) { @@ -66,7 +79,7 @@ func TestApiRegisterWithMockDB(t *testing.T) { defer db.Close() mock.ExpectBegin() mock.ExpectPrepare("INSERT INTO records").WillReturnError(errors.New("error")) - e.GET("/register").Expect(). + e.POST("/register").Expect(). Status(iris.StatusInternalServerError). JSON().Object(). ContainsKey("error") @@ -90,7 +103,7 @@ func TestApiUpdateWithCredentials(t *testing.T) { "txt": ""} e := setupIris(t, false, false) - newUser, err := DB.Register() + newUser, err := DB.Register(cidrslice{}) if err != nil { t.Errorf("Could not create new user, got error [%v]", err) } @@ -146,10 +159,25 @@ func TestApiManyUpdateWithCredentials(t *testing.T) { "txt": ""} e := setupIris(t, false, false) - newUser, err := DB.Register() + // User without defined CIDR masks + newUser, err := DB.Register(cidrslice{}) if err != nil { t.Errorf("Could not create new user, got error [%v]", err) } + + // User with defined allow from - CIDR masks, all invalid + // (httpexpect doesn't provide a way to mock remote ip) + newUserWithCIDR, err := DB.Register(cidrslice{"192.168.1.1/32", "invalid"}) + if err != nil { + t.Errorf("Could not create new user with CIDR, got error [%v]", err) + } + + // Another user with valid CIDR mask to match the httpexpect default + newUserWithValidCIDR, err := DB.Register(cidrslice{"0.0.0.0/32", "invalid"}) + if err != nil { + t.Errorf("Could not create new user with a valid CIDR, got error [%v]", err) + } + for _, test := range []struct { user string pass string @@ -164,6 +192,9 @@ func TestApiManyUpdateWithCredentials(t *testing.T) { {newUser.Username.String(), newUser.Password, newUser.Subdomain, "tooshortfortxt", 400}, {newUser.Username.String(), newUser.Password, newUser.Subdomain, 1234567890, 400}, {newUser.Username.String(), newUser.Password, newUser.Subdomain, validTxtData, 200}, + {newUserWithCIDR.Username.String(), newUserWithCIDR.Password, newUserWithCIDR.Subdomain, validTxtData, 401}, + {newUserWithValidCIDR.Username.String(), newUserWithValidCIDR.Password, newUserWithValidCIDR.Subdomain, validTxtData, 200}, + {newUser.Username.String(), "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", newUser.Subdomain, validTxtData, 401}, } { updateJSON = map[string]interface{}{ "subdomain": test.subdomain, @@ -176,3 +207,56 @@ 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) + } + + newUserWithIP6CIDR, err := DB.Register(cidrslice{"2002:c0a8::0/32"}) + if err != nil { + t.Errorf("Could not create a new user with IP6 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}, + {newUserWithIP6CIDR, "2002:c0a8:b4dc:0d3::0", 200}, + {newUserWithIP6CIDR, "2002:c0a7:0ff::0", 401}, + {newUserWithIP6CIDR, "2002:c0a8:d3ad:b33f:c0ff:33b4:dc0d:3b4d", 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 4bf5643..795ca15 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/db.go b/db.go index 868cf6e..3cac758 100644 --- a/db.go +++ b/db.go @@ -2,13 +2,16 @@ package main import ( "database/sql" + "encoding/json" "errors" + "regexp" + "time" + + log "github.com/Sirupsen/logrus" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" "github.com/satori/go.uuid" "golang.org/x/crypto/bcrypt" - "regexp" - "time" ) var recordsTable = ` @@ -43,10 +46,11 @@ func (d *acmedb) Init(engine string, connection string) error { return nil } -func (d *acmedb) Register() (ACMETxt, error) { +func (d *acmedb) Register(afrom cidrslice) (ACMETxt, error) { d.Lock() defer d.Unlock() a := newACMETxt() + a.AllowFrom = cidrslice(afrom.ValidEntries()) passwordHash, err := bcrypt.GenerateFromPassword([]byte(a.Password), 10) timenow := time.Now().Unix() regSQL := ` @@ -63,10 +67,11 @@ func (d *acmedb) Register() (ACMETxt, error) { } sm, err := d.DB.Prepare(regSQL) if err != nil { + log.WithFields(log.Fields{"error": err.Error()}).Error("Database error in prepare") return a, errors.New("SQL error") } defer sm.Close() - _, err = sm.Exec(a.Username.String(), passwordHash, a.Subdomain, timenow, a.AllowFrom) + _, err = sm.Exec(a.Username.String(), passwordHash, a.Subdomain, timenow, a.AllowFrom.JSON()) if err != nil { return a, err } @@ -173,13 +178,24 @@ func (d *acmedb) Update(a ACMETxt) error { func getModelFromRow(r *sql.Rows) (ACMETxt, error) { txt := ACMETxt{} + afrom := "" err := r.Scan( &txt.Username, &txt.Password, &txt.Subdomain, &txt.Value, &txt.LastActive, - &txt.AllowFrom) + &afrom) + if err != nil { + log.WithFields(log.Fields{"error": err.Error()}).Error("Row scan error") + } + + cslice := cidrslice{} + err = json.Unmarshal([]byte(afrom), &cslice) + if err != nil { + log.WithFields(log.Fields{"error": err.Error()}).Error("JSON unmarshall error") + } + txt.AllowFrom = cslice return txt, err } diff --git a/db_test.go b/db_test.go index 665bed3..4580a34 100644 --- a/db_test.go +++ b/db_test.go @@ -41,17 +41,44 @@ func TestDBInit(t *testing.T) { errorDB.Close() } -func TestRegister(t *testing.T) { +func TestRegisterNoCIDR(t *testing.T) { // Register tests - _, err := DB.Register() + _, err := DB.Register(cidrslice{}) if err != nil { t.Errorf("Registration failed, got error [%v]", err) } } +func TestRegisterMany(t *testing.T) { + for i, test := range []struct { + input cidrslice + output cidrslice + }{ + {cidrslice{"127.0.0.1/8", "8.8.8.8/32", "1.0.0.1/1"}, cidrslice{"127.0.0.1/8", "8.8.8.8/32", "1.0.0.1/1"}}, + {cidrslice{"1.1.1./32", "1922.168.42.42/8", "1.1.1.1/33", "1.2.3.4/"}, cidrslice{}}, + {cidrslice{"7.6.5.4/32", "invalid", "1.0.0.1/2"}, cidrslice{"7.6.5.4/32", "1.0.0.1/2"}}, + } { + user, err := DB.Register(test.input) + if err != nil { + t.Errorf("Test %d: Got error from register method: [%v]", i, err) + } + res, err := DB.GetByUsername(user.Username) + if err != nil { + t.Errorf("Test %d: Got error when fetching username: [%v]", i, err) + } + if len(user.AllowFrom) != len(test.output) { + t.Errorf("Test %d: Expected to recieve struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(user.AllowFrom)) + } + if len(res.AllowFrom) != len(test.output) { + t.Errorf("Test %d: Expected to recieve struct with [%d] entries in AllowFrom, but got [%d] records", i, len(test.output), len(res.AllowFrom)) + } + + } +} + func TestGetByUsername(t *testing.T) { // Create reg to refer to - reg, err := DB.Register() + reg, err := DB.Register(cidrslice{}) if err != nil { t.Errorf("Registration failed, got error [%v]", err) } @@ -76,7 +103,7 @@ func TestGetByUsername(t *testing.T) { } func TestPrepareErrors(t *testing.T) { - reg, _ := DB.Register() + reg, _ := DB.Register(cidrslice{}) tdb, err := sql.Open("testdb", "") if err != nil { t.Errorf("Got error: %v", err) @@ -98,7 +125,7 @@ func TestPrepareErrors(t *testing.T) { } func TestQueryExecErrors(t *testing.T) { - reg, _ := DB.Register() + reg, _ := DB.Register(cidrslice{}) testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) { return testResult{1, 0}, errors.New("Prepared query error") }) @@ -129,7 +156,7 @@ func TestQueryExecErrors(t *testing.T) { t.Errorf("Expected error from exec in GetByDomain, but got none") } - _, err = DB.Register() + _, err = DB.Register(cidrslice{}) if err == nil { t.Errorf("Expected error from exec in Register, but got none") } @@ -142,7 +169,7 @@ func TestQueryExecErrors(t *testing.T) { } func TestQueryScanErrors(t *testing.T) { - reg, _ := DB.Register() + reg, _ := DB.Register(cidrslice{}) testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) { return testResult{1, 0}, errors.New("Prepared query error") @@ -176,7 +203,7 @@ func TestQueryScanErrors(t *testing.T) { } func TestBadDBValues(t *testing.T) { - reg, _ := DB.Register() + reg, _ := DB.Register(cidrslice{}) testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) { columns := []string{"Username", "Password", "Subdomain", "Value", "LastActive"} @@ -209,7 +236,7 @@ func TestGetByDomain(t *testing.T) { var regDomain = ACMETxt{} // Create reg to refer to - reg, err := DB.Register() + reg, err := DB.Register(cidrslice{}) if err != nil { t.Errorf("Registration failed, got error [%v]", err) } @@ -246,7 +273,7 @@ func TestGetByDomain(t *testing.T) { func TestUpdate(t *testing.T) { // Create reg to refer to - reg, err := DB.Register() + reg, err := DB.Register(cidrslice{}) if err != nil { t.Errorf("Registration failed, got error [%v]", err) } diff --git a/dns_test.go b/dns_test.go index e550ec8..63c2701 100644 --- a/dns_test.go +++ b/dns_test.go @@ -139,7 +139,7 @@ func TestResolveTXT(t *testing.T) { resolv := resolver{server: "0.0.0.0:15353"} validTXT := "______________valid_response_______________" - atxt, err := DB.Register() + atxt, err := DB.Register(cidrslice{}) if err != nil { t.Errorf("Could not initiate db record: [%v]", err) return diff --git a/main.go b/main.go index d114ad3..2ba08cd 100644 --- a/main.go +++ b/main.go @@ -49,7 +49,6 @@ func startHTTPAPI() { }) api.Use(crs) var ForceAuth = authMiddleware{} - api.Get("/register", webRegisterGet) api.Post("/register", webRegisterPost) api.Post("/update", ForceAuth.Serve, webUpdatePost) switch DNSConf.API.TLS { 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 c6bdf46..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 @@ -66,21 +68,6 @@ type logconfig struct { Format string `toml:"logformat"` } -// ACMETxt is the default structure for the user controlled record -type ACMETxt struct { - Username uuid.UUID - Password string - ACMETxtPost - LastActive int64 - AllowFrom string -} - -// ACMETxtPost holds the DNS part of the ACMETxt struct -type ACMETxtPost struct { - Subdomain string `json:"subdomain"` - Value string `json:"txt"` -} - type acmedb struct { sync.Mutex DB *sql.DB @@ -88,7 +75,7 @@ type acmedb struct { type database interface { Init(string, string) error - Register() (ACMETxt, error) + Register(cidrslice) (ACMETxt, error) GetByUsername(uuid.UUID) (ACMETxt, error) GetByDomain(string) ([]ACMETxt, error) Update(ACMETxt) error diff --git a/util.go b/util.go index 7f51d06..44a5f3e 100644 --- a/util.go +++ b/util.go @@ -2,13 +2,13 @@ package main import ( "crypto/rand" - "github.com/BurntSushi/toml" - log "github.com/Sirupsen/logrus" - "github.com/miekg/dns" - "github.com/satori/go.uuid" "math/big" "regexp" "strings" + + "github.com/BurntSushi/toml" + log "github.com/Sirupsen/logrus" + "github.com/miekg/dns" ) func readConfig(fname string) DNSConfig { @@ -45,15 +45,6 @@ func sanitizeDomainQuestion(d string) string { return dom } -func newACMETxt() ACMETxt { - var a = ACMETxt{} - password := generatePassword(40) - a.Username = uuid.NewV4() - a.Password = password - a.Subdomain = uuid.NewV4().String() - return a -} - func setupLogging(format string, level string) { if format == "json" { log.SetFormatter(&log.JSONFormatter{}) @@ -78,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) + } + + } + } + } +} diff --git a/validation.go b/validation.go index 036b9bc..66e1626 100644 --- a/validation.go +++ b/validation.go @@ -1,9 +1,10 @@ package main import ( + "unicode/utf8" + "github.com/satori/go.uuid" "golang.org/x/crypto/bcrypt" - "unicode/utf8" ) func getValidUsername(u string) (uuid.UUID, error) { diff --git a/validation_test.go b/validation_test.go index 58801ca..fd63cf4 100644 --- a/validation_test.go +++ b/validation_test.go @@ -106,3 +106,25 @@ func TestCorrectPassword(t *testing.T) { } } } + +func TestGetValidCIDRMasks(t *testing.T) { + for i, test := range []struct { + input cidrslice + output cidrslice + }{ + {cidrslice{"10.0.0.1/24"}, cidrslice{"10.0.0.1/24"}}, + {cidrslice{"invalid", "127.0.0.1/32"}, cidrslice{"127.0.0.1/32"}}, + {cidrslice{"2002:c0a8::0/32", "8.8.8.8/32"}, cidrslice{"2002:c0a8::0/32", "8.8.8.8/32"}}, + } { + ret := test.input.ValidEntries() + if len(ret) == len(test.output) { + for i, v := range ret { + if v != test.output[i] { + t.Errorf("Test %d: Expected %q but got %q", i, test.output, ret) + } + } + } else { + t.Errorf("Test %d: Expected %q but got %q", i, test.output, ret) + } + } +}