Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd3a587e43 | ||
|
|
439da9c09f | ||
|
|
5c2e60a828 | ||
|
|
d542ee03b5 | ||
|
|
92f8cc2802 | ||
|
|
978ac5d62b | ||
|
|
4d214d7f52 | ||
|
|
f463d07d0b | ||
|
|
9a908d7d6b | ||
|
|
11c852ee91 | ||
|
|
32608e9f47 | ||
|
|
77b5fda6fb | ||
|
|
efdd560ee4 | ||
|
|
830cceb62c | ||
|
|
9f896759f0 | ||
|
|
476f6fc555 | ||
|
|
e2e84ede6b |
@@ -0,0 +1,17 @@
|
|||||||
|
builds:
|
||||||
|
- binary: acme-dns
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=1
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- 386
|
||||||
|
- amd64
|
||||||
|
|
||||||
|
archive:
|
||||||
|
format: tar.gz
|
||||||
|
files:
|
||||||
|
- LICENSE
|
||||||
|
- README.md
|
||||||
|
- Dockerfile
|
||||||
|
- config.cfg
|
||||||
@@ -14,6 +14,7 @@ COPY --from=builder /go/src/github.com/joohoi/acme-dns .
|
|||||||
RUN mkdir -p /etc/acme-dns
|
RUN mkdir -p /etc/acme-dns
|
||||||
RUN mkdir -p /var/lib/acme-dns
|
RUN mkdir -p /var/lib/acme-dns
|
||||||
RUN rm -rf ./config.cfg
|
RUN rm -rf ./config.cfg
|
||||||
|
RUN apk --no-cache add ca-certificates && update-ca-certificates
|
||||||
|
|
||||||
VOLUME ["/etc/acme-dns", "/var/lib/acme-dns"]
|
VOLUME ["/etc/acme-dns", "/var/lib/acme-dns"]
|
||||||
ENTRYPOINT ["./acme-dns"]
|
ENTRYPOINT ["./acme-dns"]
|
||||||
|
|||||||
@@ -110,13 +110,11 @@ Check out how in the INSTALL section.
|
|||||||
|
|
||||||
1) Install [Go 1.9 or newer](https://golang.org/doc/install)
|
1) Install [Go 1.9 or newer](https://golang.org/doc/install)
|
||||||
|
|
||||||
2) Clone this repo: `git clone https://github.com/joohoi/acme-dns $GOPATH/src/acme-dns`
|
2) Install acme-dns: `go get github.com/joohoi/acme-dns/...`
|
||||||
|
|
||||||
3) Build ACME-DNS: `go build`
|
3) Edit config.cfg to suit your needs (see [configuration](#configuration)). `acme-dns` will read the configuration file from `/etc/acme-dns/config.cfg` or `./config.cfg`
|
||||||
|
|
||||||
4) Edit config.cfg to suit your needs (see [configuration](#configuration))
|
4) Run acme-dns. Please note that acme-dns needs to open a privileged port (53, domain), so it needs to be run with elevated privileges.
|
||||||
|
|
||||||
5) Run acme-dns. Please note that acme-dns needs to open a privileged port (53, domain), so it needs to be run with elevated privileges.
|
|
||||||
|
|
||||||
## Using Docker
|
## Using Docker
|
||||||
|
|
||||||
@@ -186,8 +184,12 @@ connection = "acme-dns.db"
|
|||||||
[api]
|
[api]
|
||||||
# domain name to listen requests for, mandatory if using tls = "letsencrypt"
|
# domain name to listen requests for, mandatory if using tls = "letsencrypt"
|
||||||
api_domain = ""
|
api_domain = ""
|
||||||
|
# disable registration endpoint
|
||||||
|
disable_registration = false
|
||||||
# autocert HTTP port, eg. 80 for answering Let's Encrypt HTTP-01 challenges. Mandatory if using tls = "letsencrypt".
|
# autocert HTTP port, eg. 80 for answering Let's Encrypt HTTP-01 challenges. Mandatory if using tls = "letsencrypt".
|
||||||
autocert_port = "80"
|
autocert_port = "80"
|
||||||
|
# listen ip, default "" listens on all interfaces/addresses
|
||||||
|
ip = "127.0.0.1"
|
||||||
# listen port, eg. 443 for default HTTPS
|
# listen port, eg. 443 for default HTTPS
|
||||||
port = "8080"
|
port = "8080"
|
||||||
# possible values: "letsencrypt", "cert", "none"
|
# possible values: "letsencrypt", "cert", "none"
|
||||||
@@ -216,6 +218,9 @@ header_name = "X-Forwarded-For"
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
- v0.4 Clear error messages for bad TXT record content, proper handling of static CNAME records, fixed IP address parsing from the request, added option to disable registration endpoint in the configuration.
|
||||||
|
- v0.3.2 Dockerfile was fixed for users using autocert feature
|
||||||
|
- v0.3.1 Added goreleaser for distributing binary builds of the releases
|
||||||
- v0.3 Changed autocert to use HTTP-01 challenges, as TLS-SNI is disabled by Let's Encrypt
|
- v0.3 Changed autocert to use HTTP-01 challenges, as TLS-SNI is disabled by Let's Encrypt
|
||||||
- v0.2 Now powered by httprouter, support wildcard certificates, Docker images
|
- v0.2 Now powered by httprouter, support wildcard certificates, Docker images
|
||||||
- v0.1 Initial release
|
- v0.1 Initial release
|
||||||
|
|||||||
+4
-2
@@ -5,6 +5,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/satori/go.uuid"
|
"github.com/satori/go.uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ACMETxt is the default structure for the user controlled record
|
// ACMETxt is the default structure for the user controlled record
|
||||||
@@ -32,9 +33,9 @@ func (c *cidrslice) JSON() string {
|
|||||||
func (c *cidrslice) ValidEntries() []string {
|
func (c *cidrslice) ValidEntries() []string {
|
||||||
valid := []string{}
|
valid := []string{}
|
||||||
for _, v := range *c {
|
for _, v := range *c {
|
||||||
_, _, err := net.ParseCIDR(v)
|
_, _, err := net.ParseCIDR(sanitizeIPv6addr(v))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
valid = append(valid, v)
|
valid = append(valid, sanitizeIPv6addr(v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return valid
|
return valid
|
||||||
@@ -47,6 +48,7 @@ func (a ACMETxt) allowedFrom(ip string) bool {
|
|||||||
if len(a.AllowFrom.ValidEntries()) == 0 {
|
if len(a.AllowFrom.ValidEntries()) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
log.WithFields(log.Fields{"ip": remoteIP}).Debug("Checking if update is permitted from IP")
|
||||||
for _, v := range a.AllowFrom.ValidEntries() {
|
for _, v := range a.AllowFrom.ValidEntries() {
|
||||||
_, vnet, _ := net.ParseCIDR(v)
|
_, vnet, _ := net.ParseCIDR(v)
|
||||||
if vnet.Contains(remoteIP) {
|
if vnet.Contains(remoteIP) {
|
||||||
|
|||||||
@@ -66,7 +66,18 @@ func webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
|
|||||||
if !ok {
|
if !ok {
|
||||||
log.WithFields(log.Fields{"error": "context"}).Error("Context error")
|
log.WithFields(log.Fields{"error": "context"}).Error("Context error")
|
||||||
}
|
}
|
||||||
if validSubdomain(a.Subdomain) && validTXT(a.Value) {
|
// NOTE: An invalid subdomain should not happen - the auth handler should
|
||||||
|
// reject POSTs with an invalid subdomain before this handler. Reject any
|
||||||
|
// invalid subdomains anyway as a matter of caution.
|
||||||
|
if !validSubdomain(a.Subdomain) {
|
||||||
|
log.WithFields(log.Fields{"error": "subdomain", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad update data")
|
||||||
|
updStatus = http.StatusBadRequest
|
||||||
|
upd = jsonError("bad_subdomain")
|
||||||
|
} else if !validTXT(a.Value) {
|
||||||
|
log.WithFields(log.Fields{"error": "txt", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad update data")
|
||||||
|
updStatus = http.StatusBadRequest
|
||||||
|
upd = jsonError("bad_txt")
|
||||||
|
} else if validSubdomain(a.Subdomain) && validTXT(a.Value) {
|
||||||
err := DB.Update(a)
|
err := DB.Update(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"error": err.Error()}).Debug("Error while trying to update record")
|
log.WithFields(log.Fields{"error": err.Error()}).Debug("Error while trying to update record")
|
||||||
@@ -77,10 +88,6 @@ func webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
|
|||||||
updStatus = http.StatusOK
|
updStatus = http.StatusOK
|
||||||
upd = []byte("{\"txt\": \"" + a.Value + "\"}")
|
upd = []byte("{\"txt\": \"" + a.Value + "\"}")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log.WithFields(log.Fields{"error": "subdomain", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad update data")
|
|
||||||
updStatus = http.StatusBadRequest
|
|
||||||
upd = jsonError("bad_subdomain")
|
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(updStatus)
|
w.WriteHeader(updStatus)
|
||||||
|
|||||||
+60
@@ -159,6 +159,66 @@ func TestApiRegisterWithMockDB(t *testing.T) {
|
|||||||
DB.SetBackend(oldDb)
|
DB.SetBackend(oldDb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApiUpdateWithInvalidSubdomain(t *testing.T) {
|
||||||
|
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
|
||||||
|
updateJSON := map[string]interface{}{
|
||||||
|
"subdomain": "",
|
||||||
|
"txt": ""}
|
||||||
|
|
||||||
|
router := setupRouter(false, false)
|
||||||
|
server := httptest.NewServer(router)
|
||||||
|
defer server.Close()
|
||||||
|
e := getExpect(t, server)
|
||||||
|
newUser, err := DB.Register(cidrslice{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not create new user, got error [%v]", err)
|
||||||
|
}
|
||||||
|
// Invalid subdomain data
|
||||||
|
updateJSON["subdomain"] = "example.com"
|
||||||
|
updateJSON["txt"] = validTxtData
|
||||||
|
e.POST("/update").
|
||||||
|
WithJSON(updateJSON).
|
||||||
|
WithHeader("X-Api-User", newUser.Username.String()).
|
||||||
|
WithHeader("X-Api-Key", newUser.Password).
|
||||||
|
Expect().
|
||||||
|
Status(http.StatusUnauthorized).
|
||||||
|
JSON().Object().
|
||||||
|
ContainsKey("error").
|
||||||
|
NotContainsKey("txt").
|
||||||
|
ValueEqual("error", "forbidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiUpdateWithInvalidTxt(t *testing.T) {
|
||||||
|
invalidTXTData := "idk m8 bbl lmao"
|
||||||
|
|
||||||
|
updateJSON := map[string]interface{}{
|
||||||
|
"subdomain": "",
|
||||||
|
"txt": ""}
|
||||||
|
|
||||||
|
router := setupRouter(false, false)
|
||||||
|
server := httptest.NewServer(router)
|
||||||
|
defer server.Close()
|
||||||
|
e := getExpect(t, server)
|
||||||
|
newUser, err := DB.Register(cidrslice{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not create new user, got error [%v]", err)
|
||||||
|
}
|
||||||
|
updateJSON["subdomain"] = newUser.Subdomain
|
||||||
|
// Invalid txt data
|
||||||
|
updateJSON["txt"] = invalidTXTData
|
||||||
|
e.POST("/update").
|
||||||
|
WithJSON(updateJSON).
|
||||||
|
WithHeader("X-Api-User", newUser.Username.String()).
|
||||||
|
WithHeader("X-Api-Key", newUser.Password).
|
||||||
|
Expect().
|
||||||
|
Status(http.StatusBadRequest).
|
||||||
|
JSON().Object().
|
||||||
|
ContainsKey("error").
|
||||||
|
NotContainsKey("txt").
|
||||||
|
ValueEqual("error", "bad_txt")
|
||||||
|
}
|
||||||
|
|
||||||
func TestApiUpdateWithoutCredentials(t *testing.T) {
|
func TestApiUpdateWithoutCredentials(t *testing.T) {
|
||||||
router := setupRouter(false, false)
|
router := setupRouter(false, false)
|
||||||
server := httptest.NewServer(router)
|
server := httptest.NewServer(router)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
@@ -83,5 +84,10 @@ func updateAllowedFromIP(r *http.Request, user ACMETxt) bool {
|
|||||||
ips := getIPListFromHeader(r.Header.Get(Config.API.HeaderName))
|
ips := getIPListFromHeader(r.Header.Get(Config.API.HeaderName))
|
||||||
return user.allowedFromList(ips)
|
return user.allowedFromList(ips)
|
||||||
}
|
}
|
||||||
return user.allowedFrom(r.RemoteAddr)
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{"error": err.Error(), "remoteaddr": r.RemoteAddr}).Error("Error while parsing remote address")
|
||||||
|
host = ""
|
||||||
|
}
|
||||||
|
return user.allowedFrom(host)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateAllowedFromIP(t *testing.T) {
|
||||||
|
userWithAllow := newACMETxt()
|
||||||
|
userWithAllow.AllowFrom = cidrslice{"192.168.1.2/32", "[::1]/128"}
|
||||||
|
userWithoutAllow := newACMETxt()
|
||||||
|
|
||||||
|
for i, test := range []struct {
|
||||||
|
remoteaddr string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"192.168.1.2:1234", true},
|
||||||
|
{"192.168.1.1:1234", false},
|
||||||
|
{"invalid", false},
|
||||||
|
{"[::1]:4567", true},
|
||||||
|
} {
|
||||||
|
newreq, _ := http.NewRequest("GET", "/whatever", nil)
|
||||||
|
newreq.RemoteAddr = test.remoteaddr
|
||||||
|
ret := updateAllowedFromIP(newreq, userWithAllow)
|
||||||
|
if test.expected != ret {
|
||||||
|
t.Errorf("Test %d: Unexpected result for user with allowForm set", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updateAllowedFromIP(newreq, userWithoutAllow) {
|
||||||
|
t.Errorf("Test %d: Unexpected result for user without allowForm set", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,8 @@ connection = "/var/lib/acme-dns/acme-dns.db"
|
|||||||
api_domain = ""
|
api_domain = ""
|
||||||
# listen ip eg. 127.0.0.1
|
# listen ip eg. 127.0.0.1
|
||||||
ip = "0.0.0.0"
|
ip = "0.0.0.0"
|
||||||
|
# disable registration endpoint
|
||||||
|
disable_registration = false
|
||||||
# autocert HTTP port, eg. 80 for answering Let's Encrypt HTTP-01 challenges. Mandatory if using tls = "letsencrypt".
|
# autocert HTTP port, eg. 80 for answering Let's Encrypt HTTP-01 challenges. Mandatory if using tls = "letsencrypt".
|
||||||
autocert_port = "80"
|
autocert_port = "80"
|
||||||
# listen port, eg. 443 for default HTTPS
|
# listen port, eg. 443 for default HTTPS
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ func answer(q dns.Question) ([]dns.RR, int, error) {
|
|||||||
var rtype = q.Qtype
|
var rtype = q.Qtype
|
||||||
r, ok := RR.Records[rtype][domain]
|
r, ok := RR.Records[rtype][domain]
|
||||||
if !ok {
|
if !ok {
|
||||||
rcode = dns.RcodeNameError
|
r, ok = RR.Records[dns.TypeCNAME][domain]
|
||||||
|
if !ok {
|
||||||
|
rcode = dns.RcodeNameError
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
log.WithFields(log.Fields{"qtype": dns.TypeToString[rtype], "domain": domain, "rcode": dns.RcodeToString[rcode]}).Debug("Answering question for domain")
|
log.WithFields(log.Fields{"qtype": dns.TypeToString[rtype], "domain": domain, "rcode": dns.RcodeToString[rcode]}).Debug("Answering question for domain")
|
||||||
return r, rcode, nil
|
return r, rcode, nil
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ func startHTTPAPI() {
|
|||||||
// Logwriter for saner log output
|
// Logwriter for saner log output
|
||||||
c.Log = stdlog.New(logwriter, "", 0)
|
c.Log = stdlog.New(logwriter, "", 0)
|
||||||
}
|
}
|
||||||
api.POST("/register", webRegisterPost)
|
if !Config.API.DisableRegistration {
|
||||||
|
api.POST("/register", webRegisterPost)
|
||||||
|
}
|
||||||
api.POST("/update", Auth(webUpdatePost))
|
api.POST("/update", Auth(webUpdatePost))
|
||||||
|
|
||||||
host := Config.API.IP + ":" + Config.API.Port
|
host := Config.API.IP + ":" + Config.API.Port
|
||||||
|
|||||||
@@ -50,16 +50,17 @@ type dbsettings struct {
|
|||||||
|
|
||||||
// API config
|
// API config
|
||||||
type httpapi struct {
|
type httpapi struct {
|
||||||
Domain string `toml:"api_domain"`
|
Domain string `toml:"api_domain"`
|
||||||
IP string
|
IP string
|
||||||
AutocertPort string `toml:"autocert_port"`
|
DisableRegistration bool `toml:"disable_registration"`
|
||||||
Port string `toml:"port"`
|
AutocertPort string `toml:"autocert_port"`
|
||||||
TLS string
|
Port string `toml:"port"`
|
||||||
TLSCertPrivkey string `toml:"tls_cert_privkey"`
|
TLS string
|
||||||
TLSCertFullchain string `toml:"tls_cert_fullchain"`
|
TLSCertPrivkey string `toml:"tls_cert_privkey"`
|
||||||
CorsOrigins []string
|
TLSCertFullchain string `toml:"tls_cert_fullchain"`
|
||||||
UseHeader bool `toml:"use_header"`
|
CorsOrigins []string
|
||||||
HeaderName string `toml:"header_name"`
|
UseHeader bool `toml:"use_header"`
|
||||||
|
HeaderName string `toml:"header_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logging config
|
// Logging config
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ func sanitizeString(s string) string {
|
|||||||
return re.ReplaceAllString(s, "")
|
return re.ReplaceAllString(s, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sanitizeIPv6addr(s string) string {
|
||||||
|
// Remove brackets from IPv6 addresses, net.ParseCIDR needs this
|
||||||
|
re, _ := regexp.Compile("[\\[\\]]+")
|
||||||
|
return re.ReplaceAllString(s, "")
|
||||||
|
}
|
||||||
|
|
||||||
func generatePassword(length int) string {
|
func generatePassword(length int) string {
|
||||||
ret := make([]byte, length)
|
ret := make([]byte, length)
|
||||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_"
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_"
|
||||||
|
|||||||
Reference in New Issue
Block a user