about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorBenjamin Morrison <ben@gbmor.dev>2021-10-21 21:31:17 -0400
committerBenjamin Morrison <ben@gbmor.dev>2021-10-21 21:35:23 -0400
commitdd3d87bd97e41c77eea270812d338139fd87b9fc (patch)
tree5ee99ab38e00c41190f82bef4b08a298c914f329
parentdcb254618dc5541bb50c488c96b5e02c36951c06 (diff)
downloadgetwtxt-dd3d87bd97e41c77eea270812d338139fd87b9fc.tar.gz
delete a user, new config option for admin pass. pass is bcrypt hashed on startup and not stored in plaintext.
-rw-r--r--Makefile3
-rw-r--r--README.md8
-rw-r--r--assets/tmpl/index.html4
-rw-r--r--go.mod2
-rw-r--r--go.sum7
-rw-r--r--svc/db.go11
-rw-r--r--svc/handlers.go38
-rw-r--r--svc/leveldb.go21
-rw-r--r--svc/sqlite.go4
-rw-r--r--svc/svc.go4
10 files changed, 96 insertions, 6 deletions
diff --git a/Makefile b/Makefile
index d76ac3a..4897f5d 100644
--- a/Makefile
+++ b/Makefile
@@ -28,7 +28,8 @@ install:
 
 	@printf "\n%s\n" "Copying files..."
 	install -m755 getwtxt $(BINDIR)
-	@if [ -f "$(BINDIR)/getwtxt.yml" ]; then printf "%s\n" "getwtxt.yml exists. Skipping ..."; else printf "%s\n" "getwtxt.yml ..." && install -m644 getwtxt.yml "$(BINDIR)"; fi
+	@if [ -f "$(BINDIR)/getwtxt.yml" ]; then printf "%s\n" "getwtxt.yml exists. Skipping ..."; else printf "%s\n" "getwtxt.yml ..." && install -m600 getwtxt.yml "$(BINDIR)"; fi
+	chmod 600 $(BINDIR)/getwtxt.yml
 	@if [ -f "$(BINDIR)/assets/style.css" ]; then printf "%s\n" "style.css exists. Skipping ..."; else printf "%s\n" "style.css ..." && install -m644 assets/style.css "$(BINDIR)/assets/style.css"; fi
 	@if [ -f "$(BINDIR)/assets/tmpl/index.html" ]; then printf "%s\n" "tmpl/index.html exists. Skipping ..."; else printf "%s\n" "tmpl/index.html ..." && install -m644 assets/tmpl/index.html "$(BINDIR)/assets/tmpl/index.html"; fi
 	install -m644 static/kognise.water.css.dark.min.css $(BINDIR)/static
