source: code/trunk/service.go@ 431

Last change on this file since 431 was 421, checked in by contact, 5 years ago

Switch DB API to user IDs

This commit changes the Network schema to use user IDs instead of
usernames. While at it, a new UNIQUE(user, name) constraint ensures
there is no conflict with custom network names.

Closes: https://todo.sr.ht/~emersion/soju/86
References: https://todo.sr.ht/~emersion/soju/29

File size: 17.1 KB
Line 
1package soju
2
3import (
4 "crypto"
5 "crypto/ecdsa"
6 "crypto/ed25519"
7 "crypto/elliptic"
8 "crypto/rand"
9 "crypto/rsa"
10 "crypto/sha1"
11 "crypto/sha256"
12 "crypto/x509"
13 "crypto/x509/pkix"
14 "encoding/hex"
15 "errors"
16 "flag"
17 "fmt"
18 "io/ioutil"
19 "math/big"
20 "sort"
21 "strings"
22 "time"
23
24 "github.com/google/shlex"
25 "golang.org/x/crypto/bcrypt"
26 "gopkg.in/irc.v3"
27)
28
29const serviceNick = "BouncerServ"
30const serviceRealname = "soju bouncer service"
31
32var servicePrefix = &irc.Prefix{
33 Name: serviceNick,
34 User: serviceNick,
35 Host: serviceNick,
36}
37
38type serviceCommandSet map[string]*serviceCommand
39
40type serviceCommand struct {
41 usage string
42 desc string
43 handle func(dc *downstreamConn, params []string) error
44 children serviceCommandSet
45 admin bool
46}
47
48func sendServiceNOTICE(dc *downstreamConn, text string) {
49 dc.SendMessage(&irc.Message{
50 Prefix: servicePrefix,
51 Command: "NOTICE",
52 Params: []string{dc.nick, text},
53 })
54}
55
56func sendServicePRIVMSG(dc *downstreamConn, text string) {
57 dc.SendMessage(&irc.Message{
58 Prefix: servicePrefix,
59 Command: "PRIVMSG",
60 Params: []string{dc.nick, text},
61 })
62}
63
64func handleServicePRIVMSG(dc *downstreamConn, text string) {
65 words, err := shlex.Split(text)
66 if err != nil {
67 sendServicePRIVMSG(dc, fmt.Sprintf("error: failed to parse command: %v", err))
68 return
69 }
70
71 cmd, params, err := serviceCommands.Get(words)
72 if err != nil {
73 sendServicePRIVMSG(dc, fmt.Sprintf(`error: %v (type "help" for a list of commands)`, err))
74 return
75 }
76 if cmd.admin && !dc.user.Admin {
77 sendServicePRIVMSG(dc, fmt.Sprintf(`error: you must be an admin to use this command`))
78 return
79 }
80
81 if cmd.handle == nil {
82 if len(cmd.children) > 0 {
83 var l []string
84 appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
85 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
86 } else {
87 // Pretend the command does not exist if it has neither children nor handler.
88 // This is obviously a bug but it is better to not die anyway.
89 dc.logger.Printf("command without handler and subcommands invoked:", words[0])
90 sendServicePRIVMSG(dc, fmt.Sprintf("command %q not found", words[0]))
91 }
92 return
93 }
94
95 if err := cmd.handle(dc, params); err != nil {
96 sendServicePRIVMSG(dc, fmt.Sprintf("error: %v", err))
97 }
98}
99
100func (cmds serviceCommandSet) Get(params []string) (*serviceCommand, []string, error) {
101 if len(params) == 0 {
102 return nil, nil, fmt.Errorf("no command specified")
103 }
104
105 name := params[0]
106 params = params[1:]
107
108 cmd, ok := cmds[name]
109 if !ok {
110 for k := range cmds {
111 if !strings.HasPrefix(k, name) {
112 continue
113 }
114 if cmd != nil {
115 return nil, params, fmt.Errorf("command %q is ambiguous", name)
116 }
117 cmd = cmds[k]
118 }
119 }
120 if cmd == nil {
121 return nil, params, fmt.Errorf("command %q not found", name)
122 }
123
124 if len(params) == 0 || len(cmd.children) == 0 {
125 return cmd, params, nil
126 }
127 return cmd.children.Get(params)
128}
129
130func (cmds serviceCommandSet) Names() []string {
131 l := make([]string, 0, len(cmds))
132 for name := range cmds {
133 l = append(l, name)
134 }
135 sort.Strings(l)
136 return l
137}
138
139var serviceCommands serviceCommandSet
140
141func init() {
142 serviceCommands = serviceCommandSet{
143 "help": {
144 usage: "[command]",
145 desc: "print help message",
146 handle: handleServiceHelp,
147 },
148 "network": {
149 children: serviceCommandSet{
150 "create": {
151 usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-connect-command command]...",
152 desc: "add a new network",
153 handle: handleServiceNetworkCreate,
154 },
155 "status": {
156 desc: "show a list of saved networks and their current status",
157 handle: handleServiceNetworkStatus,
158 },
159 "update": {
160 usage: "<name> [-addr addr] [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-connect-command command]...",
161 desc: "update a network",
162 handle: handleServiceNetworkUpdate,
163 },
164 "delete": {
165 usage: "<name>",
166 desc: "delete a network",
167 handle: handleServiceNetworkDelete,
168 },
169 },
170 },
171 "certfp": {
172 children: serviceCommandSet{
173 "generate": {
174 usage: "[-key-type rsa|ecdsa|ed25519] [-bits N] <network name>",
175 desc: "generate a new self-signed certificate, defaults to using RSA-3072 key",
176 handle: handleServiceCertfpGenerate,
177 },
178 "fingerprint": {
179 usage: "<network name>",
180 desc: "show fingerprints of certificate associated with the network",
181 handle: handleServiceCertfpFingerprints,
182 },
183 },
184 },
185 "sasl": {
186 children: serviceCommandSet{
187 "set-plain": {
188 usage: "<network name> <username> <password>",
189 desc: "set SASL PLAIN credentials",
190 handle: handleServiceSASLSetPlain,
191 },
192 "reset": {
193 usage: "<network name>",
194 desc: "disable SASL authentication and remove stored credentials",
195 handle: handleServiceSASLReset,
196 },
197 },
198 },
199 "user": {
200 children: serviceCommandSet{
201 "create": {
202 usage: "-username <username> -password <password> [-admin]",
203 desc: "create a new soju user",
204 handle: handleUserCreate,
205 admin: true,
206 },
207 "delete": {
208 usage: "<username>",
209 desc: "delete a user",
210 handle: handleUserDelete,
211 admin: true,
212 },
213 },
214 admin: true,
215 },
216 "change-password": {
217 usage: "<new password>",
218 desc: "change your password",
219 handle: handlePasswordChange,
220 },
221 }
222}
223
224func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, admin bool, l *[]string) {
225 for _, name := range cmds.Names() {
226 cmd := cmds[name]
227 if cmd.admin && !admin {
228 continue
229 }
230 words := append(prefix, name)
231 if len(cmd.children) == 0 {
232 s := strings.Join(words, " ")
233 *l = append(*l, s)
234 } else {
235 appendServiceCommandSetHelp(cmd.children, words, admin, l)
236 }
237 }
238}
239
240func handleServiceHelp(dc *downstreamConn, params []string) error {
241 if len(params) > 0 {
242 cmd, rest, err := serviceCommands.Get(params)
243 if err != nil {
244 return err
245 }
246 words := params[:len(params)-len(rest)]
247
248 if len(cmd.children) > 0 {
249 var l []string
250 appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
251 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
252 } else {
253 text := strings.Join(words, " ")
254 if cmd.usage != "" {
255 text += " " + cmd.usage
256 }
257 text += ": " + cmd.desc
258
259 sendServicePRIVMSG(dc, text)
260 }
261 } else {
262 var l []string
263 appendServiceCommandSetHelp(serviceCommands, nil, dc.user.Admin, &l)
264 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
265 }
266 return nil
267}
268
269func newFlagSet() *flag.FlagSet {
270 fs := flag.NewFlagSet("", flag.ContinueOnError)
271 fs.SetOutput(ioutil.Discard)
272 return fs
273}
274
275type stringSliceFlag []string
276
277func (v *stringSliceFlag) String() string {
278 return fmt.Sprint([]string(*v))
279}
280
281func (v *stringSliceFlag) Set(s string) error {
282 *v = append(*v, s)
283 return nil
284}
285
286// stringPtrFlag is a flag value populating a string pointer. This allows to
287// disambiguate between a flag that hasn't been set and a flag that has been
288// set to an empty string.
289type stringPtrFlag struct {
290 ptr **string
291}
292
293func (f stringPtrFlag) String() string {
294 if f.ptr == nil || *f.ptr == nil {
295 return ""
296 }
297 return **f.ptr
298}
299
300func (f stringPtrFlag) Set(s string) error {
301 *f.ptr = &s
302 return nil
303}
304
305type networkFlagSet struct {
306 *flag.FlagSet
307 Addr, Name, Nick, Username, Pass, Realname *string
308 ConnectCommands []string
309}
310
311func newNetworkFlagSet() *networkFlagSet {
312 fs := &networkFlagSet{FlagSet: newFlagSet()}
313 fs.Var(stringPtrFlag{&fs.Addr}, "addr", "")
314 fs.Var(stringPtrFlag{&fs.Name}, "name", "")
315 fs.Var(stringPtrFlag{&fs.Nick}, "nick", "")
316 fs.Var(stringPtrFlag{&fs.Username}, "username", "")
317 fs.Var(stringPtrFlag{&fs.Pass}, "pass", "")
318 fs.Var(stringPtrFlag{&fs.Realname}, "realname", "")
319 fs.Var((*stringSliceFlag)(&fs.ConnectCommands), "connect-command", "")
320 return fs
321}
322
323func (fs *networkFlagSet) update(network *Network) error {
324 if fs.Addr != nil {
325 if addrParts := strings.SplitN(*fs.Addr, "://", 2); len(addrParts) == 2 {
326 scheme := addrParts[0]
327 switch scheme {
328 case "ircs", "irc+insecure", "unix":
329 default:
330 return fmt.Errorf("unknown scheme %q (supported schemes: ircs, irc+insecure, unix)", scheme)
331 }
332 }
333 network.Addr = *fs.Addr
334 }
335 if fs.Name != nil {
336 network.Name = *fs.Name
337 }
338 if fs.Nick != nil {
339 network.Nick = *fs.Nick
340 }
341 if fs.Username != nil {
342 network.Username = *fs.Username
343 }
344 if fs.Pass != nil {
345 network.Pass = *fs.Pass
346 }
347 if fs.Realname != nil {
348 network.Realname = *fs.Realname
349 }
350 if fs.ConnectCommands != nil {
351 if len(fs.ConnectCommands) == 1 && fs.ConnectCommands[0] == "" {
352 network.ConnectCommands = nil
353 } else {
354 for _, command := range fs.ConnectCommands {
355 _, err := irc.ParseMessage(command)
356 if err != nil {
357 return fmt.Errorf("flag -connect-command must be a valid raw irc command string: %q: %v", command, err)
358 }
359 }
360 network.ConnectCommands = fs.ConnectCommands
361 }
362 }
363 return nil
364}
365
366func handleServiceNetworkCreate(dc *downstreamConn, params []string) error {
367 fs := newNetworkFlagSet()
368 if err := fs.Parse(params); err != nil {
369 return err
370 }
371 if fs.Addr == nil {
372 return fmt.Errorf("flag -addr is required")
373 }
374
375 record := &Network{
376 Addr: *fs.Addr,
377 Nick: dc.nick,
378 }
379 if err := fs.update(record); err != nil {
380 return err
381 }
382
383 network, err := dc.user.createNetwork(record)
384 if err != nil {
385 return fmt.Errorf("could not create network: %v", err)
386 }
387
388 sendServicePRIVMSG(dc, fmt.Sprintf("created network %q", network.GetName()))
389 return nil
390}
391
392func handleServiceNetworkStatus(dc *downstreamConn, params []string) error {
393 dc.user.forEachNetwork(func(net *network) {
394 var statuses []string
395 var details string
396 if uc := net.conn; uc != nil {
397 if dc.nick != uc.nick {
398 statuses = append(statuses, "connected as "+uc.nick)
399 } else {
400 statuses = append(statuses, "connected")
401 }
402 details = fmt.Sprintf("%v channels", len(uc.channels))
403 } else {
404 statuses = append(statuses, "disconnected")
405 if net.lastError != nil {
406 details = net.lastError.Error()
407 }
408 }
409
410 if net == dc.network {
411 statuses = append(statuses, "current")
412 }
413
414 name := net.GetName()
415 if name != net.Addr {
416 name = fmt.Sprintf("%v (%v)", name, net.Addr)
417 }
418
419 s := fmt.Sprintf("%v [%v]", name, strings.Join(statuses, ", "))
420 if details != "" {
421 s += ": " + details
422 }
423 sendServicePRIVMSG(dc, s)
424 })
425 return nil
426}
427
428func handleServiceNetworkUpdate(dc *downstreamConn, params []string) error {
429 if len(params) < 1 {
430 return fmt.Errorf("expected exactly one argument")
431 }
432
433 fs := newNetworkFlagSet()
434 if err := fs.Parse(params[1:]); err != nil {
435 return err
436 }
437
438 net := dc.user.getNetwork(params[0])
439 if net == nil {
440 return fmt.Errorf("unknown network %q", params[0])
441 }
442
443 record := net.Network // copy network record because we'll mutate it
444 if err := fs.update(&record); err != nil {
445 return err
446 }
447
448 network, err := dc.user.updateNetwork(&record)
449 if err != nil {
450 return fmt.Errorf("could not update network: %v", err)
451 }
452
453 sendServicePRIVMSG(dc, fmt.Sprintf("updated network %q", network.GetName()))
454 return nil
455}
456
457func handleServiceNetworkDelete(dc *downstreamConn, params []string) error {
458 if len(params) != 1 {
459 return fmt.Errorf("expected exactly one argument")
460 }
461
462 net := dc.user.getNetwork(params[0])
463 if net == nil {
464 return fmt.Errorf("unknown network %q", params[0])
465 }
466
467 if err := dc.user.deleteNetwork(net.ID); err != nil {
468 return err
469 }
470
471 sendServicePRIVMSG(dc, fmt.Sprintf("deleted network %q", net.GetName()))
472 return nil
473}
474
475func handleServiceCertfpGenerate(dc *downstreamConn, params []string) error {
476 fs := newFlagSet()
477 keyType := fs.String("key-type", "rsa", "key type to generate (rsa, ecdsa, ed25519)")
478 bits := fs.Int("bits", 3072, "size of key to generate, meaningful only for RSA")
479
480 if err := fs.Parse(params); err != nil {
481 return err
482 }
483
484 if len(fs.Args()) != 1 {
485 return errors.New("exactly one argument is required")
486 }
487
488 net := dc.user.getNetwork(fs.Arg(0))
489 if net == nil {
490 return fmt.Errorf("unknown network %q", fs.Arg(0))
491 }
492
493 var (
494 privKey crypto.PrivateKey
495 pubKey crypto.PublicKey
496 )
497 switch *keyType {
498 case "rsa":
499 key, err := rsa.GenerateKey(rand.Reader, *bits)
500 if err != nil {
501 return err
502 }
503 privKey = key
504 pubKey = key.Public()
505 case "ecdsa":
506 key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
507 if err != nil {
508 return err
509 }
510 privKey = key
511 pubKey = key.Public()
512 case "ed25519":
513 var err error
514 pubKey, privKey, err = ed25519.GenerateKey(rand.Reader)
515 if err != nil {
516 return err
517 }
518 }
519
520 // Using PKCS#8 allows easier extension for new key types.
521 privKeyBytes, err := x509.MarshalPKCS8PrivateKey(privKey)
522 if err != nil {
523 return err
524 }
525
526 notBefore := time.Now()
527 // Lets make a fair assumption nobody will use the same cert for more than 20 years...
528 notAfter := notBefore.Add(24 * time.Hour * 365 * 20)
529 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
530 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
531 if err != nil {
532 return err
533 }
534 cert := &x509.Certificate{
535 SerialNumber: serialNumber,
536 Subject: pkix.Name{CommonName: "soju auto-generated certificate"},
537 NotBefore: notBefore,
538 NotAfter: notAfter,
539 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
540 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
541 }
542 derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, pubKey, privKey)
543 if err != nil {
544 return err
545 }
546
547 net.SASL.External.CertBlob = derBytes
548 net.SASL.External.PrivKeyBlob = privKeyBytes
549 net.SASL.Mechanism = "EXTERNAL"
550
551 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
552 return err
553 }
554
555 sendServicePRIVMSG(dc, "certificate generated")
556
557 sha1Sum := sha1.Sum(derBytes)
558 sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
559 sha256Sum := sha256.Sum256(derBytes)
560 sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
561
562 return nil
563}
564
565func handleServiceCertfpFingerprints(dc *downstreamConn, params []string) error {
566 if len(params) != 1 {
567 return fmt.Errorf("expected exactly one argument")
568 }
569
570 net := dc.user.getNetwork(params[0])
571 if net == nil {
572 return fmt.Errorf("unknown network %q", params[0])
573 }
574
575 sha1Sum := sha1.Sum(net.SASL.External.CertBlob)
576 sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
577 sha256Sum := sha256.Sum256(net.SASL.External.CertBlob)
578 sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
579 return nil
580}
581
582func handleServiceSASLSetPlain(dc *downstreamConn, params []string) error {
583 if len(params) != 3 {
584 return fmt.Errorf("expected exactly 3 arguments")
585 }
586
587 net := dc.user.getNetwork(params[0])
588 if net == nil {
589 return fmt.Errorf("unknown network %q", params[0])
590 }
591
592 net.SASL.Plain.Username = params[1]
593 net.SASL.Plain.Password = params[2]
594 net.SASL.Mechanism = "PLAIN"
595
596 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
597 return err
598 }
599
600 sendServicePRIVMSG(dc, "credentials saved")
601 return nil
602}
603
604func handleServiceSASLReset(dc *downstreamConn, params []string) error {
605 if len(params) != 1 {
606 return fmt.Errorf("expected exactly one argument")
607 }
608
609 net := dc.user.getNetwork(params[0])
610 if net == nil {
611 return fmt.Errorf("unknown network %q", params[0])
612 }
613
614 net.SASL.Plain.Username = ""
615 net.SASL.Plain.Password = ""
616 net.SASL.External.CertBlob = nil
617 net.SASL.External.PrivKeyBlob = nil
618 net.SASL.Mechanism = ""
619
620 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
621 return err
622 }
623
624 sendServicePRIVMSG(dc, "credentials reset")
625 return nil
626}
627
628func handlePasswordChange(dc *downstreamConn, params []string) error {
629 if len(params) != 1 {
630 return fmt.Errorf("expected exactly one argument")
631 }
632
633 hashed, err := bcrypt.GenerateFromPassword([]byte(params[0]), bcrypt.DefaultCost)
634 if err != nil {
635 return fmt.Errorf("failed to hash password: %v", err)
636 }
637 if err := dc.user.updatePassword(string(hashed)); err != nil {
638 return err
639 }
640
641 sendServicePRIVMSG(dc, "password updated")
642 return nil
643}
644
645func handleUserCreate(dc *downstreamConn, params []string) error {
646 fs := newFlagSet()
647 username := fs.String("username", "", "")
648 password := fs.String("password", "", "")
649 admin := fs.Bool("admin", false, "")
650
651 if err := fs.Parse(params); err != nil {
652 return err
653 }
654 if *username == "" {
655 return fmt.Errorf("flag -username is required")
656 }
657 if *password == "" {
658 return fmt.Errorf("flag -password is required")
659 }
660
661 hashed, err := bcrypt.GenerateFromPassword([]byte(*password), bcrypt.DefaultCost)
662 if err != nil {
663 return fmt.Errorf("failed to hash password: %v", err)
664 }
665
666 user := &User{
667 Username: *username,
668 Password: string(hashed),
669 Admin: *admin,
670 }
671 if _, err := dc.srv.createUser(user); err != nil {
672 return fmt.Errorf("could not create user: %v", err)
673 }
674
675 sendServicePRIVMSG(dc, fmt.Sprintf("created user %q", *username))
676 return nil
677}
678
679func handleUserDelete(dc *downstreamConn, params []string) error {
680 if len(params) != 1 {
681 return fmt.Errorf("expected exactly one argument")
682 }
683 username := params[0]
684
685 u := dc.srv.getUser(username)
686 if u == nil {
687 return fmt.Errorf("unknown username %q", username)
688 }
689
690 u.stop()
691
692 if err := dc.srv.db.DeleteUser(dc.user.ID); err != nil {
693 return fmt.Errorf("failed to delete user: %v", err)
694 }
695
696 sendServicePRIVMSG(dc, fmt.Sprintf("deleted user %q", username))
697 return nil
698}
Note: See TracBrowser for help on using the repository browser.