about summary refs log tree commit diff stats
path: root/svc
diff options
context:
space:
mode:
Diffstat (limited to 'svc')
-rw-r--r--svc/common.go16
-rw-r--r--svc/common_test.go34
-rw-r--r--svc/conf.go13
-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
8 files changed, 141 insertions, 0 deletions
diff --git a/svc/common.go b/svc/common.go
new file mode 100644
index 0000000..5f169af
--- /dev/null
+++ b/svc/common.go
@@ -0,0 +1,16 @@
+package svc
+
+import "golang.org/x/crypto/bcrypt"
+
+// HashPass returns the bcrypt hash of the provided string.
+// If an empty string is provided, return an empty string.
+func HashPass(s string) (string, error) {
+	if s == "" {
+		return "", nil
+	}
+	h, err := bcrypt.GenerateFromPassword([]byte(s), 14)
+	if err != nil {
+		return "", err
+	}
+	return string(h), nil
+}
diff --git a/svc/common_test.go b/svc/common_test.go
new file mode 100644
index 0000000..d9a08b3
--- /dev/null
+++ b/svc/common_test.go
@@ -0,0 +1,34 @@
+package svc
+
+import (
+	"testing"
+)
+
+func TestHashPass(t *testing.T) {
+	cases := []struct {
+		in, name   string
+		shouldFail bool
+	}{
+		{
+			in:         "foo",
+			name:       "non-empty password",
+			shouldFail: false,
+		},
+		{
+			in:         "",
+			name:       "empty password",
+			shouldFail: true,
+		},
+	}
+	for _, v := range cases {
+		t.Run(v.name, func(t *testing.T) {
+			out, err := HashPass(v.in)
+			if err != nil && !v.shouldFail {
+				t.Errorf("Shouldn't have failed: Case %s, Error: %s", v.name, err)
+			}
+			if out == "" && v.in != "" {
+				t.Errorf("Got empty out for case %s input %s", v.name, v.in)
+			}
+		})
+	}
+}
diff --git a/svc/conf.go b/svc/conf.go
index 7365b2b..5f826fb 100644
--- a/svc/conf.go
+++ b/svc/conf.go
@@ -20,6 +20,7 @@ along with Getwtxt.  If not, see <https://www.gnu.org/licenses/>.
 package svc // import "git.sr.ht/~gbmor/getwtxt/svc"
 
 import (
+	"fmt"
 	"log"
 	"os"
 	"path/filepath"
@@ -43,6 +44,7 @@ type Configuration struct {
 	DBPath        string        `yaml:"DatabasePath"`
 	AssetsDir     string        `yaml:"AssetsDirectory"`
 	StaticDir     string        `yaml:"StaticFilesDirectory"`
+	AdminPassHash string        `yaml:"-"`
 	StdoutLogging bool          `yaml:"StdoutLogging"`
 	CacheInterval time.Duration `yaml:"StatusFetchInterval"`
 	DBInterval    time.Duration `yaml:"DatabasePushInterval"`
@@ -126,6 +128,7 @@ func setConfigDefaults() {
 	viper.SetDefault("StdoutLogging", false)
 	viper.SetDefault("ReCacheInterval", "1h")
 	viper.SetDefault("DatabasePushInterval", "5m")
+	viper.SetDefault("AdminPassword", "please_change_me")
 
 	viper.SetDefault("Instance.SiteName", "getwtxt")
 	viper.SetDefault("Instance.OwnerName", "Anonymous Microblogger")
@@ -173,6 +176,16 @@ func bindConfig() {
 	confObj.StdoutLogging = viper.GetBool("StdoutLogging")
 	confObj.CacheInterval = viper.GetDuration("StatusFetchInterval")
 	confObj.DBInterval = viper.GetDuration("DatabasePushInterval")
+	txtPass := viper.GetString("AdminPassword")
+	if txtPass == "please_change_me" {
+		fmt.Println("Please set AdminPassword in getwtxt.yml")
+		os.Exit(1)
+	}
+	passHash, err := HashPass(txtPass)
+	if err != nil {
+		errFatal("Failed to hash administrator password: ", err)
+	}
+	confObj.AdminPassHash = passHash
 
 	confObj.Instance.Vers = Vers
 	confObj.Instance.Name = viper.GetString("Instance.SiteName")
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)}").