Merge pull request #4 from joohoi/cidr-range

Implement possibility to provide a list of CIDR ranges to limit update request origins
This commit is contained in:
Joona Hoikkala 2016-12-04 23:16:51 +02:00 committed by GitHub
commit d156937bc5
15 changed files with 375 additions and 93 deletions

View File

@ -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

78
acmetxt.go Normal file
View File

@ -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
}

55
api.go
View File

@ -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{}

View File

@ -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
}

View File

@ -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"

26
db.go
View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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 {

View File

@ -68,6 +68,8 @@ func setupConfig() {
Port: "8080",
TLS: "none",
CorsOrigins: []string{"*"},
UseHeader: false,
HeaderName: "X-Forwarded-For",
}
var dnscfg = DNSConfig{

View File

@ -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

28
util.go
View File

@ -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
}

View File

@ -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)
}
}
}
}
}

View File

@ -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) {

View File

@ -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)
}
}
}