Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to delete txt records #245

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,42 @@ func webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
w.Write(upd)
}

func webDeletePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var delStatus int
var del []byte
// Get user
a, ok := r.Context().Value(ACMETxtKey).(ACMETxt)
if !ok {
log.WithFields(log.Fields{"error": "context"}).Error("Context error")
}
// NOTE: An invalid subdomain should not happen - the auth handler should
// reject POSTs with an invalid subdomain before this handler. Reject any
// invalid subdomains anyway as a matter of caution.
if !validSubdomain(a.Subdomain) {
log.WithFields(log.Fields{"error": "subdomain", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad delete data")
delStatus = http.StatusBadRequest
del = jsonError("bad_subdomain")
} else if !validTXT(a.Value) {
log.WithFields(log.Fields{"error": "txt", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad delete data")
delStatus = http.StatusBadRequest
del = jsonError("bad_txt")
} else if validSubdomain(a.Subdomain) && validTXT(a.Value) {
err := DB.Delete(a.ACMETxtPost)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Debug("Error while trying to delete record")
delStatus = http.StatusInternalServerError
del = jsonError("db_error")
} else {
log.WithFields(log.Fields{"subdomain": a.Subdomain, "txt": a.Value}).Debug("TXT deleted")
delStatus = http.StatusOK
del = []byte("{\"txt\": \"" + a.Value + "\"}")
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(delStatus)
w.Write(del)
}

// Endpoint used to check the readiness and/or liveness (health) of the server.
func healthCheck(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
w.WriteHeader(http.StatusOK)
Expand Down
250 changes: 250 additions & 0 deletions api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ func setupRouter(debug bool, noauth bool) http.Handler {
api.GET("/health", healthCheck)
if noauth {
api.POST("/update", noAuth(webUpdatePost))
api.POST("/delete", noAuth(webDeletePost))
} else {
api.POST("/update", Auth(webUpdatePost))
api.POST("/delete", Auth(webDeletePost))
}
return c.Handler(api)
}
Expand Down Expand Up @@ -221,6 +223,36 @@ func TestApiUpdateWithInvalidSubdomain(t *testing.T) {
ValueEqual("error", "forbidden")
}

func TestApiDeleteWithInvalidSubdomain(t *testing.T) {
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

updateJSON := map[string]interface{}{
"subdomain": "",
"txt": ""}

router := setupRouter(false, false)
server := httptest.NewServer(router)
defer server.Close()
e := getExpect(t, server)
newUser, err := DB.Register(cidrslice{})
if err != nil {
t.Errorf("Could not create new user, got error [%v]", err)
}
// Invalid subdomain data
updateJSON["subdomain"] = "example.com"
updateJSON["txt"] = validTxtData
e.POST("/delete").
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"

Expand Down Expand Up @@ -251,6 +283,36 @@ func TestApiUpdateWithInvalidTxt(t *testing.T) {
ValueEqual("error", "bad_txt")
}

func TestApiDeleteWithInvalidTxt(t *testing.T) {
invalidTXTData := "idk m8 bbl lmao"

updateJSON := map[string]interface{}{
"subdomain": "",
"txt": ""}

router := setupRouter(false, false)
server := httptest.NewServer(router)
defer server.Close()
e := getExpect(t, server)
newUser, err := DB.Register(cidrslice{})
if err != nil {
t.Errorf("Could not create new user, got error [%v]", err)
}
updateJSON["subdomain"] = newUser.Subdomain
// Invalid txt data
updateJSON["txt"] = invalidTXTData
e.POST("/delete").
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)
Expand All @@ -263,6 +325,18 @@ func TestApiUpdateWithoutCredentials(t *testing.T) {
NotContainsKey("txt")
}

func TestApiDeleteWithoutCredentials(t *testing.T) {
router := setupRouter(false, false)
server := httptest.NewServer(router)
defer server.Close()
e := getExpect(t, server)
e.POST("/delete").Expect().
Status(http.StatusUnauthorized).
JSON().Object().
ContainsKey("error").
NotContainsKey("txt")
}

func TestApiUpdateWithCredentials(t *testing.T) {
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

Expand Down Expand Up @@ -293,6 +367,36 @@ func TestApiUpdateWithCredentials(t *testing.T) {
ValueEqual("txt", validTxtData)
}

func TestApiDeleteWithCredentials(t *testing.T) {
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

updateJSON := map[string]interface{}{
"subdomain": "",
"txt": ""}

router := setupRouter(false, false)
server := httptest.NewServer(router)
defer server.Close()
e := getExpect(t, server)
newUser, err := DB.Register(cidrslice{})
if err != nil {
t.Errorf("Could not create new user, got error [%v]", err)
}
// Valid data
updateJSON["subdomain"] = newUser.Subdomain
updateJSON["txt"] = validTxtData
e.POST("/delete").
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{}{
Expand Down Expand Up @@ -322,6 +426,35 @@ func TestApiUpdateWithCredentialsMockDB(t *testing.T) {
DB.SetBackend(oldDb)
}

func TestApiDeleteWithCredentialsMockDB(t *testing.T) {
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
updateJSON := map[string]interface{}{
"subdomain": "",
"txt": ""}

// Valid data
updateJSON["subdomain"] = "a097455b-52cc-4569-90c8-7a4b97c6eba8"
updateJSON["txt"] = validTxtData

router := setupRouter(false, true)
server := httptest.NewServer(router)
defer server.Close()
e := getExpect(t, server)
oldDb := DB.GetBackend()
db, mock, _ := sqlmock.New()
DB.SetBackend(db)
defer db.Close()
mock.ExpectBegin()
mock.ExpectPrepare("UPDATE records").WillReturnError(errors.New("error"))
e.POST("/delete").
WithJSON(updateJSON).
Expect().
Status(http.StatusInternalServerError).
JSON().Object().
ContainsKey("error")
DB.SetBackend(oldDb)
}

func TestApiManyUpdateWithCredentials(t *testing.T) {
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

Expand Down Expand Up @@ -383,6 +516,67 @@ func TestApiManyUpdateWithCredentials(t *testing.T) {
}
}

func TestApiManyDeleteWithCredentials(t *testing.T) {
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

updateJSON := map[string]interface{}{
"subdomain": "",
"txt": ""}

router := setupRouter(true, false)
server := httptest.NewServer(router)
defer server.Close()
e := getExpect(t, server)
// User without defined CIDR masks
newUser, err := DB.Register(cidrslice{})
if err != nil {
t.Errorf("Could not create new user, got error [%v]", err)
}

// User with defined allow from - CIDR masks, all invalid
// (httpexpect doesn't provide a way to mock remote ip)
newUserWithCIDR, err := DB.Register(cidrslice{"192.168.1.1/32", "invalid"})
if err != nil {
t.Errorf("Could not create new user with CIDR, got error [%v]", err)
}

// Another user with valid CIDR mask to match the httpexpect default
newUserWithValidCIDR, err := DB.Register(cidrslice{"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("/delete").
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) {

updateJSON := map[string]interface{}{
Expand Down Expand Up @@ -439,6 +633,62 @@ func TestApiManyUpdateWithIpCheckHeaders(t *testing.T) {
Config.API.UseHeader = false
}

func TestApiManyDeleteWithIpCheckHeaders(t *testing.T) {

updateJSON := map[string]interface{}{
"subdomain": "",
"txt": ""}

router := setupRouter(false, false)
server := httptest.NewServer(router)
defer server.Close()
e := getExpect(t, server)
// Use header checks from default header (X-Forwarded-For)
Config.API.UseHeader = true
// User without defined CIDR masks
newUser, err := DB.Register(cidrslice{})
if err != nil {
t.Errorf("Could not create new user, got error [%v]", err)
}

newUserWithCIDR, err := DB.Register(cidrslice{"192.168.1.2/32", "invalid"})
if err != nil {
t.Errorf("Could not create new user with CIDR, got error [%v]", err)
}

newUserWithIP6CIDR, err := DB.Register(cidrslice{"2002:c0a8::0/32"})
if err != nil {
t.Errorf("Could not create a new user with IP6 CIDR, got error [%v]", err)
}

for _, test := range []struct {
user ACMETxt
headerValue string
status int
}{
{newUser, "whatever goes", 200},
{newUser, "10.0.0.1, 1.2.3.4 ,3.4.5.6", 200},
{newUserWithCIDR, "127.0.0.1", 401},
{newUserWithCIDR, "10.0.0.1, 10.0.0.2, 192.168.1.3", 401},
{newUserWithCIDR, "10.1.1.1 ,192.168.1.2, 8.8.8.8", 200},
{newUserWithIP6CIDR, "2002:c0a8:b4dc:0d3::0", 200},
{newUserWithIP6CIDR, "2002:c0a7:0ff::0", 401},
{newUserWithIP6CIDR, "2002:c0a8:d3ad:b33f:c0ff:33b4:dc0d:3b4d", 200},
} {
updateJSON = map[string]interface{}{
"subdomain": test.user.Subdomain,
"txt": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
e.POST("/delete").
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)
}
Config.API.UseHeader = false
}

func TestApiHealthCheck(t *testing.T) {
router := setupRouter(false, false)
server := httptest.NewServer(router)
Expand Down
29 changes: 28 additions & 1 deletion db.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func (d *acmedb) GetTXTForDomain(domain string) ([]string, error) {
domain = sanitizeString(domain)
var txts []string
getSQL := `
SELECT Value FROM txt WHERE Subdomain=$1 LIMIT 2
SELECT Value FROM txt WHERE Subdomain=$1 AND LastUpdate > 0 LIMIT 2
`
if Config.Database.Engine == "sqlite3" {
getSQL = getSQLiteStmt(getSQL)
Expand Down Expand Up @@ -309,6 +309,33 @@ func (d *acmedb) Update(a ACMETxtPost) error {
return nil
}

func (d *acmedb) Delete(a ACMETxtPost) error {
d.Lock()
defer d.Unlock()
var err error
// Data in a is already sanitized

delSQL := `
UPDATE txt SET LastUpdate=0
WHERE rowid=(
SELECT rowid FROM txt WHERE Subdomain=$1 AND Value=$2)
`
if Config.Database.Engine == "sqlite3" {
delSQL = getSQLiteStmt(delSQL)
}

sm, err := d.DB.Prepare(delSQL)
if err != nil {
return err
}
defer sm.Close()
_, err = sm.Exec(a.Subdomain, a.Value)
if err != nil {
return err
}
return nil
}

func getModelFromRow(r *sql.Rows) (ACMETxt, error) {
txt := ACMETxt{}
afrom := ""
Expand Down
Loading