mirror of
https://github.com/joohoi/acme-dns.git
synced 2025-01-29 00:50:10 +07:00
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:
commit
d156937bc5
49
README.md
49
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
|
||||
|
78
acmetxt.go
Normal file
78
acmetxt.go
Normal 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
55
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{}
|
||||
|
108
api_test.go
108
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
|
||||
}
|
||||
|
@ -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
26
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
|
||||
}
|
||||
|
||||
|
47
db_test.go
47
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)
|
||||
}
|
||||
|
@ -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
|
||||
|
1
main.go
1
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 {
|
||||
|
@ -68,6 +68,8 @@ func setupConfig() {
|
||||
Port: "8080",
|
||||
TLS: "none",
|
||||
CorsOrigins: []string{"*"},
|
||||
UseHeader: false,
|
||||
HeaderName: "X-Forwarded-For",
|
||||
}
|
||||
|
||||
var dnscfg = DNSConfig{
|
||||
|
19
types.go
19
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
|
||||
|
28
util.go
28
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
|
||||
}
|
||||
|
24
util_test.go
24
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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user