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 "github.com/jmoiron/sqlx"
48 "golang.org/x/crypto/bcrypt"
49 "humungus.tedunangst.com/r/go-sqlite3"
50 "humungus.tedunangst.com/r/webs/httpsig"
51 "humungus.tedunangst.com/r/webs/login"
52)
53
54var re_plainname = regexp.MustCompile("^[[:alnum:]_-]+$")
55
56var dbtimeformat = "2006-01-02 15:04:05"
57
58var alreadyopendb *sql.DB
59var alreadyopendbx *sqlx.DB
60var stmtConfig *sql.Stmt
61
62func init() {
63 vers, num, _ := sqlite3.Version()
64 if num < 3034000 {
65 fmt.Fprintf(os.Stderr, "libsqlite is too old. required: %s found: %s\n",
66 "3.34.0", vers)
67 os.Exit(1)
68 }
69}
70
71func initdb() {
72 blobdbname := dataDir + "/blob.db"
73 dbname := dataDir + "/honk.db"
74 _, err := os.Stat(dbname)
75 if err == nil {
76 elog.Fatalf("%s already exists", dbname)
77 }
78 db, err := sql.Open("sqlite3", dbname)
79 if err != nil {
80 elog.Fatal(err)
81 }
82 dbx, err := sqlx.Open("sqlite3", dbname)
83 if err != nil {
84 elog.Fatal(err)
85 }
86 alreadyopendbx = dbx
87
88 defer func() {
89 os.Remove(dbname)
90 os.Remove(blobdbname)
91 os.Exit(1)
92 }()
93 c := make(chan os.Signal, 1)
94 signal.Notify(c, os.Interrupt)
95 go func() {
96 <-c
97 C.termecho(1)
98 fmt.Printf("\n")
99 os.Remove(dbname)
100 os.Remove(blobdbname)
101 os.Exit(1)
102 }()
103
104 _, err = db.Exec("PRAGMA journal_mode=WAL")
105 if err != nil {
106 elog.Print(err)
107 return
108 }
109 for _, line := range strings.Split(sqlSchema, ";") {
110 _, err = db.Exec(line)
111 if err != nil {
112 elog.Print(err)
113 return
114 }
115 }
116 r := bufio.NewReader(os.Stdin)
117
118 initblobdb(blobdbname)
119
120 prepareStatements(db)
121 prepareStatementsx(dbx)
122
123 err = createuser(db, r)
124 if err != nil {
125 elog.Print(err)
126 return
127 }
128 // must came later or user above will have negative id
129 err = createserveruser(db)
130 if err != nil {
131 elog.Print(err)
132 return
133 }
134
135 fmt.Printf("listen address: ")
136 addr, err := r.ReadString('\n')
137 if err != nil {
138 elog.Print(err)
139 return
140 }
141 addr = addr[:len(addr)-1]
142 if len(addr) < 1 {
143 elog.Print("that's way too short")
144 return
145 }
146 setconfig("listenaddr", addr)
147 fmt.Printf("server name: ")
148 addr, err = r.ReadString('\n')
149 if err != nil {
150 elog.Print(err)
151 return
152 }
153 addr = addr[:len(addr)-1]
154 if len(addr) < 1 {
155 elog.Print("that's way too short")
156 return
157 }
158 setconfig("servername", addr)
159 var randbytes [16]byte
160 rand.Read(randbytes[:])
161 key := fmt.Sprintf("%x", randbytes)
162 setconfig("csrfkey", key)
163 setconfig("dbversion", myVersion)
164
165 setconfig("servermsg", "<h2>Things happen.</h2>")
166 setconfig("aboutmsg", "<h3>What is honk?</h3><p>Honk is amazing!")
167 setconfig("loginmsg", "<h2>login</h2>")
168 setconfig("devel", 0)
169
170 db.Close()
171 fmt.Printf("done.\n")
172 os.Exit(0)
173}
174
175func initblobdb(blobdbname string) {
176 _, err := os.Stat(blobdbname)
177 if err == nil {
178 elog.Fatalf("%s already exists", blobdbname)
179 }
180 blobdb, err := sql.Open("sqlite3", blobdbname)
181 if err != nil {
182 elog.Print(err)
183 return
184 }
185 _, err = blobdb.Exec("PRAGMA journal_mode=WAL")
186 if err != nil {
187 elog.Print(err)
188 return
189 }
190 _, err = blobdb.Exec("create table filedata (xid text, media text, hash text, content blob)")
191 if err != nil {
192 elog.Print(err)
193 return
194 }
195 _, err = blobdb.Exec("create index idx_filexid on filedata(xid)")
196 if err != nil {
197 elog.Print(err)
198 return
199 }
200 _, err = blobdb.Exec("create index idx_filehash on filedata(hash)")
201 if err != nil {
202 elog.Print(err)
203 return
204 }
205 blobdb.Close()
206}
207
208func adduser() {
209 db := opendatabase()
210 defer func() {
211 os.Exit(1)
212 }()
213 c := make(chan os.Signal, 1)
214 signal.Notify(c, os.Interrupt)
215 go func() {
216 <-c
217 C.termecho(1)
218 fmt.Printf("\n")
219 os.Exit(1)
220 }()
221
222 r := bufio.NewReader(os.Stdin)
223
224 err := createuser(db, r)
225 if err != nil {
226 elog.Print(err)
227 return
228 }
229
230 os.Exit(0)
231}
232
233func deluser(username string) {
234 user, _ := butwhatabout(username)
235 if user == nil {
236 elog.Printf("no userfound")
237 return
238 }
239 userid := user.ID
240 db := opendatabase()
241
242 where := " where honkid in (select honkid from honks where userid = ?)"
243 doordie(db, "delete from donks"+where, userid)
244 doordie(db, "delete from onts"+where, userid)
245 doordie(db, "delete from honkmeta"+where, userid)
246 where = " where chonkid in (select chonkid from chonks where userid = ?)"
247 doordie(db, "delete from donks"+where, userid)
248
249 doordie(db, "delete from honks where userid = ?", userid)
250 doordie(db, "delete from chonks where userid = ?", userid)
251 doordie(db, "delete from honkers where userid = ?", userid)
252 doordie(db, "delete from zonkers where userid = ?", userid)
253 doordie(db, "delete from doovers where userid = ?", userid)
254 doordie(db, "delete from hfcs where userid = ?", userid)
255 doordie(db, "delete from auth where userid = ?", userid)
256 doordie(db, "delete from users where userid = ?", userid)
257}
258
259func chpass(username string) {
260 user, err := butwhatabout(username)
261 if err != nil {
262 elog.Fatal(err)
263 }
264 defer func() {
265 os.Exit(1)
266 }()
267 c := make(chan os.Signal, 1)
268 signal.Notify(c, os.Interrupt)
269 go func() {
270 <-c
271 C.termecho(1)
272 fmt.Printf("\n")
273 os.Exit(1)
274 }()
275
276 db := opendatabase()
277 login.Init(login.InitArgs{Db: db, Logger: ilog})
278
279 r := bufio.NewReader(os.Stdin)
280
281 pass, err := askpassword(r)
282 if err != nil {
283 elog.Print(err)
284 return
285 }
286 err = login.SetPassword(int64(user.ID), pass)
287 if err != nil {
288 elog.Print(err)
289 return
290 }
291 fmt.Printf("done\n")
292 os.Exit(0)
293}
294
295func askpassword(r *bufio.Reader) (string, error) {
296 C.termecho(0)
297 fmt.Printf("password: ")
298 pass, err := r.ReadString('\n')
299 C.termecho(1)
300 fmt.Printf("\n")
301 if err != nil {
302 return "", err
303 }
304 pass = pass[:len(pass)-1]
305 if len(pass) < 6 {
306 return "", fmt.Errorf("that's way too short")
307 }
308 return pass, nil
309}
310
311func createuser(db *sql.DB, r *bufio.Reader) error {
312 fmt.Printf("username: ")
313 name, err := r.ReadString('\n')
314 if err != nil {
315 return err
316 }
317 name = name[:len(name)-1]
318 if len(name) < 1 {
319 return fmt.Errorf("that's way too short")
320 }
321 if !re_plainname.MatchString(name) {
322 return fmt.Errorf("alphanumeric only please")
323 }
324 if _, err := butwhatabout(name); err == nil {
325 return fmt.Errorf("user already exists")
326 }
327 pass, err := askpassword(r)
328 if err != nil {
329 return err
330 }
331 hash, err := bcrypt.GenerateFromPassword([]byte(pass), 12)
332 if err != nil {
333 return err
334 }
335 k, err := rsa.GenerateKey(rand.Reader, 2048)
336 if err != nil {
337 return err
338 }
339 pubkey, err := httpsig.EncodeKey(&k.PublicKey)
340 if err != nil {
341 return err
342 }
343 seckey, err := httpsig.EncodeKey(k)
344 if err != nil {
345 return err
346 }
347 chatpubkey, chatseckey := newChatKeys()
348 var opts UserOptions
349 opts.ChatPubKey = tob64(chatpubkey.key[:])
350 opts.ChatSecKey = tob64(chatseckey.key[:])
351 jopt, _ := jsonify(opts)
352 about := "what about me?"
353 _, err = db.Exec("insert into users (username, displayname, about, hash, pubkey, seckey, options) values (?, ?, ?, ?, ?, ?, ?)", name, name, about, hash, pubkey, seckey, jopt)
354 if err != nil {
355 return err
356 }
357 return nil
358}
359
360func createserveruser(db *sql.DB) error {
361 k, err := rsa.GenerateKey(rand.Reader, 2048)
362 if err != nil {
363 return err
364 }
365 pubkey, err := httpsig.EncodeKey(&k.PublicKey)
366 if err != nil {
367 return err
368 }
369 seckey, err := httpsig.EncodeKey(k)
370 if err != nil {
371 return err
372 }
373 name := "server"
374 about := "server"
375 hash := "*"
376 _, err = db.Exec("insert into users (userid, username, displayname, about, hash, pubkey, seckey, options) values (?, ?, ?, ?, ?, ?, ?, ?)", serverUID, name, name, about, hash, pubkey, seckey, "")
377 if err != nil {
378 return err
379 }
380 return nil
381}
382
383func opendatabasex() *sqlx.DB {
384 if alreadyopendbx != nil {
385 return alreadyopendbx
386 }
387 dbname := dataDir + "/honk.db"
388 _, err := os.Stat(dbname)
389 if err != nil {
390 elog.Fatalf("unable to open database: %s", err)
391 }
392 db, err := sqlx.Open("sqlite3", dbname)
393 if err != nil {
394 elog.Fatalf("unable to open database: %s", err)
395 }
396 stmtConfig, err = db.Prepare("select value from config where key = ?")
397 if err != nil {
398 elog.Fatal(err)
399 }
400 alreadyopendbx = db
401 return db
402}
403
404func opendatabase() *sql.DB {
405 if alreadyopendb != nil {
406 return alreadyopendb
407 }
408 dbname := dataDir + "/honk.db"
409 _, err := os.Stat(dbname)
410 if err != nil {
411 elog.Fatalf("unable to open database: %s", err)
412 }
413 db, err := sql.Open("sqlite3", dbname)
414 if err != nil {
415 elog.Fatalf("unable to open database: %s", err)
416 }
417 stmtConfig, err = db.Prepare("select value from config where key = ?")
418 if err != nil {
419 elog.Fatal(err)
420 }
421 alreadyopendb = db
422 return db
423}
424
425func openblobdb() *sql.DB {
426 blobdbname := dataDir + "/blob.db"
427 _, err := os.Stat(blobdbname)
428 if err != nil {
429 elog.Fatalf("unable to open database: %s", err)
430 }
431 db, err := sql.Open("sqlite3", blobdbname)
432 if err != nil {
433 elog.Fatalf("unable to open database: %s", err)
434 }
435 return db
436}
437
438func getconfig(key string, value interface{}) error {
439 m, ok := value.(*map[string]bool)
440 if ok {
441 rows, err := stmtConfig.Query(key)
442 if err != nil {
443 return err
444 }
445 defer rows.Close()
446 for rows.Next() {
447 var s string
448 err = rows.Scan(&s)
449 if err != nil {
450 return err
451 }
452 (*m)[s] = true
453 }
454 return nil
455 }
456 row := stmtConfig.QueryRow(key)
457 err := row.Scan(value)
458 if err == sql.ErrNoRows {
459 err = nil
460 }
461 return err
462}
463
464func setconfig(key string, val interface{}) error {
465 db := opendatabase()
466 db.Exec("delete from config where key = ?", key)
467 _, err := db.Exec("insert into config (key, value) values (?, ?)", key, val)
468 return err
469}
470
471func openListener() (net.Listener, error) {
472 var listenAddr string
473 err := getconfig("listenaddr", &listenAddr)
474 if err != nil {
475 return nil, err
476 }
477 if strings.HasPrefix(listenAddr, "fcgi:") {
478 listenAddr = listenAddr[5:]
479 usefcgi = true
480 }
481 if listenAddr == "" {
482 return nil, fmt.Errorf("must have listenaddr")
483 }
484 proto := "tcp"
485 if listenAddr[0] == '/' {
486 proto = "unix"
487 err := os.Remove(listenAddr)
488 if err != nil && !os.IsNotExist(err) {
489 elog.Printf("unable to unlink socket: %s", err)
490 }
491 }
492 listener, err := net.Listen(proto, listenAddr)
493 if err != nil {
494 return nil, err
495 }
496 if proto == "unix" {
497 os.Chmod(listenAddr, 0777)
498 }
499 listenSocket = listener
500 return listener, nil
501}
502
503func getenv(key, def string) string {
504 if val, ok := os.LookupEnv(key); ok {
505 return val
506 }
507 return def
508}