util.go (view raw)
1//
2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
3//
4// Permission to use, copy, modify, and distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16package main
17
18/*
19#include <termios.h>
20
21void
22termecho(int on)
23{
24 struct termios t;
25 tcgetattr(1, &t);
26 if (on)
27 t.c_lflag |= ECHO;
28 else
29 t.c_lflag &= ~ECHO;
30 tcsetattr(1, TCSADRAIN, &t);
31}
32*/
33import "C"
34
35import (
36 "bufio"
37 "crypto/rand"
38 "crypto/rsa"
39 "database/sql"
40 "fmt"
41 "net"
42 "os"
43 "os/signal"
44 "regexp"
45 "strings"
46
47 "golang.org/x/crypto/bcrypt"
48 _ "humungus.tedunangst.com/r/go-sqlite3"
49 "humungus.tedunangst.com/r/webs/httpsig"
50 "humungus.tedunangst.com/r/webs/login"
51)
52
53var re_plainname = regexp.MustCompile("^[[:alnum:]_-]+$")
54
55var dbtimeformat = "2006-01-02 15:04:05"
56
57var alreadyopendb *sql.DB
58var stmtConfig *sql.Stmt
59
60func initdb() {
61 blobdbname := dataDir + "/blob.db"
62 dbname := dataDir + "/honk.db"
63 _, err := os.Stat(dbname)
64 if err == nil {
65 elog.Fatalf("%s already exists", dbname)
66 }
67 db, err := sql.Open("sqlite3", dbname)
68 if err != nil {
69 elog.Fatal(err)
70 }
71 alreadyopendb = db
72 defer func() {
73 os.Remove(dbname)
74 os.Remove(blobdbname)
75 os.Exit(1)
76 }()
77 c := make(chan os.Signal, 1)
78 signal.Notify(c, os.Interrupt)
79 go func() {
80 <-c
81 C.termecho(1)
82 fmt.Printf("\n")
83 os.Remove(dbname)
84 os.Remove(blobdbname)
85 os.Exit(1)
86 }()
87
88 _, err = db.Exec("PRAGMA journal_mode=WAL")
89 if err != nil {
90 elog.Print(err)
91 return
92 }
93 for _, line := range strings.Split(sqlSchema, ";") {
94 _, err = db.Exec(line)
95 if err != nil {
96 elog.Print(err)
97 return
98 }
99 }
100 r := bufio.NewReader(os.Stdin)
101
102 initblobdb(blobdbname)
103
104 prepareStatements(db)
105
106 err = createuser(db, r)
107 if err != nil {
108 elog.Print(err)
109 return
110 }
111 // must came later or user above will have negative id
112 err = createserveruser(db)
113 if err != nil {
114 elog.Print(err)
115 return
116 }
117
118 fmt.Printf("listen address: ")
119 addr, err := r.ReadString('\n')
120 if err != nil {
121 elog.Print(err)
122 return
123 }
124 addr = addr[:len(addr)-1]
125 if len(addr) < 1 {
126 elog.Print("that's way too short")
127 return
128 }
129 setconfig("listenaddr", addr)
130 fmt.Printf("server name: ")
131 addr, err = r.ReadString('\n')
132 if err != nil {
133 elog.Print(err)
134 return
135 }
136 addr = addr[:len(addr)-1]
137 if len(addr) < 1 {
138 elog.Print("that's way too short")
139 return
140 }
141 setconfig("servername", addr)
142 var randbytes [16]byte
143 rand.Read(randbytes[:])
144 key := fmt.Sprintf("%x", randbytes)
145 setconfig("csrfkey", key)
146 setconfig("dbversion", myVersion)
147
148 setconfig("servermsg", "<h2>Things happen.</h2>")
149 setconfig("aboutmsg", "<h3>What is honk?</h3><p>Honk is amazing!")
150 setconfig("loginmsg", "<h2>login</h2>")
151 setconfig("devel", 0)
152
153 db.Close()
154 fmt.Printf("done.\n")
155 os.Exit(0)
156}
157
158func initblobdb(blobdbname string) {
159 _, err := os.Stat(blobdbname)
160 if err == nil {
161 elog.Fatalf("%s already exists", blobdbname)
162 }
163 blobdb, err := sql.Open("sqlite3", blobdbname)
164 if err != nil {
165 elog.Print(err)
166 return
167 }
168 _, err = blobdb.Exec("PRAGMA journal_mode=WAL")
169 if err != nil {
170 elog.Print(err)
171 return
172 }
173 _, err = blobdb.Exec("create table filedata (xid text, media text, hash text, content blob)")
174 if err != nil {
175 elog.Print(err)
176 return
177 }
178 _, err = blobdb.Exec("create index idx_filexid on filedata(xid)")
179 if err != nil {
180 elog.Print(err)
181 return
182 }
183 _, err = blobdb.Exec("create index idx_filehash on filedata(hash)")
184 if err != nil {
185 elog.Print(err)
186 return
187 }
188 blobdb.Close()
189}
190
191func adduser() {
192 db := opendatabase()
193 defer func() {
194 os.Exit(1)
195 }()
196 c := make(chan os.Signal, 1)
197 signal.Notify(c, os.Interrupt)
198 go func() {
199 <-c
200 C.termecho(1)
201 fmt.Printf("\n")
202 os.Exit(1)
203 }()
204
205 r := bufio.NewReader(os.Stdin)
206
207 err := createuser(db, r)
208 if err != nil {
209 elog.Print(err)
210 return
211 }
212
213 os.Exit(0)
214}
215
216func deluser(username string) {
217 user, _ := butwhatabout(username)
218 if user == nil {
219 elog.Printf("no userfound")
220 return
221 }
222 userid := user.ID
223 db := opendatabase()
224
225 where := " where honkid in (select honkid from honks where userid = ?)"
226 doordie(db, "delete from donks"+where, userid)
227 doordie(db, "delete from onts"+where, userid)
228 doordie(db, "delete from honkmeta"+where, userid)
229 where = " where chonkid in (select chonkid from chonks where userid = ?)"
230 doordie(db, "delete from donks"+where, userid)
231
232 doordie(db, "delete from honks where userid = ?", userid)
233 doordie(db, "delete from chonks where userid = ?", userid)
234 doordie(db, "delete from honkers where userid = ?", userid)
235 doordie(db, "delete from zonkers where userid = ?", userid)
236 doordie(db, "delete from doovers where userid = ?", userid)
237 doordie(db, "delete from hfcs where userid = ?", userid)
238 doordie(db, "delete from auth where userid = ?", userid)
239 doordie(db, "delete from users where userid = ?", userid)
240}
241
242func chpass(username string) {
243 user, err := butwhatabout(username)
244 if err != nil {
245 elog.Fatal(err)
246 }
247 defer func() {
248 os.Exit(1)
249 }()
250 c := make(chan os.Signal, 1)
251 signal.Notify(c, os.Interrupt)
252 go func() {
253 <-c
254 C.termecho(1)
255 fmt.Printf("\n")
256 os.Exit(1)
257 }()
258
259 db := opendatabase()
260 login.Init(login.InitArgs{Db: db, Logger: ilog})
261
262 r := bufio.NewReader(os.Stdin)
263
264 pass, err := askpassword(r)
265 if err != nil {
266 elog.Print(err)
267 return
268 }
269 err = login.SetPassword(user.ID, pass)
270 if err != nil {
271 elog.Print(err)
272 return
273 }
274 fmt.Printf("done\n")
275 os.Exit(0)
276}
277
278func askpassword(r *bufio.Reader) (string, error) {
279 C.termecho(0)
280 fmt.Printf("password: ")
281 pass, err := r.ReadString('\n')
282 C.termecho(1)
283 fmt.Printf("\n")
284 if err != nil {
285 return "", err
286 }
287 pass = pass[:len(pass)-1]
288 if len(pass) < 6 {
289 return "", fmt.Errorf("that's way too short")
290 }
291 return pass, nil
292}
293
294func createuser(db *sql.DB, r *bufio.Reader) error {
295 fmt.Printf("username: ")
296 name, err := r.ReadString('\n')
297 if err != nil {
298 return err
299 }
300 name = name[:len(name)-1]
301 if len(name) < 1 {
302 return fmt.Errorf("that's way too short")
303 }
304 if !re_plainname.MatchString(name) {
305 return fmt.Errorf("alphanumeric only please")
306 }
307 if _, err := butwhatabout(name); err == nil {
308 return fmt.Errorf("user already exists")
309 }
310 pass, err := askpassword(r)
311 if err != nil {
312 return err
313 }
314 hash, err := bcrypt.GenerateFromPassword([]byte(pass), 12)
315 if err != nil {
316 return err
317 }
318 k, err := rsa.GenerateKey(rand.Reader, 2048)
319 if err != nil {
320 return err
321 }
322 pubkey, err := httpsig.EncodeKey(&k.PublicKey)
323 if err != nil {
324 return err
325 }
326 seckey, err := httpsig.EncodeKey(k)
327 if err != nil {
328 return err
329 }
330 about := "what about me?"
331 _, err = db.Exec("insert into users (username, displayname, about, hash, pubkey, seckey, options) values (?, ?, ?, ?, ?, ?, ?)", name, name, about, hash, pubkey, seckey, "{}")
332 if err != nil {
333 return err
334 }
335 return nil
336}
337
338func createserveruser(db *sql.DB) error {
339 k, err := rsa.GenerateKey(rand.Reader, 2048)
340 if err != nil {
341 return err
342 }
343 pubkey, err := httpsig.EncodeKey(&k.PublicKey)
344 if err != nil {
345 return err
346 }
347 seckey, err := httpsig.EncodeKey(k)
348 if err != nil {
349 return err
350 }
351 name := "server"
352 about := "server"
353 hash := "*"
354 _, err = db.Exec("insert into users (userid, username, displayname, about, hash, pubkey, seckey, options) values (?, ?, ?, ?, ?, ?, ?, ?)", serverUID, name, name, about, hash, pubkey, seckey, "")
355 if err != nil {
356 return err
357 }
358 return nil
359}
360
361func opendatabase() *sql.DB {
362 if alreadyopendb != nil {
363 return alreadyopendb
364 }
365 dbname := dataDir + "/honk.db"
366 _, err := os.Stat(dbname)
367 if err != nil {
368 elog.Fatalf("unable to open database: %s", err)
369 }
370 db, err := sql.Open("sqlite3", dbname)
371 if err != nil {
372 elog.Fatalf("unable to open database: %s", err)
373 }
374 stmtConfig, err = db.Prepare("select value from config where key = ?")
375 if err != nil {
376 elog.Fatal(err)
377 }
378 alreadyopendb = db
379 return db
380}
381
382func openblobdb() *sql.DB {
383 blobdbname := dataDir + "/blob.db"
384 _, err := os.Stat(blobdbname)
385 if err != nil {
386 elog.Fatalf("unable to open database: %s", err)
387 }
388 db, err := sql.Open("sqlite3", blobdbname)
389 if err != nil {
390 elog.Fatalf("unable to open database: %s", err)
391 }
392 return db
393}
394
395func getconfig(key string, value interface{}) error {
396 m, ok := value.(*map[string]bool)
397 if ok {
398 rows, err := stmtConfig.Query(key)
399 if err != nil {
400 return err
401 }
402 defer rows.Close()
403 for rows.Next() {
404 var s string
405 err = rows.Scan(&s)
406 if err != nil {
407 return err
408 }
409 (*m)[s] = true
410 }
411 return nil
412 }
413 row := stmtConfig.QueryRow(key)
414 err := row.Scan(value)
415 if err == sql.ErrNoRows {
416 err = nil
417 }
418 return err
419}
420
421func setconfig(key string, val interface{}) error {
422 db := opendatabase()
423 db.Exec("delete from config where key = ?", key)
424 _, err := db.Exec("insert into config (key, value) values (?, ?)", key, val)
425 return err
426}
427
428func openListener() (net.Listener, error) {
429 var listenAddr string
430 err := getconfig("listenaddr", &listenAddr)
431 if err != nil {
432 return nil, err
433 }
434 if strings.HasPrefix(listenAddr, "fcgi:") {
435 listenAddr = listenAddr[5:]
436 usefcgi = true
437 }
438 if listenAddr == "" {
439 return nil, fmt.Errorf("must have listenaddr")
440 }
441 proto := "tcp"
442 if listenAddr[0] == '/' {
443 proto = "unix"
444 err := os.Remove(listenAddr)
445 if err != nil && !os.IsNotExist(err) {
446 elog.Printf("unable to unlink socket: %s", err)
447 }
448 }
449 listener, err := net.Listen(proto, listenAddr)
450 if err != nil {
451 return nil, err
452 }
453 if proto == "unix" {
454 os.Chmod(listenAddr, 0777)
455 }
456 listenSocket = listener
457 return listener, nil
458}