Re-added tests
This commit is contained in:
+21
-1
@@ -2,6 +2,7 @@ package acmedns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
@@ -20,7 +21,7 @@ func FileIsAccessible(fname string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func ReadConfig(fname string) (AcmeDnsConfig, error) {
|
||||
func readTomlConfig(fname string) (AcmeDnsConfig, error) {
|
||||
var conf AcmeDnsConfig
|
||||
_, err := toml.DecodeFile(fname, &conf)
|
||||
if err != nil {
|
||||
@@ -46,3 +47,22 @@ func prepareConfig(conf AcmeDnsConfig) (AcmeDnsConfig, error) {
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func ReadConfig(configFile string) (AcmeDnsConfig, string, error) {
|
||||
var usedConfigFile string
|
||||
var config AcmeDnsConfig
|
||||
var err error
|
||||
if FileIsAccessible(configFile) {
|
||||
usedConfigFile = configFile
|
||||
config, err = readTomlConfig(configFile)
|
||||
} else if FileIsAccessible("./config.cfg") {
|
||||
usedConfigFile = "./config.cfg"
|
||||
config, err = readTomlConfig("./config.cfg")
|
||||
} else {
|
||||
err = fmt.Errorf("configuration file not found")
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("encountered an error while trying to read configuration file: %s\n", err)
|
||||
}
|
||||
return config, usedConfigFile, err
|
||||
}
|
||||
|
||||
@@ -18,5 +18,6 @@ type AcmednsDB interface {
|
||||
type AcmednsNS interface {
|
||||
Start(errorChannel chan error)
|
||||
SetOwnAuthKey(key string)
|
||||
SetNotifyStartedFunc(func())
|
||||
ParseRecords()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package acmedns
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func SetupLogging(config AcmeDnsConfig) (*zap.Logger, error) {
|
||||
var logger *zap.Logger
|
||||
logformat := "console"
|
||||
if config.Logconfig.Format == "json" {
|
||||
logformat = "json"
|
||||
}
|
||||
outputPath := "stdout"
|
||||
if config.Logconfig.Logtype == "file" {
|
||||
outputPath = config.Logconfig.File
|
||||
}
|
||||
errorPath := "stderr"
|
||||
if config.Logconfig.Logtype == "file" {
|
||||
errorPath = config.Logconfig.File
|
||||
}
|
||||
zapConfigJson := fmt.Sprintf(`{
|
||||
"level": "%s",
|
||||
"encoding": "%s",
|
||||
"outputPaths": ["%s"],
|
||||
"errorOutputPaths": ["%s"],
|
||||
"encoderConfig": {
|
||||
"timeKey": "time",
|
||||
"messageKey": "msg",
|
||||
"levelKey": "level",
|
||||
"levelEncoder": "lowercase",
|
||||
"timeEncoder": "iso8601"
|
||||
}
|
||||
}`, config.Logconfig.Level, logformat, outputPath, errorPath)
|
||||
var zapCfg zap.Config
|
||||
if err := json.Unmarshal([]byte(zapConfigJson), &zapCfg); err != nil {
|
||||
return logger, err
|
||||
}
|
||||
logger, err := zapCfg.Build()
|
||||
return logger, err
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package acmedns
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"math/big"
|
||||
"regexp"
|
||||
)
|
||||
@@ -29,3 +30,10 @@ func generatePassword(length int) string {
|
||||
}
|
||||
return string(ret)
|
||||
}
|
||||
|
||||
func CorrectPassword(pw string, hash string) bool {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package acmedns
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func fakeConfig() AcmeDnsConfig {
|
||||
conf := AcmeDnsConfig{}
|
||||
conf.Logconfig.Logtype = "stdout"
|
||||
return conf
|
||||
}
|
||||
|
||||
func TestSetupLogging(t *testing.T) {
|
||||
conf := fakeConfig()
|
||||
for i, test := range []struct {
|
||||
format string
|
||||
level string
|
||||
expected zapcore.Level
|
||||
}{
|
||||
{"text", "warn", zap.WarnLevel},
|
||||
{"json", "debug", zap.DebugLevel},
|
||||
{"text", "info", zap.InfoLevel},
|
||||
{"json", "error", zap.ErrorLevel},
|
||||
} {
|
||||
conf.Logconfig.Format = test.format
|
||||
conf.Logconfig.Level = test.level
|
||||
logger, err := SetupLogging(conf)
|
||||
if err != nil {
|
||||
t.Errorf("Got unexpected error: %s", err)
|
||||
} else {
|
||||
if logger.Sugar().Level() != test.expected {
|
||||
t.Errorf("Test %d: Expected loglevel %s but got %s", i, test.expected, log.GetLevel().String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupLoggingError(t *testing.T) {
|
||||
conf := fakeConfig()
|
||||
for _, test := range []struct {
|
||||
format string
|
||||
level string
|
||||
errexpected bool
|
||||
}{
|
||||
{"text", "warn", false},
|
||||
{"json", "debug", false},
|
||||
{"text", "info", false},
|
||||
{"json", "error", false},
|
||||
{"text", "something", true},
|
||||
} {
|
||||
conf.Logconfig.Format = test.format
|
||||
conf.Logconfig.Level = test.level
|
||||
_, err := SetupLogging(conf)
|
||||
if test.errexpected && err == nil {
|
||||
t.Errorf("Expected error but did not get one for loglevel: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadConfig(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
inFile []byte
|
||||
output AcmeDnsConfig
|
||||
}{
|
||||
{
|
||||
[]byte("[general]\nlisten = \":53\"\ndebug = true\n[api]\napi_domain = \"something.strange\""),
|
||||
AcmeDnsConfig{
|
||||
General: general{
|
||||
Listen: ":53",
|
||||
Debug: true,
|
||||
},
|
||||
API: httpapi{
|
||||
Domain: "something.strange",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
[]byte("[\x00[[[[[[[[[de\nlisten =]"),
|
||||
AcmeDnsConfig{},
|
||||
},
|
||||
} {
|
||||
tmpfile, err := os.CreateTemp("", "acmedns")
|
||||
if err != nil {
|
||||
t.Error("Could not create temporary file")
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.Write(test.inFile); err != nil {
|
||||
t.Error("Could not write to temporary file")
|
||||
}
|
||||
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Error("Could not close temporary file")
|
||||
}
|
||||
ret, _, _ := ReadConfig(tmpfile.Name())
|
||||
if ret.General.Listen != test.output.General.Listen {
|
||||
t.Errorf("Test %d: Expected listen value %s, but got %s", i, test.output.General.Listen, ret.General.Listen)
|
||||
}
|
||||
if ret.API.Domain != test.output.API.Domain {
|
||||
t.Errorf("Test %d: Expected HTTP API domain %s, but got %s", i, test.output.API.Domain, ret.API.Domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileCheckPermissionDenied(t *testing.T) {
|
||||
tmpfile, err := ioutil.TempFile("", "acmedns")
|
||||
if err != nil {
|
||||
t.Error("Could not create temporary file")
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
_ = syscall.Chmod(tmpfile.Name(), 0000)
|
||||
if FileIsAccessible(tmpfile.Name()) {
|
||||
t.Errorf("File should not be accessible")
|
||||
}
|
||||
_ = syscall.Chmod(tmpfile.Name(), 0644)
|
||||
}
|
||||
|
||||
func TestFileCheckNotExists(t *testing.T) {
|
||||
if FileIsAccessible("/path/that/does/not/exist") {
|
||||
t.Errorf("File should not be accessible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileCheckOK(t *testing.T) {
|
||||
tmpfile, err := ioutil.TempFile("", "acmedns")
|
||||
if err != nil {
|
||||
t.Error("Could not create temporary file")
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
if !FileIsAccessible(tmpfile.Name()) {
|
||||
t.Errorf("File should be accessible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareConfig(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input AcmeDnsConfig
|
||||
shoulderror bool
|
||||
}{
|
||||
{AcmeDnsConfig{Database: dbsettings{Engine: "whatever", Connection: "whatever_too"}}, false},
|
||||
{AcmeDnsConfig{Database: dbsettings{Engine: "", Connection: "whatever_too"}}, true},
|
||||
{AcmeDnsConfig{Database: dbsettings{Engine: "whatever", Connection: ""}}, true},
|
||||
} {
|
||||
_, err := prepareConfig(test.input)
|
||||
if test.shoulderror {
|
||||
if err == nil {
|
||||
t.Errorf("Test %d: Expected error with prepareConfig input data [%v]", i, test.input)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: Expected no error with prepareConfig input data [%v]", i, test.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/acme-dns/acme-dns/pkg/acmedns"
|
||||
"github.com/acme-dns/acme-dns/pkg/database"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"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 = "none"
|
||||
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": []interface{}{"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": []interface{}{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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -74,11 +74,11 @@ func (a *AcmednsAPI) getUserFromRequest(r *http.Request) (acmedns.ACMETxt, error
|
||||
a.Logger.Errorw("Error while trying to get user",
|
||||
"error", err.Error())
|
||||
// To protect against timed side channel (never gonna give you up)
|
||||
correctPassword(passwd, "$2a$10$8JEFVNYYhLoBysjAxe2yBuXrkDojBQBkVpXEQgyQyjn43SvJ4vL36")
|
||||
acmedns.CorrectPassword(passwd, "$2a$10$8JEFVNYYhLoBysjAxe2yBuXrkDojBQBkVpXEQgyQyjn43SvJ4vL36")
|
||||
|
||||
return acmedns.ACMETxt{}, fmt.Errorf("invalid username: %s", uname)
|
||||
}
|
||||
if correctPassword(passwd, dbuser.Password) {
|
||||
if acmedns.CorrectPassword(passwd, dbuser.Password) {
|
||||
return dbuser, nil
|
||||
}
|
||||
return acmedns.ACMETxt{}, fmt.Errorf("invalid password for user %s", uname)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"github.com/acme-dns/acme-dns/pkg/acmedns"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -31,13 +30,6 @@ func validKey(k string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func correctPassword(pw string, hash string) bool {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getIPListFromHeader(header string) []string {
|
||||
iplist := []string{}
|
||||
for _, v := range strings.Split(header, ",") {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/acme-dns/acme-dns/pkg/acmedns"
|
||||
"testing"
|
||||
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-33
@@ -1,10 +1,12 @@
|
||||
package main
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"github.com/acme-dns/acme-dns/pkg/acmedns"
|
||||
"github.com/erikstmartin/go-testdb"
|
||||
"go.uber.org/zap"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -21,42 +23,38 @@ func (r testResult) RowsAffected() (int64, error) {
|
||||
return r.affectedRows, nil
|
||||
}
|
||||
|
||||
func TestDBInit(t *testing.T) {
|
||||
fakeDB := new(acmedb)
|
||||
err := fakeDB.Init("notarealegine", "connectionstring")
|
||||
if err == nil {
|
||||
t.Errorf("Was expecting error, didn't get one.")
|
||||
}
|
||||
func fakeConfigAndLogger() (acmedns.AcmeDnsConfig, *zap.SugaredLogger) {
|
||||
c := acmedns.AcmeDnsConfig{}
|
||||
c.Database.Engine = "sqlite"
|
||||
c.Database.Connection = ":memory:"
|
||||
l := zap.NewNop().Sugar()
|
||||
return c, l
|
||||
}
|
||||
|
||||
testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) {
|
||||
return testResult{1, 0}, errors.New("Prepared query error")
|
||||
})
|
||||
defer testdb.Reset()
|
||||
|
||||
errorDB := new(acmedb)
|
||||
err = errorDB.Init("testdb", "")
|
||||
if err == nil {
|
||||
t.Errorf("Was expecting DB initiation error but got none")
|
||||
}
|
||||
errorDB.Close()
|
||||
func fakeDB() acmedns.AcmednsDB {
|
||||
conf, logger := fakeConfigAndLogger()
|
||||
db, _ := Init(&conf, logger)
|
||||
return db
|
||||
}
|
||||
|
||||
func TestRegisterNoCIDR(t *testing.T) {
|
||||
// Register tests
|
||||
_, err := DB.Register(cidrslice{})
|
||||
DB := fakeDB()
|
||||
_, err := DB.Register(acmedns.Cidrslice{})
|
||||
if err != nil {
|
||||
t.Errorf("Registration failed, got error [%v]", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterMany(t *testing.T) {
|
||||
DB := fakeDB()
|
||||
for i, test := range []struct {
|
||||
input cidrslice
|
||||
output cidrslice
|
||||
input acmedns.Cidrslice
|
||||
output acmedns.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"}},
|
||||
{acmedns.Cidrslice{"127.0.0.1/8", "8.8.8.8/32", "1.0.0.1/1"}, acmedns.Cidrslice{"127.0.0.1/8", "8.8.8.8/32", "1.0.0.1/1"}},
|
||||
{acmedns.Cidrslice{"1.1.1./32", "1922.168.42.42/8", "1.1.1.1/33", "1.2.3.4/"}, acmedns.Cidrslice{}},
|
||||
{acmedns.Cidrslice{"7.6.5.4/32", "invalid", "1.0.0.1/2"}, acmedns.Cidrslice{"7.6.5.4/32", "1.0.0.1/2"}},
|
||||
} {
|
||||
user, err := DB.Register(test.input)
|
||||
if err != nil {
|
||||
@@ -77,8 +75,9 @@ func TestRegisterMany(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetByUsername(t *testing.T) {
|
||||
DB := fakeDB()
|
||||
// Create reg to refer to
|
||||
reg, err := DB.Register(cidrslice{})
|
||||
reg, err := DB.Register(acmedns.Cidrslice{})
|
||||
if err != nil {
|
||||
t.Errorf("Registration failed, got error [%v]", err)
|
||||
}
|
||||
@@ -97,13 +96,14 @@ func TestGetByUsername(t *testing.T) {
|
||||
}
|
||||
|
||||
// regUser password already is a bcrypt hash
|
||||
if !correctPassword(reg.Password, regUser.Password) {
|
||||
if !acmedns.CorrectPassword(reg.Password, regUser.Password) {
|
||||
t.Errorf("The password [%s] does not match the hash [%s]", reg.Password, regUser.Password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareErrors(t *testing.T) {
|
||||
reg, _ := DB.Register(cidrslice{})
|
||||
DB := fakeDB()
|
||||
reg, _ := DB.Register(acmedns.Cidrslice{})
|
||||
tdb, err := sql.Open("testdb", "")
|
||||
if err != nil {
|
||||
t.Errorf("Got error: %v", err)
|
||||
@@ -125,7 +125,8 @@ func TestPrepareErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestQueryExecErrors(t *testing.T) {
|
||||
reg, _ := DB.Register(cidrslice{})
|
||||
DB := fakeDB()
|
||||
reg, _ := DB.Register(acmedns.Cidrslice{})
|
||||
testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) {
|
||||
return testResult{1, 0}, errors.New("Prepared query error")
|
||||
})
|
||||
@@ -156,7 +157,7 @@ func TestQueryExecErrors(t *testing.T) {
|
||||
t.Errorf("Expected error from exec in GetByDomain, but got none")
|
||||
}
|
||||
|
||||
_, err = DB.Register(cidrslice{})
|
||||
_, err = DB.Register(acmedns.Cidrslice{})
|
||||
if err == nil {
|
||||
t.Errorf("Expected error from exec in Register, but got none")
|
||||
}
|
||||
@@ -169,7 +170,8 @@ func TestQueryExecErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestQueryScanErrors(t *testing.T) {
|
||||
reg, _ := DB.Register(cidrslice{})
|
||||
DB := fakeDB()
|
||||
reg, _ := DB.Register(acmedns.Cidrslice{})
|
||||
|
||||
testdb.SetExecWithArgsFunc(func(query string, args []driver.Value) (result driver.Result, err error) {
|
||||
return testResult{1, 0}, errors.New("Prepared query error")
|
||||
@@ -198,7 +200,8 @@ func TestQueryScanErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBadDBValues(t *testing.T) {
|
||||
reg, _ := DB.Register(cidrslice{})
|
||||
DB := fakeDB()
|
||||
reg, _ := DB.Register(acmedns.Cidrslice{})
|
||||
|
||||
testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) {
|
||||
columns := []string{"Username", "Password", "Subdomain", "Value", "LastActive"}
|
||||
@@ -228,8 +231,9 @@ func TestBadDBValues(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetTXTForDomain(t *testing.T) {
|
||||
DB := fakeDB()
|
||||
// Create reg to refer to
|
||||
reg, err := DB.Register(cidrslice{})
|
||||
reg, err := DB.Register(acmedns.Cidrslice{})
|
||||
if err != nil {
|
||||
t.Errorf("Registration failed, got error [%v]", err)
|
||||
}
|
||||
@@ -276,8 +280,9 @@ func TestGetTXTForDomain(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
DB := fakeDB()
|
||||
// Create reg to refer to
|
||||
reg, err := DB.Register(cidrslice{})
|
||||
reg, err := DB.Register(acmedns.Cidrslice{})
|
||||
if err != nil {
|
||||
t.Errorf("Registration failed, got error [%v]", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/acme-dns/acme-dns/pkg/acmedns"
|
||||
"github.com/acme-dns/acme-dns/pkg/database"
|
||||
"github.com/erikstmartin/go-testdb"
|
||||
"github.com/miekg/dns"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest/observer"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type resolver struct {
|
||||
server string
|
||||
}
|
||||
|
||||
var records = []string{
|
||||
"auth.example.org. A 192.168.1.100",
|
||||
"ns1.auth.example.org. A 192.168.1.101",
|
||||
"cn.example.org CNAME something.example.org.",
|
||||
"!''b', unparseable ",
|
||||
"ns2.auth.example.org. A 192.168.1.102",
|
||||
}
|
||||
|
||||
func loggerHasEntryWithMessage(message string, logObserver *observer.ObservedLogs) bool {
|
||||
if len(logObserver.FilterMessage(message).All()) > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fakeConfigAndLogger() (acmedns.AcmeDnsConfig, *zap.SugaredLogger, *observer.ObservedLogs) {
|
||||
c := acmedns.AcmeDnsConfig{}
|
||||
c.Database.Engine = "sqlite"
|
||||
c.Database.Connection = ":memory:"
|
||||
obsCore, logObserver := observer.New(zap.DebugLevel)
|
||||
obsLogger := zap.New(obsCore).Sugar()
|
||||
return c, obsLogger, logObserver
|
||||
}
|
||||
|
||||
func setupDNS() (acmedns.AcmednsNS, acmedns.AcmednsDB, *observer.ObservedLogs) {
|
||||
config, logger, logObserver := fakeConfigAndLogger()
|
||||
config.General.Domain = "auth.example.org"
|
||||
config.General.Listen = "127.0.0.1:15353"
|
||||
config.General.Proto = "udp"
|
||||
config.General.Nsname = "ns1.auth.example.org"
|
||||
config.General.Nsadmin = "admin.example.org"
|
||||
config.General.StaticRecords = records
|
||||
config.General.Debug = false
|
||||
db, _ := database.Init(&config, logger)
|
||||
server := Nameserver{Config: &config, DB: db, Logger: logger, personalAuthKey: ""}
|
||||
server.Domains = make(map[string]Records)
|
||||
server.Server = &dns.Server{Addr: config.General.Listen, Net: config.General.Proto}
|
||||
server.ParseRecords()
|
||||
server.OwnDomain = "auth.example.org."
|
||||
return &server, db, logObserver
|
||||
}
|
||||
|
||||
func (r *resolver) lookup(host string, qtype uint16) (*dns.Msg, error) {
|
||||
msg := new(dns.Msg)
|
||||
msg.Id = dns.Id()
|
||||
msg.Question = make([]dns.Question, 1)
|
||||
msg.Question[0] = dns.Question{Name: dns.Fqdn(host), Qtype: qtype, Qclass: dns.ClassINET}
|
||||
in, err := dns.Exchange(msg, r.server)
|
||||
if err != nil {
|
||||
return in, fmt.Errorf("Error querying the server [%v]", err)
|
||||
}
|
||||
if in != nil && in.Rcode != dns.RcodeSuccess {
|
||||
return in, fmt.Errorf("Received error from the server [%s]", dns.RcodeToString[in.Rcode])
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func hasExpectedTXTAnswer(answer []dns.RR, cmpTXT string) error {
|
||||
for _, record := range answer {
|
||||
// We expect only one answer, so no need to loop through the answer slice
|
||||
if rec, ok := record.(*dns.TXT); ok {
|
||||
for _, txtValue := range rec.Txt {
|
||||
if txtValue == cmpTXT {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errmsg := fmt.Sprintf("Got answer of unexpected type [%q]", answer[0])
|
||||
return errors.New(errmsg)
|
||||
}
|
||||
}
|
||||
return errors.New("Expected answer not found")
|
||||
}
|
||||
|
||||
func TestQuestionDBError(t *testing.T) {
|
||||
config, logger, _ := fakeConfigAndLogger()
|
||||
config.General.Listen = "127.0.0.1:15353"
|
||||
config.General.Proto = "udp"
|
||||
config.General.Domain = "auth.example.org"
|
||||
config.General.Nsname = "ns1.auth.example.org"
|
||||
config.General.Nsadmin = "admin.example.org"
|
||||
config.General.StaticRecords = records
|
||||
config.General.Debug = false
|
||||
db, _ := database.Init(&config, logger)
|
||||
server := Nameserver{Config: &config, DB: db, Logger: logger, personalAuthKey: ""}
|
||||
server.Domains = make(map[string]Records)
|
||||
server.ParseRecords()
|
||||
testdb.SetQueryWithArgsFunc(func(query string, args []driver.Value) (result driver.Rows, err error) {
|
||||
columns := []string{"Username", "Password", "Subdomain", "Value", "LastActive"}
|
||||
return testdb.RowsFromSlice(columns, [][]driver.Value{}), errors.New("Prepared query error")
|
||||
})
|
||||
|
||||
defer testdb.Reset()
|
||||
|
||||
tdb, err := sql.Open("testdb", "")
|
||||
if err != nil {
|
||||
t.Errorf("Got error: %v", err)
|
||||
}
|
||||
oldDb := db.GetBackend()
|
||||
|
||||
db.SetBackend(tdb)
|
||||
defer db.SetBackend(oldDb)
|
||||
|
||||
q := dns.Question{Name: dns.Fqdn("whatever.tld"), Qtype: dns.TypeTXT, Qclass: dns.ClassINET}
|
||||
_, err = server.answerTXT(q)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
config, logger, logObserver := fakeConfigAndLogger()
|
||||
config.General.Listen = "127.0.0.1:15353"
|
||||
config.General.Proto = "udp"
|
||||
config.General.Domain = ")"
|
||||
config.General.Nsname = "ns1.auth.example.org"
|
||||
config.General.Nsadmin = "admin.example.org"
|
||||
config.General.StaticRecords = records
|
||||
config.General.Debug = false
|
||||
config.General.StaticRecords = []string{}
|
||||
db, _ := database.Init(&config, logger)
|
||||
server := Nameserver{Config: &config, DB: db, Logger: logger, personalAuthKey: ""}
|
||||
server.Domains = make(map[string]Records)
|
||||
server.ParseRecords()
|
||||
if !loggerHasEntryWithMessage("Error while adding SOA record", logObserver) {
|
||||
t.Errorf("Expected SOA parsing to return error, but did not find one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveA(t *testing.T) {
|
||||
server, _, _ := setupDNS()
|
||||
errChan := make(chan error, 1)
|
||||
waitLock := sync.Mutex{}
|
||||
waitLock.Lock()
|
||||
server.SetNotifyStartedFunc(waitLock.Unlock)
|
||||
go server.Start(errChan)
|
||||
waitLock.Lock()
|
||||
resolv := resolver{server: "127.0.0.1:15353"}
|
||||
answer, err := resolv.lookup("auth.example.org", dns.TypeA)
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(answer.Answer) == 0 {
|
||||
t.Error("No answer for DNS query")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = resolv.lookup("nonexistent.domain.tld", dns.TypeA)
|
||||
if err == nil {
|
||||
t.Errorf("Was expecting error because of NXDOMAIN but got none")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestEDNS(t *testing.T) {
|
||||
resolv := resolver{server: "127.0.0.1:15353"}
|
||||
answer, _ := resolv.lookup("auth.example.org", dns.TypeOPT)
|
||||
if answer.Rcode != dns.RcodeSuccess {
|
||||
t.Errorf("Was expecing NOERROR rcode for OPT query, but got [%s] instead.", dns.RcodeToString[answer.Rcode])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEDNSA(t *testing.T) {
|
||||
msg := new(dns.Msg)
|
||||
msg.Id = dns.Id()
|
||||
msg.Question = make([]dns.Question, 1)
|
||||
msg.Question[0] = dns.Question{Name: dns.Fqdn("auth.example.org"), Qtype: dns.TypeA, Qclass: dns.ClassINET}
|
||||
// Set EDNS0 with DO=1
|
||||
msg.SetEdns0(512, true)
|
||||
in, err := dns.Exchange(msg, "127.0.0.1:15353")
|
||||
if err != nil {
|
||||
t.Errorf("Error querying the server [%v]", err)
|
||||
}
|
||||
if in != nil && in.Rcode != dns.RcodeSuccess {
|
||||
t.Errorf("Received error from the server [%s]", dns.RcodeToString[in.Rcode])
|
||||
}
|
||||
opt := in.IsEdns0()
|
||||
if opt == nil {
|
||||
t.Errorf("Should have got OPT back")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEDNSBADVERS(t *testing.T) {
|
||||
msg := new(dns.Msg)
|
||||
msg.Id = dns.Id()
|
||||
msg.Question = make([]dns.Question, 1)
|
||||
msg.Question[0] = dns.Question{Name: dns.Fqdn("auth.example.org"), Qtype: dns.TypeA, Qclass: dns.ClassINET}
|
||||
// Set EDNS0 with version 1
|
||||
o := new(dns.OPT)
|
||||
o.SetVersion(1)
|
||||
o.Hdr.Name = "."
|
||||
o.Hdr.Rrtype = dns.TypeOPT
|
||||
msg.Extra = append(msg.Extra, o)
|
||||
in, err := dns.Exchange(msg, "127.0.0.1:15353")
|
||||
if err != nil {
|
||||
t.Errorf("Error querying the server [%v]", err)
|
||||
}
|
||||
if in != nil && in.Rcode != dns.RcodeBadVers {
|
||||
t.Errorf("Received unexpected rcode from the server [%s]", dns.RcodeToString[in.Rcode])
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCNAME(t *testing.T) {
|
||||
resolv := resolver{server: "127.0.0.1:15353"}
|
||||
expected := "cn.example.org. 3600 IN CNAME something.example.org."
|
||||
answer, err := resolv.lookup("cn.example.org", dns.TypeCNAME)
|
||||
if err != nil {
|
||||
t.Errorf("Got unexpected error: %s", err)
|
||||
}
|
||||
if len(answer.Answer) != 1 {
|
||||
t.Errorf("Expected exactly 1 RR in answer, but got %d instead.", len(answer.Answer))
|
||||
}
|
||||
if answer.Answer[0].Header().Rrtype != dns.TypeCNAME {
|
||||
t.Errorf("Expected a CNAME answer, but got [%s] instead.", dns.TypeToString[answer.Answer[0].Header().Rrtype])
|
||||
}
|
||||
if answer.Answer[0].String() != expected {
|
||||
t.Errorf("Expected CNAME answer [%s] but got [%s] instead.", expected, answer.Answer[0].String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthoritative(t *testing.T) {
|
||||
resolv := resolver{server: "127.0.0.1:15353"}
|
||||
answer, _ := resolv.lookup("nonexistent.auth.example.org", dns.TypeA)
|
||||
if answer.Rcode != dns.RcodeNameError {
|
||||
t.Errorf("Was expecing NXDOMAIN rcode, but got [%s] instead.", dns.RcodeToString[answer.Rcode])
|
||||
}
|
||||
if len(answer.Ns) != 1 {
|
||||
t.Errorf("Was expecting exactly one answer (SOA) for invalid subdomain, but got %d", len(answer.Ns))
|
||||
}
|
||||
if answer.Ns[0].Header().Rrtype != dns.TypeSOA {
|
||||
t.Errorf("Was expecting SOA record as answer for NXDOMAIN but got [%s]", dns.TypeToString[answer.Ns[0].Header().Rrtype])
|
||||
}
|
||||
if !answer.MsgHdr.Authoritative {
|
||||
t.Errorf("Was expecting authoritative bit to be set")
|
||||
}
|
||||
nanswer, _ := resolv.lookup("nonexsitent.nonauth.tld", dns.TypeA)
|
||||
if len(nanswer.Answer) > 0 {
|
||||
t.Errorf("Didn't expect answers for non authotitative domain query")
|
||||
}
|
||||
if nanswer.MsgHdr.Authoritative {
|
||||
t.Errorf("Authoritative bit should not be set for non-authoritative domain.")
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func TestResolveTXT(t *testing.T) {
|
||||
_, db, _ := setupDNS()
|
||||
resolv := resolver{server: "127.0.0.1:15353"}
|
||||
validTXT := "______________valid_response_______________"
|
||||
|
||||
atxt, err := db.Register(acmedns.Cidrslice{})
|
||||
if err != nil {
|
||||
t.Errorf("Could not initiate db record: [%v]", err)
|
||||
return
|
||||
}
|
||||
atxt.Value = validTXT
|
||||
|
||||
err = db.Update(atxt.ACMETxtPost)
|
||||
if err != nil {
|
||||
t.Errorf("Could not update db record: [%v]", err)
|
||||
return
|
||||
}
|
||||
|
||||
for i, test := range []struct {
|
||||
subDomain string
|
||||
expTXT string
|
||||
getAnswer bool
|
||||
validAnswer bool
|
||||
}{
|
||||
{atxt.Subdomain, validTXT, true, true},
|
||||
{atxt.Subdomain, "invalid", true, false},
|
||||
{"a097455b-52cc-4569-90c8-7a4b97c6eba8", validTXT, false, false},
|
||||
} {
|
||||
answer, err := resolv.lookup(test.subDomain+".auth.example.org", dns.TypeTXT)
|
||||
if err != nil {
|
||||
if test.getAnswer {
|
||||
t.Fatalf("Test %d: Expected answer but got: %v", i, err)
|
||||
}
|
||||
} else {
|
||||
if !test.getAnswer {
|
||||
t.Errorf("Test %d: Expected no answer, but got one.", i)
|
||||
}
|
||||
}
|
||||
|
||||
if len(answer.Answer) > 0 {
|
||||
if !test.getAnswer && answer.Answer[0].Header().Rrtype != dns.TypeSOA {
|
||||
t.Errorf("Test %d: Expected no answer, but got: [%q]", i, answer)
|
||||
}
|
||||
if test.getAnswer {
|
||||
err = hasExpectedTXTAnswer(answer.Answer, test.expTXT)
|
||||
if err != nil {
|
||||
if test.validAnswer {
|
||||
t.Errorf("Test %d: %v", i, err)
|
||||
}
|
||||
} else {
|
||||
if !test.validAnswer {
|
||||
t.Errorf("Test %d: Answer was not expected to be valid, answer [%q], compared to [%s]", i, answer, test.expTXT)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if test.getAnswer {
|
||||
t.Errorf("Test %d: Expected answer, but didn't get one", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
func TestCaseInsensitiveResolveA(t *testing.T) {
|
||||
resolv := resolver{server: "127.0.0.1:15353"}
|
||||
answer, err := resolv.lookup("aUtH.eXAmpLe.org", dns.TypeA)
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
|
||||
if len(answer.Answer) == 0 {
|
||||
t.Error("No answer for DNS query")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseInsensitiveResolveSOA(t *testing.T) {
|
||||
resolv := resolver{server: "127.0.0.1:15353"}
|
||||
answer, _ := resolv.lookup("doesnotexist.aUtH.eXAmpLe.org", dns.TypeSOA)
|
||||
if answer.Rcode != dns.RcodeNameError {
|
||||
t.Errorf("Was expecing NXDOMAIN rcode, but got [%s] instead.", dns.RcodeToString[answer.Rcode])
|
||||
}
|
||||
|
||||
if len(answer.Ns) == 0 {
|
||||
t.Error("No SOA answer for DNS query")
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
func (n *Nameserver) handleRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
|
||||
// handle edns0
|
||||
opt := r.IsEdns0()
|
||||
if opt != nil {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/acme-dns/acme-dns/pkg/acmedns"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"go.uber.org/zap"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Records is a slice of ResourceRecords
|
||||
@@ -15,21 +14,23 @@ type Records struct {
|
||||
}
|
||||
|
||||
type Nameserver struct {
|
||||
Config *acmedns.AcmeDnsConfig
|
||||
DB acmedns.AcmednsDB
|
||||
Logger *zap.SugaredLogger
|
||||
Server *dns.Server
|
||||
OwnDomain string
|
||||
SOA dns.RR
|
||||
personalAuthKey string
|
||||
Domains map[string]Records
|
||||
errChan chan error
|
||||
Config *acmedns.AcmeDnsConfig
|
||||
DB acmedns.AcmednsDB
|
||||
Logger *zap.SugaredLogger
|
||||
Server *dns.Server
|
||||
OwnDomain string
|
||||
NotifyStartedFunc func()
|
||||
SOA dns.RR
|
||||
personalAuthKey string
|
||||
Domains map[string]Records
|
||||
errChan chan error
|
||||
}
|
||||
|
||||
func InitAndStart(config *acmedns.AcmeDnsConfig, db acmedns.AcmednsDB, logger *zap.SugaredLogger, errChan chan error) []acmedns.AcmednsNS {
|
||||
dnsservers := make([]acmedns.AcmednsNS, 0)
|
||||
|
||||
waitLock := sync.Mutex{}
|
||||
if strings.HasPrefix(config.General.Proto, "both") {
|
||||
|
||||
// Handle the case where DNS server should be started for both udp and tcp
|
||||
udpProto := "udp"
|
||||
tcpProto := "tcp"
|
||||
@@ -46,13 +47,22 @@ func InitAndStart(config *acmedns.AcmeDnsConfig, db acmedns.AcmednsDB, logger *z
|
||||
dnsServerTCP := NewDNSServer(config, db, logger, tcpProto)
|
||||
dnsservers = append(dnsservers, dnsServerTCP)
|
||||
dnsServerTCP.ParseRecords()
|
||||
// wait for the server to get started to proceed
|
||||
waitLock.Lock()
|
||||
dnsServerUDP.SetNotifyStartedFunc(waitLock.Unlock)
|
||||
go dnsServerUDP.Start(errChan)
|
||||
waitLock.Lock()
|
||||
dnsServerTCP.SetNotifyStartedFunc(waitLock.Unlock)
|
||||
go dnsServerTCP.Start(errChan)
|
||||
waitLock.Lock()
|
||||
} else {
|
||||
dnsServer := NewDNSServer(config, db, logger, config.General.Proto)
|
||||
dnsservers = append(dnsservers, dnsServer)
|
||||
dnsServer.ParseRecords()
|
||||
waitLock.Lock()
|
||||
dnsServer.SetNotifyStartedFunc(waitLock.Unlock)
|
||||
go dnsServer.Start(errChan)
|
||||
waitLock.Lock()
|
||||
}
|
||||
return dnsservers
|
||||
}
|
||||
@@ -67,7 +77,6 @@ func NewDNSServer(config *acmedns.AcmeDnsConfig, db acmedns.AcmednsDB, logger *z
|
||||
domain = domain + "."
|
||||
}
|
||||
server.OwnDomain = strings.ToLower(domain)
|
||||
server.DB = db
|
||||
server.personalAuthKey = ""
|
||||
server.Domains = make(map[string]Records)
|
||||
return &server
|
||||
@@ -79,8 +88,15 @@ func (n *Nameserver) Start(errorChannel chan error) {
|
||||
n.Logger.Infow("Starting DNS listener",
|
||||
"addr", n.Server.Addr,
|
||||
"proto", n.Server.Net)
|
||||
if n.NotifyStartedFunc != nil {
|
||||
n.Server.NotifyStartedFunc = n.NotifyStartedFunc
|
||||
}
|
||||
err := n.Server.ListenAndServe()
|
||||
if err != nil {
|
||||
errorChannel <- err
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nameserver) SetNotifyStartedFunc(fun func()) {
|
||||
n.Server.NotifyStartedFunc = fun
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user