Refactoring (#325)
* Refactor core * Re-added tests * Small fixes * Add tests for acmetxt cidrslice and util funcs * Remove the last dangling reference to old logging package * Refactoring (#327) * chore: enable more linters and fix linter issues * ci: enable linter checks on all branches and disable recurring checks recurring linter checks don't make that much sense. The code & linter checks should not change on their own over night ;) * chore: update packages * Revert "chore: update packages" This reverts commit 30250bf28c4b39e9e5b3af012a4e28ab036bf9af. * chore: manually upgrade some packages * Updated dependencies, wrote changelog entry and fixed namespace for release * Refactoring - improving coverage (#371) * Increase code coverage in acmedns * More testing of ReadConfig() and its fallback mechanism * Found that if someone put a '"' double quote into the filename that we configure zap to log to, it would cause the the JSON created to be invalid. I have replaced the JSON string with proper config * Better handling of config options for api.TLS - we now error on an invalid value instead of silently failing. added a basic test for api.setupTLS() (to increase test coverage) * testing nameserver isOwnChallenge and isAuthoritative methods * add a unit test for nameserver answerOwnChallenge * fix linting errors * bump go and golangci-lint versions in github actions * Update golangci-lint.yml Bumping github-actions workflow versions to accommodate some changes in upstream golanci-lint * Bump Golang version to 1.23 (currently the oldest supported version) Bump golanglint-ci to 2.0.2 and migrate the config file. This should resolve the math/rand/v2 issue * bump golanglint-ci action version * Fixing up new golanglint-ci warnings and errors --------- Co-authored-by: Joona Hoikkala <5235109+joohoi@users.noreply.github.com> * Minor refactoring, error returns and e2e testing suite * Add a few tests * Fix linter and umask setting * Update github actions * Refine concurrency configuration for GitHub actions * HTTP timeouts to API, and self-validation mutex to nameserver ops --------- Co-authored-by: Florian Ritterhoff <32478819+fritterhoff@users.noreply.github.com> Co-authored-by: Jason Playne <jason@jasonplayne.com>
This commit is contained in:
co-authored by
Florian Ritterhoff
Jason Playne
parent
b7a0a8a7bc
commit
5a7bc230b8
+135
@@ -0,0 +1,135 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/rs/cors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AcmednsAPI struct {
|
||||
Config *acmedns.AcmeDnsConfig
|
||||
DB acmedns.AcmednsDB
|
||||
Logger *zap.SugaredLogger
|
||||
errChan chan error
|
||||
}
|
||||
|
||||
func Init(config *acmedns.AcmeDnsConfig, db acmedns.AcmednsDB, logger *zap.SugaredLogger, errChan chan error) AcmednsAPI {
|
||||
a := AcmednsAPI{Config: config, DB: db, Logger: logger, errChan: errChan}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *AcmednsAPI) Start(dnsservers []acmedns.AcmednsNS) {
|
||||
var err error
|
||||
//TODO: do we want to debug log the HTTP server?
|
||||
stderrorlog, err := zap.NewStdLogAt(a.Logger.Desugar(), zap.ErrorLevel)
|
||||
if err != nil {
|
||||
a.errChan <- err
|
||||
return
|
||||
}
|
||||
api := httprouter.New()
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: a.Config.API.CorsOrigins,
|
||||
AllowedMethods: []string{"GET", "POST"},
|
||||
OptionsPassthrough: false,
|
||||
Debug: a.Config.General.Debug,
|
||||
})
|
||||
if a.Config.General.Debug {
|
||||
// Logwriter for saner log output
|
||||
c.Log = stderrorlog
|
||||
}
|
||||
if !a.Config.API.DisableRegistration {
|
||||
api.POST("/register", a.webRegisterPost)
|
||||
}
|
||||
api.POST("/update", a.Auth(a.webUpdatePost))
|
||||
api.GET("/health", a.healthCheck)
|
||||
|
||||
host := a.Config.API.IP + ":" + a.Config.API.Port
|
||||
|
||||
// TLS specific general settings
|
||||
cfg := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
switch a.Config.API.TLS {
|
||||
case acmedns.ApiTlsProviderLetsEncrypt, acmedns.ApiTlsProviderLetsEncryptStaging:
|
||||
magic := a.setupTLS(dnsservers)
|
||||
err = magic.ManageAsync(context.Background(), []string{a.Config.General.Domain})
|
||||
if err != nil {
|
||||
a.errChan <- err
|
||||
return
|
||||
}
|
||||
cfg.GetCertificate = magic.GetCertificate
|
||||
srv := &http.Server{
|
||||
Addr: host,
|
||||
Handler: c.Handler(api),
|
||||
TLSConfig: cfg,
|
||||
ErrorLog: stderrorlog,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
a.Logger.Infow("Listening HTTPS",
|
||||
"host", host,
|
||||
"domain", a.Config.General.Domain)
|
||||
err = srv.ListenAndServeTLS("", "")
|
||||
case acmedns.ApiTlsProviderCert:
|
||||
srv := &http.Server{
|
||||
Addr: host,
|
||||
Handler: c.Handler(api),
|
||||
TLSConfig: cfg,
|
||||
ErrorLog: stderrorlog,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
a.Logger.Infow("Listening HTTPS",
|
||||
"host", host,
|
||||
"domain", a.Config.General.Domain)
|
||||
err = srv.ListenAndServeTLS(a.Config.API.TLSCertFullchain, a.Config.API.TLSCertPrivkey)
|
||||
default:
|
||||
a.Logger.Infow("Listening HTTP",
|
||||
"host", host)
|
||||
err = http.ListenAndServe(host, c.Handler(api))
|
||||
}
|
||||
if err != nil {
|
||||
a.errChan <- err
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AcmednsAPI) setupTLS(dnsservers []acmedns.AcmednsNS) *certmagic.Config {
|
||||
provider := NewChallengeProvider(dnsservers)
|
||||
certmagic.Default.Logger = a.Logger.Desugar()
|
||||
storage := certmagic.FileStorage{Path: a.Config.API.ACMECacheDir}
|
||||
|
||||
// Set up certmagic for getting certificate for acme-dns api
|
||||
certmagic.DefaultACME.DNS01Solver = &provider
|
||||
certmagic.DefaultACME.Agreed = true
|
||||
certmagic.DefaultACME.Logger = a.Logger.Desugar()
|
||||
if a.Config.API.TLS == acmedns.ApiTlsProviderLetsEncrypt {
|
||||
certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA
|
||||
} else {
|
||||
certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA
|
||||
}
|
||||
certmagic.DefaultACME.Email = a.Config.API.NotificationEmail
|
||||
|
||||
magicConf := certmagic.Default
|
||||
magicConf.Logger = a.Logger.Desugar()
|
||||
magicConf.Storage = &storage
|
||||
magicConf.DefaultServerName = a.Config.General.Domain
|
||||
magicCache := certmagic.NewCache(certmagic.CacheOptions{
|
||||
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
|
||||
return &magicConf, nil
|
||||
},
|
||||
Logger: a.Logger.Desugar(),
|
||||
})
|
||||
magic := certmagic.New(magicCache, magicConf)
|
||||
return magic
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||
"github.com/joohoi/acme-dns/pkg/database"
|
||||
"github.com/joohoi/acme-dns/pkg/nameserver"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/gavv/httpexpect"
|
||||
"github.com/google/uuid"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/rs/cors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func fakeConfigAndLogger() (acmedns.AcmeDnsConfig, *zap.SugaredLogger) {
|
||||
c := acmedns.AcmeDnsConfig{}
|
||||
c.Database.Engine = "sqlite"
|
||||
c.Database.Connection = ":memory:"
|
||||
l := zap.NewNop().Sugar()
|
||||
return c, l
|
||||
}
|
||||
|
||||
// noAuth function to write ACMETxt model to context while not preforming any validation
|
||||
func noAuth(update httprouter.Handle) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
postData := acmedns.ACMETxt{}
|
||||
uname := r.Header.Get("X-Api-User")
|
||||
passwd := r.Header.Get("X-Api-Key")
|
||||
|
||||
dec := json.NewDecoder(r.Body)
|
||||
_ = dec.Decode(&postData)
|
||||
// Set user info to the decoded ACMETxt object
|
||||
postData.Username, _ = uuid.Parse(uname)
|
||||
postData.Password = passwd
|
||||
// Set the ACMETxt struct to context to pull in from update function
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, ACMETxtKey, postData)
|
||||
r = r.WithContext(ctx)
|
||||
update(w, r, p)
|
||||
}
|
||||
}
|
||||
|
||||
func getExpect(t *testing.T, server *httptest.Server) *httpexpect.Expect {
|
||||
return httpexpect.WithConfig(httpexpect.Config{
|
||||
BaseURL: server.URL,
|
||||
Reporter: httpexpect.NewAssertReporter(t),
|
||||
Printers: []httpexpect.Printer{
|
||||
httpexpect.NewCurlPrinter(t),
|
||||
httpexpect.NewDebugPrinter(t, true),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func setupRouter(debug bool, noauth bool) (http.Handler, AcmednsAPI, acmedns.AcmednsDB) {
|
||||
api := httprouter.New()
|
||||
config, logger := fakeConfigAndLogger()
|
||||
config.API.Domain = ""
|
||||
config.API.Port = "8080"
|
||||
config.API.TLS = acmedns.ApiTlsProviderNone
|
||||
config.API.CorsOrigins = []string{"*"}
|
||||
config.API.UseHeader = true
|
||||
config.API.HeaderName = "X-Forwarded-For"
|
||||
|
||||
db, _ := database.Init(&config, logger)
|
||||
errChan := make(chan error, 1)
|
||||
adnsapi := Init(&config, db, logger, errChan)
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: config.API.CorsOrigins,
|
||||
AllowedMethods: []string{"GET", "POST"},
|
||||
OptionsPassthrough: false,
|
||||
Debug: config.General.Debug,
|
||||
})
|
||||
api.POST("/register", adnsapi.webRegisterPost)
|
||||
api.GET("/health", adnsapi.healthCheck)
|
||||
if noauth {
|
||||
api.POST("/update", noAuth(adnsapi.webUpdatePost))
|
||||
} else {
|
||||
api.POST("/update", adnsapi.Auth(adnsapi.webUpdatePost))
|
||||
}
|
||||
return c.Handler(api), adnsapi, db
|
||||
}
|
||||
|
||||
func TestApiRegister(t *testing.T) {
|
||||
router, _, _ := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
e.POST("/register").Expect().
|
||||
Status(http.StatusCreated).
|
||||
JSON().Object().
|
||||
ContainsKey("fulldomain").
|
||||
ContainsKey("subdomain").
|
||||
ContainsKey("username").
|
||||
ContainsKey("password").
|
||||
NotContainsKey("error")
|
||||
|
||||
allowfrom := map[string][]interface{}{
|
||||
"allowfrom": {"123.123.123.123/32",
|
||||
"2001:db8:a0b:12f0::1/32",
|
||||
"[::1]/64",
|
||||
},
|
||||
}
|
||||
|
||||
response := e.POST("/register").
|
||||
WithJSON(allowfrom).
|
||||
Expect().
|
||||
Status(http.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", "2001:db8:a0b:12f0::1/32", "::1/64")
|
||||
}
|
||||
|
||||
func TestApiRegisterBadAllowFrom(t *testing.T) {
|
||||
router, _, _ := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
invalidVals := []string{
|
||||
"invalid",
|
||||
"1.2.3.4/33",
|
||||
"1.2/24",
|
||||
"1.2.3.4",
|
||||
"12345:db8:a0b:12f0::1/32",
|
||||
"1234::123::123::1/32",
|
||||
}
|
||||
|
||||
for _, v := range invalidVals {
|
||||
|
||||
allowfrom := map[string][]interface{}{
|
||||
"allowfrom": {v}}
|
||||
|
||||
response := e.POST("/register").
|
||||
WithJSON(allowfrom).
|
||||
Expect().
|
||||
Status(http.StatusBadRequest).
|
||||
JSON().Object().
|
||||
ContainsKey("error")
|
||||
|
||||
response.Value("error").Equal("invalid_allowfrom_cidr")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiRegisterMalformedJSON(t *testing.T) {
|
||||
router, _, _ := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
|
||||
malPayloads := []string{
|
||||
"{\"allowfrom': '1.1.1.1/32'}",
|
||||
"\"allowfrom\": \"1.1.1.1/32\"",
|
||||
"{\"allowfrom\": \"[1.1.1.1/32]\"",
|
||||
"\"allowfrom\": \"1.1.1.1/32\"}",
|
||||
"{allowfrom: \"1.2.3.4\"}",
|
||||
"{allowfrom: [1.2.3.4]}",
|
||||
"whatever that's not a json payload",
|
||||
}
|
||||
for _, test := range malPayloads {
|
||||
e.POST("/register").
|
||||
WithBytes([]byte(test)).
|
||||
Expect().
|
||||
Status(http.StatusBadRequest).
|
||||
JSON().Object().
|
||||
ContainsKey("error").
|
||||
NotContainsKey("subdomain").
|
||||
NotContainsKey("username")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiRegisterWithMockDB(t *testing.T) {
|
||||
router, _, db := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
oldDb := db.GetBackend()
|
||||
mdb, mock, _ := sqlmock.New()
|
||||
db.SetBackend(mdb)
|
||||
defer db.Close()
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectPrepare("INSERT INTO records").WillReturnError(errors.New("error"))
|
||||
e.POST("/register").Expect().
|
||||
Status(http.StatusInternalServerError).
|
||||
JSON().Object().
|
||||
ContainsKey("error")
|
||||
db.SetBackend(oldDb)
|
||||
}
|
||||
|
||||
func TestApiUpdateWithInvalidSubdomain(t *testing.T) {
|
||||
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
updateJSON := map[string]interface{}{
|
||||
"subdomain": "",
|
||||
"txt": ""}
|
||||
|
||||
router, _, db := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
newUser, err := db.Register(acmedns.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, _, db := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
newUser, err := db.Register(acmedns.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)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
e.POST("/update").Expect().
|
||||
Status(http.StatusUnauthorized).
|
||||
JSON().Object().
|
||||
ContainsKey("error").
|
||||
NotContainsKey("txt")
|
||||
}
|
||||
|
||||
func TestApiUpdateWithCredentials(t *testing.T) {
|
||||
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
updateJSON := map[string]interface{}{
|
||||
"subdomain": "",
|
||||
"txt": ""}
|
||||
|
||||
router, _, db := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
newUser, err := db.Register(acmedns.Cidrslice{})
|
||||
if err != nil {
|
||||
t.Errorf("Could not create new user, got error [%v]", err)
|
||||
}
|
||||
// Valid data
|
||||
updateJSON["subdomain"] = newUser.Subdomain
|
||||
updateJSON["txt"] = validTxtData
|
||||
e.POST("/update").
|
||||
WithJSON(updateJSON).
|
||||
WithHeader("X-Api-User", newUser.Username.String()).
|
||||
WithHeader("X-Api-Key", newUser.Password).
|
||||
Expect().
|
||||
Status(http.StatusOK).
|
||||
JSON().Object().
|
||||
ContainsKey("txt").
|
||||
NotContainsKey("error").
|
||||
ValueEqual("txt", validTxtData)
|
||||
}
|
||||
|
||||
func TestApiUpdateWithCredentialsMockDB(t *testing.T) {
|
||||
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
updateJSON := map[string]interface{}{
|
||||
"subdomain": "",
|
||||
"txt": ""}
|
||||
|
||||
// Valid data
|
||||
updateJSON["subdomain"] = "a097455b-52cc-4569-90c8-7a4b97c6eba8"
|
||||
updateJSON["txt"] = validTxtData
|
||||
|
||||
router, _, db := setupRouter(false, true)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
oldDb := db.GetBackend()
|
||||
mdb, mock, _ := sqlmock.New()
|
||||
db.SetBackend(mdb)
|
||||
defer db.Close()
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectPrepare("UPDATE records").WillReturnError(errors.New("error"))
|
||||
e.POST("/update").
|
||||
WithJSON(updateJSON).
|
||||
Expect().
|
||||
Status(http.StatusInternalServerError).
|
||||
JSON().Object().
|
||||
ContainsKey("error")
|
||||
db.SetBackend(oldDb)
|
||||
}
|
||||
|
||||
func TestApiManyUpdateWithCredentials(t *testing.T) {
|
||||
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
router, _, db := setupRouter(true, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
// User without defined CIDR masks
|
||||
newUser, err := db.Register(acmedns.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(acmedns.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(acmedns.Cidrslice{"10.1.2.3/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
|
||||
subdomain string
|
||||
txt interface{}
|
||||
status int
|
||||
}{
|
||||
{"non-uuid-user", "tooshortpass", "non-uuid-subdomain", validTxtData, 401},
|
||||
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", "tooshortpass", "bb97455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401},
|
||||
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", "LongEnoughPassButNoUserExists___________", "bb97455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401},
|
||||
{newUser.Username.String(), newUser.Password, "a097455b-52cc-4569-90c8-7a4b97c6eba8", validTxtData, 401},
|
||||
{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,
|
||||
"txt": test.txt}
|
||||
e.POST("/update").
|
||||
WithJSON(updateJSON).
|
||||
WithHeader("X-Api-User", test.user).
|
||||
WithHeader("X-Api-Key", test.pass).
|
||||
WithHeader("X-Forwarded-For", "10.1.2.3").
|
||||
Expect().
|
||||
Status(test.status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {
|
||||
|
||||
router, adnsapi, db := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
// Use header checks from default header (X-Forwarded-For)
|
||||
adnsapi.Config.API.UseHeader = true
|
||||
// User without defined CIDR masks
|
||||
newUser, err := db.Register(acmedns.Cidrslice{})
|
||||
if err != nil {
|
||||
t.Errorf("Could not create new user, got error [%v]", err)
|
||||
}
|
||||
|
||||
newUserWithCIDR, err := db.Register(acmedns.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(acmedns.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 acmedns.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)
|
||||
}
|
||||
adnsapi.Config.API.UseHeader = false
|
||||
}
|
||||
|
||||
func TestApiHealthCheck(t *testing.T) {
|
||||
router, _, _ := setupRouter(false, false)
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
e := getExpect(t, server)
|
||||
e.GET("/health").Expect().Status(http.StatusOK)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAllowedFromIP(t *testing.T) {
|
||||
_, adnsapi, _ := setupRouter(false, false)
|
||||
adnsapi.Config.API.UseHeader = false
|
||||
userWithAllow := acmedns.NewACMETxt()
|
||||
userWithAllow.AllowFrom = acmedns.Cidrslice{"192.168.1.2/32", "[::1]/128"}
|
||||
userWithoutAllow := acmedns.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 := adnsapi.updateAllowedFromIP(newreq, userWithAllow)
|
||||
if test.expected != ret {
|
||||
t.Errorf("Test %d: Unexpected result for user with allowForm set", i)
|
||||
}
|
||||
|
||||
if !adnsapi.updateAllowedFromIP(newreq, userWithoutAllow) {
|
||||
t.Errorf("Test %d: Unexpected result for user without allowForm set", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupTLS(t *testing.T) {
|
||||
_, svr, _ := setupRouter(false, false)
|
||||
|
||||
for _, test := range []struct {
|
||||
apiTls string
|
||||
expectedCA string
|
||||
}{
|
||||
{
|
||||
apiTls: acmedns.ApiTlsProviderLetsEncrypt,
|
||||
expectedCA: certmagic.LetsEncryptProductionCA,
|
||||
},
|
||||
{
|
||||
apiTls: acmedns.ApiTlsProviderLetsEncryptStaging,
|
||||
expectedCA: certmagic.LetsEncryptStagingCA,
|
||||
},
|
||||
} {
|
||||
svr.Config.API.TLS = test.apiTls
|
||||
ns := &nameserver.Nameserver{}
|
||||
magic := svr.setupTLS([]acmedns.AcmednsNS{ns})
|
||||
|
||||
if test.expectedCA != certmagic.DefaultACME.CA {
|
||||
t.Errorf("failed to configure default ACME CA. got %s, want %s", certmagic.DefaultACME.CA, test.expectedCA)
|
||||
}
|
||||
|
||||
if magic.DefaultServerName != svr.Config.General.Domain {
|
||||
t.Errorf("failed to set the correct doman. got: %s, want %s", magic.DefaultServerName, svr.Config.General.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
// ACMETxtKey is a context key for ACMETxt struct
|
||||
const ACMETxtKey key = 0
|
||||
|
||||
// Auth middleware for update request
|
||||
func (a *AcmednsAPI) Auth(update httprouter.Handle) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
postData := acmedns.ACMETxt{}
|
||||
userOK := false
|
||||
user, err := a.getUserFromRequest(r)
|
||||
if err == nil {
|
||||
if a.updateAllowedFromIP(r, user) {
|
||||
dec := json.NewDecoder(r.Body)
|
||||
err = dec.Decode(&postData)
|
||||
if err != nil {
|
||||
a.Logger.Errorw("Decoding error",
|
||||
"error", "json_error")
|
||||
}
|
||||
if user.Subdomain == postData.Subdomain {
|
||||
userOK = true
|
||||
} else {
|
||||
a.Logger.Errorw("Subdomain mismatch",
|
||||
"error", "subdomain_mismatch",
|
||||
"name", postData.Subdomain,
|
||||
"expected", user.Subdomain)
|
||||
}
|
||||
} else {
|
||||
a.Logger.Errorw("Update not allowed from IP",
|
||||
"error", "ip_unauthorized")
|
||||
}
|
||||
} else {
|
||||
a.Logger.Errorw("Error while trying to get user",
|
||||
"error", err.Error())
|
||||
}
|
||||
if userOK {
|
||||
// Set user info to the decoded ACMETxt object
|
||||
postData.Username = user.Username
|
||||
postData.Password = user.Password
|
||||
// Set the ACMETxt struct to context to pull in from update function
|
||||
ctx := context.WithValue(r.Context(), ACMETxtKey, postData)
|
||||
update(w, r.WithContext(ctx), p)
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = w.Write(jsonError("forbidden"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AcmednsAPI) getUserFromRequest(r *http.Request) (acmedns.ACMETxt, error) {
|
||||
uname := r.Header.Get("X-Api-User")
|
||||
passwd := r.Header.Get("X-Api-Key")
|
||||
username, err := getValidUsername(uname)
|
||||
if err != nil {
|
||||
return acmedns.ACMETxt{}, fmt.Errorf("invalid username: %s: %w", uname, err)
|
||||
}
|
||||
if validKey(passwd) {
|
||||
dbuser, err := a.DB.GetByUsername(username)
|
||||
if err != nil {
|
||||
a.Logger.Errorw("Error while trying to get user",
|
||||
"error", err.Error())
|
||||
// To protect against timed side channel (never gonna give you up)
|
||||
acmedns.CorrectPassword(passwd, "$2a$10$8JEFVNYYhLoBysjAxe2yBuXrkDojBQBkVpXEQgyQyjn43SvJ4vL36")
|
||||
|
||||
return acmedns.ACMETxt{}, fmt.Errorf("invalid username: %s", uname)
|
||||
}
|
||||
if acmedns.CorrectPassword(passwd, dbuser.Password) {
|
||||
return dbuser, nil
|
||||
}
|
||||
return acmedns.ACMETxt{}, fmt.Errorf("invalid password for user %s", uname)
|
||||
}
|
||||
return acmedns.ACMETxt{}, fmt.Errorf("invalid key for user %s", uname)
|
||||
}
|
||||
|
||||
func (a *AcmednsAPI) updateAllowedFromIP(r *http.Request, user acmedns.ACMETxt) bool {
|
||||
if a.Config.API.UseHeader {
|
||||
ips := getIPListFromHeader(r.Header.Get(a.Config.API.HeaderName))
|
||||
return user.AllowedFromList(ips)
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
a.Logger.Errorw("Error while parsing remote address",
|
||||
"error", err.Error(),
|
||||
"remoteaddr", r.RemoteAddr)
|
||||
host = ""
|
||||
}
|
||||
return user.AllowedFrom(host)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mholt/acmez/v3/acme"
|
||||
|
||||
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||
)
|
||||
|
||||
// ChallengeProvider implements go-acme/lego Provider interface which is used for ACME DNS challenge handling
|
||||
type ChallengeProvider struct {
|
||||
servers []acmedns.AcmednsNS
|
||||
}
|
||||
|
||||
// NewChallengeProvider creates a new instance of ChallengeProvider
|
||||
func NewChallengeProvider(servers []acmedns.AcmednsNS) ChallengeProvider {
|
||||
return ChallengeProvider{servers: servers}
|
||||
}
|
||||
|
||||
// Present is used for making the ACME DNS challenge token available for DNS
|
||||
func (c *ChallengeProvider) Present(ctx context.Context, challenge acme.Challenge) error {
|
||||
for _, s := range c.servers {
|
||||
s.SetOwnAuthKey(challenge.DNS01KeyAuthorization())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp is called after the run to remove the ACME DNS challenge tokens from DNS records
|
||||
func (c *ChallengeProvider) CleanUp(ctx context.Context, _ acme.Challenge) error {
|
||||
for _, s := range c.servers {
|
||||
s.SetOwnAuthKey("")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait is a dummy function as we are just going to be ready to answer the challenge from the get-go
|
||||
func (c *ChallengeProvider) Wait(_ context.Context, _ acme.Challenge) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||
"github.com/mholt/acmez/v3/acme"
|
||||
)
|
||||
|
||||
type mockNameserver struct {
|
||||
acmedns.AcmednsNS
|
||||
authKey string
|
||||
}
|
||||
|
||||
func (m *mockNameserver) SetOwnAuthKey(key string) {
|
||||
m.authKey = key
|
||||
}
|
||||
|
||||
func TestChallengeProvider(t *testing.T) {
|
||||
mock := &mockNameserver{}
|
||||
servers := []acmedns.AcmednsNS{mock}
|
||||
cp := NewChallengeProvider(servers)
|
||||
|
||||
ctx := context.Background()
|
||||
challenge := acme.Challenge{
|
||||
Type: "dns-01",
|
||||
Token: "test-token",
|
||||
KeyAuthorization: "test-key-auth",
|
||||
}
|
||||
|
||||
// Test Present
|
||||
err := cp.Present(ctx, challenge)
|
||||
if err != nil {
|
||||
t.Errorf("Present failed: %v", err)
|
||||
}
|
||||
expectedKey := challenge.DNS01KeyAuthorization()
|
||||
if mock.authKey != expectedKey {
|
||||
t.Errorf("Expected auth key %s, got %s", expectedKey, mock.authKey)
|
||||
}
|
||||
|
||||
// Test CleanUp
|
||||
err = cp.CleanUp(ctx, challenge)
|
||||
if err != nil {
|
||||
t.Errorf("CleanUp failed: %v", err)
|
||||
}
|
||||
if mock.authKey != "" {
|
||||
t.Errorf("Expected empty auth key after CleanUp, got %s", mock.authKey)
|
||||
}
|
||||
|
||||
// Test Wait
|
||||
err = cp.Wait(ctx, challenge)
|
||||
if err != nil {
|
||||
t.Errorf("Wait failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
// Endpoint used to check the readiness and/or liveness (health) of the server.
|
||||
func (a *AcmednsAPI) healthCheck(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||
)
|
||||
|
||||
// RegResponse is a struct for registration response JSON
|
||||
type RegResponse struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Fulldomain string `json:"fulldomain"`
|
||||
Subdomain string `json:"subdomain"`
|
||||
Allowfrom []string `json:"allowfrom"`
|
||||
}
|
||||
|
||||
func (a *AcmednsAPI) webRegisterPost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
var regStatus int
|
||||
var reg []byte
|
||||
var err error
|
||||
aTXT := acmedns.ACMETxt{}
|
||||
bdata, _ := io.ReadAll(r.Body)
|
||||
if len(bdata) > 0 {
|
||||
err = json.Unmarshal(bdata, &aTXT)
|
||||
if err != nil {
|
||||
regStatus = http.StatusBadRequest
|
||||
reg = jsonError("malformed_json_payload")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(regStatus)
|
||||
_, _ = w.Write(reg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fail with malformed CIDR mask in allowfrom
|
||||
err = aTXT.AllowFrom.IsValid()
|
||||
if err != nil {
|
||||
regStatus = http.StatusBadRequest
|
||||
reg = jsonError("invalid_allowfrom_cidr")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(regStatus)
|
||||
_, _ = w.Write(reg)
|
||||
return
|
||||
}
|
||||
|
||||
// Create new user
|
||||
nu, err := a.DB.Register(aTXT.AllowFrom)
|
||||
if err != nil {
|
||||
errstr := fmt.Sprintf("%v", err)
|
||||
reg = jsonError(errstr)
|
||||
regStatus = http.StatusInternalServerError
|
||||
a.Logger.Errorw("Error in registration",
|
||||
"error", err.Error())
|
||||
} else {
|
||||
a.Logger.Debugw("Created new user",
|
||||
"user", nu.Username.String())
|
||||
regStruct := RegResponse{nu.Username.String(), nu.Password, nu.Subdomain + "." + a.Config.General.Domain, nu.Subdomain, nu.AllowFrom.ValidEntries()}
|
||||
regStatus = http.StatusCreated
|
||||
reg, err = json.Marshal(regStruct)
|
||||
if err != nil {
|
||||
regStatus = http.StatusInternalServerError
|
||||
reg = jsonError("json_error")
|
||||
a.Logger.Errorw("Could not marshal JSON",
|
||||
"error", "json")
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(regStatus)
|
||||
_, _ = w.Write(reg)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func (a *AcmednsAPI) webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
var updStatus int
|
||||
var upd []byte
|
||||
// Get user
|
||||
atxt, ok := r.Context().Value(ACMETxtKey).(acmedns.ACMETxt)
|
||||
if !ok {
|
||||
a.Logger.Errorw("Context error",
|
||||
"error", "context")
|
||||
}
|
||||
// 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(atxt.Subdomain) {
|
||||
a.Logger.Errorw("Bad update data",
|
||||
"error", "subdomain",
|
||||
"subdomain", atxt.Subdomain,
|
||||
"txt", atxt.Value)
|
||||
updStatus = http.StatusBadRequest
|
||||
upd = jsonError("bad_subdomain")
|
||||
} else if !validTXT(atxt.Value) {
|
||||
a.Logger.Errorw("Bad update data",
|
||||
"error", "txt",
|
||||
"subdomain", atxt.Subdomain,
|
||||
"txt", atxt.Value)
|
||||
updStatus = http.StatusBadRequest
|
||||
upd = jsonError("bad_txt")
|
||||
} else if validSubdomain(atxt.Subdomain) && validTXT(atxt.Value) {
|
||||
err := a.DB.Update(atxt.ACMETxtPost)
|
||||
if err != nil {
|
||||
a.Logger.Errorw("Error while trying to update record",
|
||||
"error", err.Error())
|
||||
updStatus = http.StatusInternalServerError
|
||||
upd = jsonError("db_error")
|
||||
} else {
|
||||
a.Logger.Debugw("TXT record updated",
|
||||
"subdomain", atxt.Subdomain,
|
||||
"txt", atxt.Value)
|
||||
updStatus = http.StatusOK
|
||||
upd = []byte("{\"txt\": \"" + atxt.Value + "\"}")
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(updStatus)
|
||||
_, _ = w.Write(upd)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||
)
|
||||
|
||||
func jsonError(message string) []byte {
|
||||
return []byte(fmt.Sprintf("{\"error\": \"%s\"}", message))
|
||||
}
|
||||
|
||||
func getValidUsername(u string) (uuid.UUID, error) {
|
||||
uname, err := uuid.Parse(u)
|
||||
if err != nil {
|
||||
return uuid.UUID{}, err
|
||||
}
|
||||
return uname, nil
|
||||
}
|
||||
|
||||
func validKey(k string) bool {
|
||||
kn := acmedns.SanitizeString(k)
|
||||
if utf8.RuneCountInString(k) == 40 && utf8.RuneCountInString(kn) == 40 {
|
||||
// Correct length and all chars valid
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func validSubdomain(s string) bool {
|
||||
// URL safe base64 alphabet without padding as defined in ACME
|
||||
RegExp := regexp.MustCompile("^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$")
|
||||
return RegExp.MatchString(s)
|
||||
}
|
||||
|
||||
func validTXT(s string) bool {
|
||||
sn := acmedns.SanitizeString(s)
|
||||
if utf8.RuneCountInString(s) == 43 && utf8.RuneCountInString(sn) == 43 {
|
||||
// 43 chars is the current LE auth key size, but not limited / defined by ACME
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/joohoi/acme-dns/pkg/acmedns"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestGetValidUsername(t *testing.T) {
|
||||
v1, _ := uuid.Parse("a097455b-52cc-4569-90c8-7a4b97c6eba8")
|
||||
for i, test := range []struct {
|
||||
uname string
|
||||
output uuid.UUID
|
||||
shouldErr bool
|
||||
}{
|
||||
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", v1, false},
|
||||
{"a-97455b-52cc-4569-90c8-7a4b97c6eba8", uuid.UUID{}, true},
|
||||
{"", uuid.UUID{}, true},
|
||||
{"&!#!25123!%!'%", uuid.UUID{}, true},
|
||||
} {
|
||||
ret, err := getValidUsername(test.uname)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error, but there was none", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got [%v]", i, err)
|
||||
}
|
||||
if ret != test.output {
|
||||
t.Errorf("Test %d: Expected return value %v, but got %v", i, test.output, ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidKey(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
key string
|
||||
output bool
|
||||
}{
|
||||
{"", false},
|
||||
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true},
|
||||
{"aaaaaaaa-aaa-aaaaaa-aaaaaaaa-aaa_aacaaaa", true},
|
||||
{"aaaaaaaa-aaa-aaaaaa#aaaaaaaa-aaa_aacaaaa", false},
|
||||
{"aaaaaaaa-aaa-aaaaaa-aaaaaaaa-aaa_aacaaaaa", false},
|
||||
} {
|
||||
ret := validKey(test.key)
|
||||
if ret != test.output {
|
||||
t.Errorf("Test %d: Expected return value %t, but got %t", i, test.output, ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetValidSubdomain(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
subdomain string
|
||||
output bool
|
||||
}{
|
||||
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", true},
|
||||
{"a-97455b-52cc-4569-90c8-7a4b97c6eba8", true},
|
||||
{"foo.example.com", false},
|
||||
{"foo-example-com", true},
|
||||
{"", false},
|
||||
{"&!#!25123!%!'%", false},
|
||||
} {
|
||||
ret := validSubdomain(test.subdomain)
|
||||
if ret != test.output {
|
||||
t.Errorf("Test %d: Expected return value %t, but got %t", i, test.output, ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidTXT(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
txt string
|
||||
output bool
|
||||
}{
|
||||
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true},
|
||||
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false},
|
||||
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaa#aaaaaaaaaaaaaa", false},
|
||||
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false},
|
||||
{"", false},
|
||||
} {
|
||||
ret := validTXT(test.txt)
|
||||
if ret != test.output {
|
||||
t.Errorf("Test %d: Expected return value %t, but got %t", i, test.output, ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrectPassword(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
pw string
|
||||
hash string
|
||||
output bool
|
||||
}{
|
||||
{"PUrNTjU24JYNEOCeS2JcjaJGv1sinT80oV9--dpX",
|
||||
"$2a$10$ldVoGU5yrdlbPzuPUbUfleVovGjaRelP9tql0IltVUJk778gf.2tu",
|
||||
true},
|
||||
{"PUrNTjU24JYNEOCeS2JcjaJGv1sinT80oV9--dpX",
|
||||
"$2a$10$ldVoGU5yrdlbPzuPUbUfleVovGjaRelP9tql0IltVUJk778gf.2t",
|
||||
false},
|
||||
{"PUrNTjU24JYNEOCeS2JcjaJGv1sinT80oV9--dp",
|
||||
"$2a$10$ldVoGU5yrdlbPzuPUbUfleVovGjaRelP9tql0IltVUJk778gf.2tu",
|
||||
false},
|
||||
{"", "", false},
|
||||
} {
|
||||
ret := acmedns.CorrectPassword(test.pw, test.hash)
|
||||
if ret != test.output {
|
||||
t.Errorf("Test %d: Expected return value %t, but got %t", i, test.output, ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetValidCIDRMasks(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input acmedns.Cidrslice
|
||||
output acmedns.Cidrslice
|
||||
}{
|
||||
{acmedns.Cidrslice{"10.0.0.1/24"}, acmedns.Cidrslice{"10.0.0.1/24"}},
|
||||
{acmedns.Cidrslice{"invalid", "127.0.0.1/32"}, acmedns.Cidrslice{"127.0.0.1/32"}},
|
||||
{acmedns.Cidrslice{"2002:c0a8::0/32", "8.8.8.8/32"}, acmedns.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user