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