diff --git a/README.md b/README.md index 6e8a845f8991a1ca1df1f882ebbe802c16eb3e30..c528f71b48846e10413e512dd974dd59219d6a3f 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 - -[](https://asciinema.org/a/94462) +[](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 0000000000000000000000000000000000000000..ad3ab821d4fd0c51467be4dc4450c5fa106a92be --- /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 7226ef9c23a613083b99953ec78f3d8e8e7c39bc..7ed5b3b26d805f482bc27df68f7947784419061f 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 b067f14f582fa28501a684f152f722b55c4a59a0..99fe655a3ffcf7c78015bc48948e464e0acd091e 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,7 +41,7 @@ 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(). + e.POST("/register").Expect(). Status(iris.StatusCreated). JSON().Object(). ContainsKey("fulldomain"). @@ -48,14 +49,26 @@ func TestApiRegister(t *testing.T) { ContainsKey("username"). ContainsKey("password"). NotContainsKey("error") - e.POST("/register").Expect(). + + 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 4bf5643c4d99069c17e81fcfe2e8208ea4458fe7..795ca15f287586aa66034fb8e2bc4c670d87e75a 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 868cf6e844ca2eb3587101d32c27d5a8a76388d2..3cac7588500b2b565da73760adfb4976b06a8142 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 665bed39b8ce6dffee46ead13ba009d50edabbc2..4580a34e68374266e8a60e10a709e9ac447f3e9c 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 e550ec809e7351338baf5b1ec36e45e4863f0d8a..63c2701604685dfb233e045cce009f471bd0d019 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 d114ad35600698b5422f8e40e6b97ef50ee280ac..2ba08cd2ffbd5f9ab745fbe70a45732bda17d8b8 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 980d0d1081390177405813391d45c3bb26c19c70..c417816937debe6b44b773b132e1ee7b25c5d0e7 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 c6bdf46a3bec243c4f464436a1fd295d1ebd1999..0a41a3e0064ea9f8a76994a9155c974fa97ff55c 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 7f51d06e7de766b765da1fe7800e63dc7f11adcc..44a5f3e8428fbf9da2c31fae3bc6ae691f5e2f35 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 3d8d9fdf9a75e4b0b928c3b7711d546b2594bb09..621918c36a7632bea7905d80cb48050046617a71 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 036b9bcbeb7ddbe9916ef113aef79e7b46247fb4..66e16260f94e337398698e2b7bd3420a405a74e9 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 58801ca730f2287d1df6594374c31c9c56d20841..fd63cf40a9e1e7d43c7806d899479dade2bd5648 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) + } + } +}