Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd3a587e43 | ||
|
|
439da9c09f | ||
|
|
5c2e60a828 | ||
|
|
d542ee03b5 | ||
|
|
92f8cc2802 | ||
|
|
978ac5d62b | ||
|
|
4d214d7f52 | ||
|
|
f463d07d0b | ||
|
|
9a908d7d6b | ||
|
|
11c852ee91 | ||
|
|
32608e9f47 | ||
|
|
77b5fda6fb | ||
|
|
efdd560ee4 | ||
|
|
830cceb62c | ||
|
|
9f896759f0 | ||
|
|
476f6fc555 | ||
|
|
e2e84ede6b | ||
|
|
562d7cbad4 |
@@ -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 /var/lib/acme-dns
|
||||
RUN rm -rf ./config.cfg
|
||||
RUN apk --no-cache add ca-certificates && update-ca-certificates
|
||||
|
||||
VOLUME ["/etc/acme-dns", "/var/lib/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)
|
||||
|
||||
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))
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Using Docker
|
||||
|
||||
@@ -186,6 +184,12 @@ connection = "acme-dns.db"
|
||||
[api]
|
||||
# domain name to listen requests for, mandatory if using tls = "letsencrypt"
|
||||
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_port = "80"
|
||||
# listen ip, default "" listens on all interfaces/addresses
|
||||
ip = "127.0.0.1"
|
||||
# listen port, eg. 443 for default HTTPS
|
||||
port = "8080"
|
||||
# possible values: "letsencrypt", "cert", "none"
|
||||
@@ -214,6 +218,10 @@ header_name = "X-Forwarded-For"
|
||||
```
|
||||
|
||||
## 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.2 Now powered by httprouter, support wildcard certificates, Docker images
|
||||
- v0.1 Initial release
|
||||
|
||||
|
||||
+4
-2
@@ -5,6 +5,7 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/satori/go.uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ACMETxt is the default structure for the user controlled record
|
||||
@@ -32,9 +33,9 @@ func (c *cidrslice) JSON() string {
|
||||
func (c *cidrslice) ValidEntries() []string {
|
||||
valid := []string{}
|
||||
for _, v := range *c {
|
||||
_, _, err := net.ParseCIDR(v)
|
||||
_, _, err := net.ParseCIDR(sanitizeIPv6addr(v))
|
||||
if err == nil {
|
||||
valid = append(valid, v)
|
||||
valid = append(valid, sanitizeIPv6addr(v))
|
||||
}
|
||||
}
|
||||
return valid
|
||||
@@ -47,6 +48,7 @@ func (a ACMETxt) allowedFrom(ip string) bool {
|
||||
if len(a.AllowFrom.ValidEntries()) == 0 {
|
||||
return true
|
||||
}
|
||||
log.WithFields(log.Fields{"ip": remoteIP}).Debug("Checking if update is permitted from IP")
|
||||
for _, v := range a.AllowFrom.ValidEntries() {
|
||||
_, vnet, _ := net.ParseCIDR(v)
|
||||
if vnet.Contains(remoteIP) {
|
||||
|
||||
@@ -66,7 +66,18 @@ func webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
|
||||
if !ok {
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
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.WriteHeader(updStatus)
|
||||
|
||||
+60
@@ -159,6 +159,66 @@ func TestApiRegisterWithMockDB(t *testing.T) {
|
||||
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) {
|
||||
router := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
@@ -83,5 +84,10 @@ func updateAllowedFromIP(r *http.Request, user ACMETxt) bool {
|
||||
ips := getIPListFromHeader(r.Header.Get(Config.API.HeaderName))
|
||||
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,10 @@ connection = "/var/lib/acme-dns/acme-dns.db"
|
||||
api_domain = ""
|
||||
# listen ip eg. 127.0.0.1
|
||||
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_port = "80"
|
||||
# listen port, eg. 443 for default HTTPS
|
||||
port = "80"
|
||||
# possible values: "letsencrypt", "cert", "none"
|
||||
|
||||
@@ -52,7 +52,11 @@ func answer(q dns.Question) ([]dns.RR, int, error) {
|
||||
var rtype = q.Qtype
|
||||
r, ok := RR.Records[rtype][domain]
|
||||
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")
|
||||
return r, rcode, nil
|
||||
|
||||
@@ -67,7 +67,9 @@ func startHTTPAPI() {
|
||||
// Logwriter for saner log output
|
||||
c.Log = stdlog.New(logwriter, "", 0)
|
||||
}
|
||||
api.POST("/register", webRegisterPost)
|
||||
if !Config.API.DisableRegistration {
|
||||
api.POST("/register", webRegisterPost)
|
||||
}
|
||||
api.POST("/update", Auth(webUpdatePost))
|
||||
|
||||
host := Config.API.IP + ":" + Config.API.Port
|
||||
@@ -83,6 +85,9 @@ func startHTTPAPI() {
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(Config.API.Domain),
|
||||
}
|
||||
autocerthost := Config.API.IP + ":" + Config.API.AutocertPort
|
||||
log.WithFields(log.Fields{"autocerthost": autocerthost, "domain": Config.API.Domain}).Debug("Opening HTTP port for autocert")
|
||||
go http.ListenAndServe(autocerthost, m.HTTPHandler(nil))
|
||||
cfg.GetCertificate = m.GetCertificate
|
||||
srv := &http.Server{
|
||||
Addr: host,
|
||||
@@ -90,7 +95,7 @@ func startHTTPAPI() {
|
||||
TLSConfig: cfg,
|
||||
ErrorLog: stdlog.New(logwriter, "", 0),
|
||||
}
|
||||
log.WithFields(log.Fields{"host": host, "domain": Config.API.Domain}).Info("Listening HTTPS autocert")
|
||||
log.WithFields(log.Fields{"host": host, "domain": Config.API.Domain}).Info("Listening HTTPS, using certificate from autocert")
|
||||
log.Fatal(srv.ListenAndServeTLS("", ""))
|
||||
case "cert":
|
||||
srv := &http.Server{
|
||||
|
||||
@@ -50,15 +50,17 @@ type dbsettings struct {
|
||||
|
||||
// API config
|
||||
type httpapi struct {
|
||||
Domain string `toml:"api_domain"`
|
||||
IP string
|
||||
Port string `toml:"port"`
|
||||
TLS string
|
||||
TLSCertPrivkey string `toml:"tls_cert_privkey"`
|
||||
TLSCertFullchain string `toml:"tls_cert_fullchain"`
|
||||
CorsOrigins []string
|
||||
UseHeader bool `toml:"use_header"`
|
||||
HeaderName string `toml:"header_name"`
|
||||
Domain string `toml:"api_domain"`
|
||||
IP string
|
||||
DisableRegistration bool `toml:"disable_registration"`
|
||||
AutocertPort string `toml:"autocert_port"`
|
||||
Port string `toml:"port"`
|
||||
TLS string
|
||||
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
|
||||
|
||||
@@ -38,6 +38,12 @@ func sanitizeString(s string) string {
|
||||
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 {
|
||||
ret := make([]byte, length)
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_"
|
||||
|
||||
Reference in New Issue
Block a user