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)}").
est, refactored roster' href='/danisanti/profani-tty/commit/Makefile.am?id=107fdd355e487793e53799ce3ebed0659263a2f7'>107fdd35 ^
92837ec1 ^
107fdd35 ^
107fdd35 ^
447d2358 ^
21ab1821 ^
107fdd35 ^

0d15c710 ^

6a9e1930 ^
9aa282f6 ^

0d15c710 ^
8c01021a ^
8c01021a ^
5be9ac32 ^
8c01021a ^
0d15c710 ^
2451b7b1 ^
8b4c7e93 ^
3487100c ^
5319a03a ^
aa4ffa7e ^
419f37fe ^
7db1bcee ^
a871ad80 ^
c47b4261 ^
9537592b ^
59296054 ^
ab7bd6fe ^
6b9f404c ^
2505d470 ^
d1f8c6cd ^
cc8c3542 ^
4906f3f6 ^
6cb5b550 ^
fa23084c ^
d0a0e5e7 ^
7a104431 ^
003002d6 ^
f7843def ^
0d15c710 ^
d782b007 ^


0fbaa6f5 ^
d782b007 ^
fa89e2aa ^
0d15c710 ^
fa89e2aa ^

0d15c710 ^
264fc55a ^
7638f379 ^


0c1092fd ^

1809064d ^

0c1092fd ^
3ceb9b0d ^
fa89e2aa ^
7638f379 ^
7e4b1b1d ^
0fbaa6f5 ^
fa89e2aa ^

0fbaa6f5 ^
fa89e2aa ^
264fc55a ^

d782b007 ^
0fbaa6f5 ^
3ceb9b0d ^



0fbaa6f5 ^


d782b007 ^


0fbaa6f5 ^
e2f37600 ^
2655d9e8 ^
0c1092fd ^

01394d6c ^
0fbaa6f5 ^

0c1092fd ^

13ee16de ^
0fbaa6f5 ^


















1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164