diff --git a/README.md b/README.md
index 44f3e43..f1833f2 100644
--- a/README.md
+++ b/README.md
@@ -263,6 +263,14 @@ $ curl 'https://twtxt.example.com/api/plain/tags/programming'
 foo    https://example.com/twtxt.txt    2019-03-01T09:31:02.000Z    I love #programming!
 ```
 
+### Delete a User
+
+```
+$ curl -X DELETE -H 'X-Auth: password_in_getwtxt.yml' 'https://twtxt.example.com/api/admin/users?url=https://example.com/twtxt.txt'
+
+200 OK
+```
+
 ## Benchmarks
 
 * [bombardier](https://github.com/codesenberg/bombardier)
diff --git a/assets/tmpl/index.html b/assets/tmpl/index.html
index 0318810..1e4b6f5 100644
--- a/assets/tmpl/index.html
+++ b/assets/tmpl/index.html
@@ -43,6 +43,10 @@
       <pre><code>$ curl '{{.URL}}/api/plain/version'
 getwtxt {{.Vers}}
         </code></pre>
+     <p>Delete a user by issuing a <code>DELETE</code> request to the <code>/api/admin/users</code> endpoint. This
+     must include the <code>X-Auth</code> header with the password specified during configuration.</p>
+     <pre><code>$ curl -X DELETE -H 'X-Auth: mypassword' '{{.URL}}/api/admin/users?url=https://foo.ext/twtxt.txt'
+200 OK</code></pre>
       <p>Add new user by submitting a <code>POST</code> request to the <code>/api/plain/users</code> endpoint.
         If both <code>?url=X</code> and <code>?nickname=X</code> are not passed, or the user already exists in
         this registry, you will receive <code>400 Bad Request</code> as a response. If you are unsure what went
diff --git a/go.mod b/go.mod
index dfa6f22..85f2b1a 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,6 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.7.0
 	github.com/syndtr/goleveldb v1.0.0
-	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
+	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
 	golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
 )
diff --git a/go.sum b/go.sum
index 1d2a295..0d7e5cb 100644
--- a/go.sum
+++ b/go.sum
@@ -237,8 +237,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -263,15 +263,14 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80=
-golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/svc/db.go b/svc/db.go
index 8cd05d1..ff92753 100644
--- a/svc/db.go
+++ b/svc/db.go
@@ -39,6 +39,7 @@ import (
 type dbase interface {
 	push() error
 	pull()
+	delUser(string) error
 }
 
 // Opens a new connection to the specified
@@ -96,3 +97,13 @@ func pullDB() {
 	dbChan <- db
 	log.Printf("Database pull took: %v\n", time.Since(start))
 }
+
+func delUser(userURL string) error {
+	db := <-dbChan
+	err := db.delUser(userURL)
+	dbChan <- db
+	if err != nil {
+		return err
+	}
+	return twtxtCache.DelUser(userURL)
+}
diff --git a/svc/handlers.go b/svc/handlers.go
index cb07349..5dbb10d 100644
--- a/svc/handlers.go
+++ b/svc/handlers.go
@@ -20,15 +20,18 @@ along with Getwtxt.  If not, see <https://www.gnu.org/licenses/>.
 package svc // import "git.sr.ht/~gbmor/getwtxt/svc"
 
 import (
+	"errors"
 	"fmt"
 	"hash/fnv"
 	"net/http"
+	"net/url"
 	"strconv"
 	"strings"
 	"time"
 
 	"git.sr.ht/~gbmor/getwtxt/registry"
 	"github.com/gorilla/mux"
+	"golang.org/x/crypto/bcrypt"
 )
 
 // Takes the modtime of one of the static files, derives
@@ -242,3 +245,38 @@ func apiTagsHandler(w http.ResponseWriter, r *http.Request) {
 	}
 	log200(r)
 }
+
+func handleUserDelete(w http.ResponseWriter, r *http.Request) {
+	pass := r.Header.Get("X-Auth")
+	if pass == "" {
+		errHTTP(w, r, errors.New("unauthorized"), http.StatusUnauthorized)
+		return
+	}
+	confObj.Mu.RLock()
+	adminHash := []byte(confObj.AdminPassHash)
+	confObj.Mu.RUnlock()
+
+	if err := bcrypt.CompareHashAndPassword(adminHash, []byte(pass)); err != nil {
+		errHTTP(w, r, errors.New("unauthorized"), http.StatusUnauthorized)
+		return
+	}
+
+	r.ParseForm()
+	userURL := strings.TrimSpace(r.Form.Get("url"))
+	if userURL == "" {
+		errHTTP(w, r, errors.New("bad request"), http.StatusBadRequest)
+		return
+	}
+	if _, err := url.Parse(userURL); err != nil {
+		errHTTP(w, r, errors.New("bad request"), http.StatusBadRequest)
+		return
+	}
+
+	if err := delUser(userURL); err != nil {
+		return
+	}
+
+	w.WriteHeader(200)
+	w.Write([]byte("200 OK\n"))
+	log200(r)
+}
diff --git a/svc/leveldb.go b/svc/leveldb.go
index 5fb4a45..a429934 100644
--- a/svc/leveldb.go
+++ b/svc/leveldb.go
@@ -33,6 +33,27 @@ type dbLevel struct {
 	db *leveldb.DB
 }
 
+func (lvl *dbLevel) delUser(userURL string) error {
+	twtxtCache.Mu.RLock()
+	defer twtxtCache.Mu.RUnlock()
+
+	userStatuses := twtxtCache.Users[userURL].Status
+	var dbBasket = &leveldb.Batch{}
+
+	dbBasket.Delete([]byte(userURL + "*Nick"))
+	dbBasket.Delete([]byte(userURL + "*URL"))
+	dbBasket.Delete([]byte(userURL + "*IP"))
+	dbBasket.Delete([]byte(userURL + "*Date"))
+	dbBasket.Delete([]byte(userURL + "*LastModified"))
+
+	for i := range userStatuses {
+		rfc := i.Format(time.RFC3339)
+		dbBasket.Delete([]byte(userURL + "*Status*" + rfc))
+	}
+
+	return lvl.db.Write(dbBasket, nil)
+}
+
 // Called intermittently to commit registry data to
 // a LevelDB database.
 func (lvl *dbLevel) push() error {
diff --git a/svc/sqlite.go b/svc/sqlite.go
index 128aed3..98a4f93 100644
--- a/svc/sqlite.go
+++ b/svc/sqlite.go
@@ -64,6 +64,10 @@ func initSqlite() *dbSqlite {
 	}
 }
 
+func (lite *dbSqlite) delUser(userURL string) error {
+	return nil
+}
+
 // Commits data from memory to a SQLite database intermittently.
 func (lite *dbSqlite) push() error {
 	if err := lite.db.Ping(); err != nil {
diff --git a/svc/svc.go b/svc/svc.go
index 72ccdc3..10cf56a 100644
--- a/svc/svc.go
+++ b/svc/svc.go
@@ -91,6 +91,10 @@ func setIndexRouting(index *mux.Router) {
 }
 
 func setEndpointRouting(api *mux.Router) {
+	api.Path("/admin/users").
+		Methods("DELETE").
+		HandlerFunc(handleUserDelete)
+
 	// May add support for other formats later.
 	// Making this future-proof.
 	api.Path("/{format:(?:plain)}").