source: code/trunk/service.go@ 575

Last change on this file since 575 was 575, checked in by yyp, 4 years ago

service: show SHA-512 fingerprint

Closes: https://todo.sr.ht/~emersion/soju/130

File size: 23.6 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/sha512"
13 "crypto/x509"
14 "crypto/x509/pkix"
15 "encoding/hex"
16 "errors"
17 "flag"
18 "fmt"
19 "io/ioutil"
20 "math/big"
21 "sort"
22 "strconv"
23 "strings"
24 "time"
25 "unicode"
26
27 "golang.org/x/crypto/bcrypt"
28 "gopkg.in/irc.v3"
29)
30
31const serviceNick = "BouncerServ"
32const serviceNickCM = "bouncerserv"
33const serviceRealname = "soju bouncer service"
34
35var servicePrefix = &irc.Prefix{
36 Name: serviceNick,
37 User: serviceNick,
38 Host: serviceNick,
39}
40
41type serviceCommandSet map[string]*serviceCommand
42
43type serviceCommand struct {
44 usage string
45 desc string
46 handle func(dc *downstreamConn, params []string) error
47 children serviceCommandSet
48 admin bool
49}
50
51func sendServiceNOTICE(dc *downstreamConn, text string) {
52 dc.SendMessage(&irc.Message{
53 Prefix: servicePrefix,
54 Command: "NOTICE",
55 Params: []string{dc.nick, text},
56 })
57}
58
59func sendServicePRIVMSG(dc *downstreamConn, text string) {
60 dc.SendMessage(&irc.Message{
61 Prefix: servicePrefix,
62 Command: "PRIVMSG",
63 Params: []string{dc.nick, text},
64 })
65}
66
67func splitWords(s string) ([]string, error) {
68 var words []string
69 var lastWord strings.Builder
70 escape := false
71 prev := ' '
72 wordDelim := ' '
73
74 for _, r := range s {
75 if escape {
76 // last char was a backslash, write the byte as-is.
77 lastWord.WriteRune(r)
78 escape = false
79 } else if r == '\\' {
80 escape = true
81 } else if wordDelim == ' ' && unicode.IsSpace(r) {
82 // end of last word
83 if !unicode.IsSpace(prev) {
84 words = append(words, lastWord.String())
85 lastWord.Reset()
86 }
87 } else if r == wordDelim {
88 // wordDelim is either " or ', switch back to
89 // space-delimited words.
90 wordDelim = ' '
91 } else if r == '"' || r == '\'' {
92 if wordDelim == ' ' {
93 // start of (double-)quoted word
94 wordDelim = r
95 } else {
96 // either wordDelim is " and r is ' or vice-versa
97 lastWord.WriteRune(r)
98 }
99 } else {
100 lastWord.WriteRune(r)
101 }
102
103 prev = r
104 }
105
106 if !unicode.IsSpace(prev) {
107 words = append(words, lastWord.String())
108 }
109
110 if wordDelim != ' ' {
111 return nil, fmt.Errorf("unterminated quoted string")
112 }
113 if escape {
114 return nil, fmt.Errorf("unterminated backslash sequence")
115 }
116
117 return words, nil
118}
119
120func handleServicePRIVMSG(dc *downstreamConn, text string) {
121 words, err := splitWords(text)
122 if err != nil {
123 sendServicePRIVMSG(dc, fmt.Sprintf(`error: failed to parse command: %v`, err))
124 return
125 }
126
127 cmd, params, err := serviceCommands.Get(words)
128 if err != nil {
129 sendServicePRIVMSG(dc, fmt.Sprintf(`error: %v (type "help" for a list of commands)`, err))
130 return
131 }
132 if cmd.admin && !dc.user.Admin {
133 sendServicePRIVMSG(dc, fmt.Sprintf(`error: you must be an admin to use this command`))
134 return
135 }
136
137 if cmd.handle == nil {
138 if len(cmd.children) > 0 {
139 var l []string
140 appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
141 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
142 } else {
143 // Pretend the command does not exist if it has neither children nor handler.
144 // This is obviously a bug but it is better to not die anyway.
145 dc.logger.Printf("command without handler and subcommands invoked:", words[0])
146 sendServicePRIVMSG(dc, fmt.Sprintf("command %q not found", words[0]))
147 }
148 return
149 }
150
151 if err := cmd.handle(dc, params); err != nil {
152 sendServicePRIVMSG(dc, fmt.Sprintf("error: %v", err))
153 }
154}
155
156func (cmds serviceCommandSet) Get(params []string) (*serviceCommand, []string, error) {
157 if len(params) == 0 {
158 return nil, nil, fmt.Errorf("no command specified")
159 }
160
161 name := params[0]
162 params = params[1:]
163
164 cmd, ok := cmds[name]
165 if !ok {
166 for k := range cmds {
167 if !strings.HasPrefix(k, name) {
168 continue
169 }
170 if cmd != nil {
171 return nil, params, fmt.Errorf("command %q is ambiguous", name)
172 }
173 cmd = cmds[k]
174 }
175 }
176 if cmd == nil {
177 return nil, params, fmt.Errorf("command %q not found", name)
178 }
179
180 if len(params) == 0 || len(cmd.children) == 0 {
181 return cmd, params, nil
182 }
183 return cmd.children.Get(params)
184}
185
186func (cmds serviceCommandSet) Names() []string {
187 l := make([]string, 0, len(cmds))
188 for name := range cmds {
189 l = append(l, name)
190 }
191 sort.Strings(l)
192 return l
193}
194
195var serviceCommands serviceCommandSet
196
197func init() {
198 serviceCommands = serviceCommandSet{
199 "help": {
200 usage: "[command]",
201 desc: "print help message",
202 handle: handleServiceHelp,
203 },
204 "network": {
205 children: serviceCommandSet{
206 "create": {
207 usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-enabled enabled] [-connect-command command]...",
208 desc: "add a new network",
209 handle: handleServiceNetworkCreate,
210 },
211 "status": {
212 desc: "show a list of saved networks and their current status",
213 handle: handleServiceNetworkStatus,
214 },
215 "update": {
216 usage: "<name> [-addr addr] [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-enabled enabled] [-connect-command command]...",
217 desc: "update a network",
218 handle: handleServiceNetworkUpdate,
219 },
220 "delete": {
221 usage: "<name>",
222 desc: "delete a network",
223 handle: handleServiceNetworkDelete,
224 },
225 },
226 },
227 "certfp": {
228 children: serviceCommandSet{
229 "generate": {
230 usage: "[-key-type rsa|ecdsa|ed25519] [-bits N] <network name>",
231 desc: "generate a new self-signed certificate, defaults to using RSA-3072 key",
232 handle: handleServiceCertfpGenerate,
233 },
234 "fingerprint": {
235 usage: "<network name>",
236 desc: "show fingerprints of certificate associated with the network",
237 handle: handleServiceCertfpFingerprints,
238 },
239 },
240 },
241 "sasl": {
242 children: serviceCommandSet{
243 "set-plain": {
244 usage: "<network name> <username> <password>",
245 desc: "set SASL PLAIN credentials",
246 handle: handleServiceSASLSetPlain,
247 },
248 "reset": {
249 usage: "<network name>",
250 desc: "disable SASL authentication and remove stored credentials",
251 handle: handleServiceSASLReset,
252 },
253 },
254 },
255 "user": {
256 children: serviceCommandSet{
257 "create": {
258 usage: "-username <username> -password <password> [-realname <realname>] [-admin]",
259 desc: "create a new soju user",
260 handle: handleUserCreate,
261 admin: true,
262 },
263 "update": {
264 usage: "[-password <password>] [-realname <realname>]",
265 desc: "update the current user",
266 handle: handleUserUpdate,
267 },
268 "delete": {
269 usage: "<username>",
270 desc: "delete a user",
271 handle: handleUserDelete,
272 admin: true,
273 },
274 },
275 },
276 "channel": {
277 children: serviceCommandSet{
278 "status": {
279 usage: "[-network name]",
280 desc: "show a list of saved channels and their current status",
281 handle: handleServiceChannelStatus,
282 },
283 "update": {
284 usage: "<name> [-relay-detached <default|none|highlight|message>] [-reattach-on <default|none|highlight|message>] [-detach-after <duration>] [-detach-on <default|none|highlight|message>]",
285 desc: "update a channel",
286 handle: handleServiceChannelUpdate,
287 },
288 },
289 },
290 }
291}
292
293func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, admin bool, l *[]string) {
294 for _, name := range cmds.Names() {
295 cmd := cmds[name]
296 if cmd.admin && !admin {
297 continue
298 }
299 words := append(prefix, name)
300 if len(cmd.children) == 0 {
301 s := strings.Join(words, " ")
302 *l = append(*l, s)
303 } else {
304 appendServiceCommandSetHelp(cmd.children, words, admin, l)
305 }
306 }
307}
308
309func handleServiceHelp(dc *downstreamConn, params []string) error {
310 if len(params) > 0 {
311 cmd, rest, err := serviceCommands.Get(params)
312 if err != nil {
313 return err
314 }
315 words := params[:len(params)-len(rest)]
316
317 if len(cmd.children) > 0 {
318 var l []string
319 appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
320 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
321 } else {
322 text := strings.Join(words, " ")
323 if cmd.usage != "" {
324 text += " " + cmd.usage
325 }
326 text += ": " + cmd.desc
327
328 sendServicePRIVMSG(dc, text)
329 }
330 } else {
331 var l []string
332 appendServiceCommandSetHelp(serviceCommands, nil, dc.user.Admin, &l)
333 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
334 }
335 return nil
336}
337
338func newFlagSet() *flag.FlagSet {
339 fs := flag.NewFlagSet("", flag.ContinueOnError)
340 fs.SetOutput(ioutil.Discard)
341 return fs
342}
343
344type stringSliceFlag []string
345
346func (v *stringSliceFlag) String() string {
347 return fmt.Sprint([]string(*v))
348}
349
350func (v *stringSliceFlag) Set(s string) error {
351 *v = append(*v, s)
352 return nil
353}
354
355// stringPtrFlag is a flag value populating a string pointer. This allows to
356// disambiguate between a flag that hasn't been set and a flag that has been
357// set to an empty string.
358type stringPtrFlag struct {
359 ptr **string
360}
361
362func (f stringPtrFlag) String() string {
363 if f.ptr == nil || *f.ptr == nil {
364 return ""
365 }
366 return **f.ptr
367}
368
369func (f stringPtrFlag) Set(s string) error {
370 *f.ptr = &s
371 return nil
372}
373
374type boolPtrFlag struct {
375 ptr **bool
376}
377
378func (f boolPtrFlag) String() string {
379 if f.ptr == nil || *f.ptr == nil {
380 return "<nil>"
381 }
382 return strconv.FormatBool(**f.ptr)
383}
384
385func (f boolPtrFlag) Set(s string) error {
386 v, err := strconv.ParseBool(s)
387 if err != nil {
388 return err
389 }
390 *f.ptr = &v
391 return nil
392}
393
394type networkFlagSet struct {
395 *flag.FlagSet
396 Addr, Name, Nick, Username, Pass, Realname *string
397 Enabled *bool
398 ConnectCommands []string
399}
400
401func newNetworkFlagSet() *networkFlagSet {
402 fs := &networkFlagSet{FlagSet: newFlagSet()}
403 fs.Var(stringPtrFlag{&fs.Addr}, "addr", "")
404 fs.Var(stringPtrFlag{&fs.Name}, "name", "")
405 fs.Var(stringPtrFlag{&fs.Nick}, "nick", "")
406 fs.Var(stringPtrFlag{&fs.Username}, "username", "")
407 fs.Var(stringPtrFlag{&fs.Pass}, "pass", "")
408 fs.Var(stringPtrFlag{&fs.Realname}, "realname", "")
409 fs.Var(boolPtrFlag{&fs.Enabled}, "enabled", "")
410 fs.Var((*stringSliceFlag)(&fs.ConnectCommands), "connect-command", "")
411 return fs
412}
413
414func (fs *networkFlagSet) update(network *Network) error {
415 if fs.Addr != nil {
416 if addrParts := strings.SplitN(*fs.Addr, "://", 2); len(addrParts) == 2 {
417 scheme := addrParts[0]
418 switch scheme {
419 case "ircs", "irc+insecure", "unix":
420 default:
421 return fmt.Errorf("unknown scheme %q (supported schemes: ircs, irc+insecure, unix)", scheme)
422 }
423 }
424 network.Addr = *fs.Addr
425 }
426 if fs.Name != nil {
427 network.Name = *fs.Name
428 }
429 if fs.Nick != nil {
430 network.Nick = *fs.Nick
431 }
432 if fs.Username != nil {
433 network.Username = *fs.Username
434 }
435 if fs.Pass != nil {
436 network.Pass = *fs.Pass
437 }
438 if fs.Realname != nil {
439 network.Realname = *fs.Realname
440 }
441 if fs.Enabled != nil {
442 network.Enabled = *fs.Enabled
443 }
444 if fs.ConnectCommands != nil {
445 if len(fs.ConnectCommands) == 1 && fs.ConnectCommands[0] == "" {
446 network.ConnectCommands = nil
447 } else {
448 for _, command := range fs.ConnectCommands {
449 _, err := irc.ParseMessage(command)
450 if err != nil {
451 return fmt.Errorf("flag -connect-command must be a valid raw irc command string: %q: %v", command, err)
452 }
453 }
454 network.ConnectCommands = fs.ConnectCommands
455 }
456 }
457 return nil
458}
459
460func handleServiceNetworkCreate(dc *downstreamConn, params []string) error {
461 fs := newNetworkFlagSet()
462 if err := fs.Parse(params); err != nil {
463 return err
464 }
465 if fs.Addr == nil {
466 return fmt.Errorf("flag -addr is required")
467 }
468
469 record := &Network{
470 Addr: *fs.Addr,
471 Nick: dc.nick,
472 Enabled: true,
473 }
474 if err := fs.update(record); err != nil {
475 return err
476 }
477
478 network, err := dc.user.createNetwork(record)
479 if err != nil {
480 return fmt.Errorf("could not create network: %v", err)
481 }
482
483 sendServicePRIVMSG(dc, fmt.Sprintf("created network %q", network.GetName()))
484 return nil
485}
486
487func handleServiceNetworkStatus(dc *downstreamConn, params []string) error {
488 n := 0
489 dc.user.forEachNetwork(func(net *network) {
490 var statuses []string
491 var details string
492 if uc := net.conn; uc != nil {
493 if dc.nick != uc.nick {
494 statuses = append(statuses, "connected as "+uc.nick)
495 } else {
496 statuses = append(statuses, "connected")
497 }
498 details = fmt.Sprintf("%v channels", uc.channels.Len())
499 } else if !net.Enabled {
500 statuses = append(statuses, "disabled")
501 } else {
502 statuses = append(statuses, "disconnected")
503 if net.lastError != nil {
504 details = net.lastError.Error()
505 }
506 }
507
508 if net == dc.network {
509 statuses = append(statuses, "current")
510 }
511
512 name := net.GetName()
513 if name != net.Addr {
514 name = fmt.Sprintf("%v (%v)", name, net.Addr)
515 }
516
517 s := fmt.Sprintf("%v [%v]", name, strings.Join(statuses, ", "))
518 if details != "" {
519 s += ": " + details
520 }
521 sendServicePRIVMSG(dc, s)
522
523 n++
524 })
525
526 if n == 0 {
527 sendServicePRIVMSG(dc, `No network configured, add one with "network create".`)
528 }
529
530 return nil
531}
532
533func handleServiceNetworkUpdate(dc *downstreamConn, params []string) error {
534 if len(params) < 1 {
535 return fmt.Errorf("expected at least one argument")
536 }
537
538 fs := newNetworkFlagSet()
539 if err := fs.Parse(params[1:]); err != nil {
540 return err
541 }
542
543 net := dc.user.getNetwork(params[0])
544 if net == nil {
545 return fmt.Errorf("unknown network %q", params[0])
546 }
547
548 record := net.Network // copy network record because we'll mutate it
549 if err := fs.update(&record); err != nil {
550 return err
551 }
552
553 network, err := dc.user.updateNetwork(&record)
554 if err != nil {
555 return fmt.Errorf("could not update network: %v", err)
556 }
557
558 sendServicePRIVMSG(dc, fmt.Sprintf("updated network %q", network.GetName()))
559 return nil
560}
561
562func handleServiceNetworkDelete(dc *downstreamConn, params []string) error {
563 if len(params) != 1 {
564 return fmt.Errorf("expected exactly one argument")
565 }
566
567 net := dc.user.getNetwork(params[0])
568 if net == nil {
569 return fmt.Errorf("unknown network %q", params[0])
570 }
571
572 if err := dc.user.deleteNetwork(net.ID); err != nil {
573 return err
574 }
575
576 sendServicePRIVMSG(dc, fmt.Sprintf("deleted network %q", net.GetName()))
577 return nil
578}
579
580func handleServiceCertfpGenerate(dc *downstreamConn, params []string) error {
581 fs := newFlagSet()
582 keyType := fs.String("key-type", "rsa", "key type to generate (rsa, ecdsa, ed25519)")
583 bits := fs.Int("bits", 3072, "size of key to generate, meaningful only for RSA")
584
585 if err := fs.Parse(params); err != nil {
586 return err
587 }
588
589 if len(fs.Args()) != 1 {
590 return errors.New("exactly one argument is required")
591 }
592
593 net := dc.user.getNetwork(fs.Arg(0))
594 if net == nil {
595 return fmt.Errorf("unknown network %q", fs.Arg(0))
596 }
597
598 var (
599 privKey crypto.PrivateKey
600 pubKey crypto.PublicKey
601 )
602 switch *keyType {
603 case "rsa":
604 key, err := rsa.GenerateKey(rand.Reader, *bits)
605 if err != nil {
606 return err
607 }
608 privKey = key
609 pubKey = key.Public()
610 case "ecdsa":
611 key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
612 if err != nil {
613 return err
614 }
615 privKey = key
616 pubKey = key.Public()
617 case "ed25519":
618 var err error
619 pubKey, privKey, err = ed25519.GenerateKey(rand.Reader)
620 if err != nil {
621 return err
622 }
623 }
624
625 // Using PKCS#8 allows easier extension for new key types.
626 privKeyBytes, err := x509.MarshalPKCS8PrivateKey(privKey)
627 if err != nil {
628 return err
629 }
630
631 notBefore := time.Now()
632 // Lets make a fair assumption nobody will use the same cert for more than 20 years...
633 notAfter := notBefore.Add(24 * time.Hour * 365 * 20)
634 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
635 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
636 if err != nil {
637 return err
638 }
639 cert := &x509.Certificate{
640 SerialNumber: serialNumber,
641 Subject: pkix.Name{CommonName: "soju auto-generated certificate"},
642 NotBefore: notBefore,
643 NotAfter: notAfter,
644 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
645 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
646 }
647 derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, pubKey, privKey)
648 if err != nil {
649 return err
650 }
651
652 net.SASL.External.CertBlob = derBytes
653 net.SASL.External.PrivKeyBlob = privKeyBytes
654 net.SASL.Mechanism = "EXTERNAL"
655
656 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
657 return err
658 }
659
660 sendServicePRIVMSG(dc, "certificate generated")
661
662 sha1Sum := sha1.Sum(derBytes)
663 sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
664 sha256Sum := sha256.Sum256(derBytes)
665 sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
666 sha512Sum := sha512.Sum512(derBytes)
667 sendServicePRIVMSG(dc, "SHA-512 fingerprint: "+hex.EncodeToString(sha512Sum[:]))
668
669 return nil
670}
671
672func handleServiceCertfpFingerprints(dc *downstreamConn, params []string) error {
673 if len(params) != 1 {
674 return fmt.Errorf("expected exactly one argument")
675 }
676
677 net := dc.user.getNetwork(params[0])
678 if net == nil {
679 return fmt.Errorf("unknown network %q", params[0])
680 }
681
682 sha1Sum := sha1.Sum(net.SASL.External.CertBlob)
683 sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
684 sha256Sum := sha256.Sum256(net.SASL.External.CertBlob)
685 sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
686 sha512Sum := sha512.Sum512(net.SASL.External.CertBlob)
687 sendServicePRIVMSG(dc, "SHA-512 fingerprint: "+hex.EncodeToString(sha512Sum[:]))
688 return nil
689}
690
691func handleServiceSASLSetPlain(dc *downstreamConn, params []string) error {
692 if len(params) != 3 {
693 return fmt.Errorf("expected exactly 3 arguments")
694 }
695
696 net := dc.user.getNetwork(params[0])
697 if net == nil {
698 return fmt.Errorf("unknown network %q", params[0])
699 }
700
701 net.SASL.Plain.Username = params[1]
702 net.SASL.Plain.Password = params[2]
703 net.SASL.Mechanism = "PLAIN"
704
705 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
706 return err
707 }
708
709 sendServicePRIVMSG(dc, "credentials saved")
710 return nil
711}
712
713func handleServiceSASLReset(dc *downstreamConn, params []string) error {
714 if len(params) != 1 {
715 return fmt.Errorf("expected exactly one argument")
716 }
717
718 net := dc.user.getNetwork(params[0])
719 if net == nil {
720 return fmt.Errorf("unknown network %q", params[0])
721 }
722
723 net.SASL.Plain.Username = ""
724 net.SASL.Plain.Password = ""
725 net.SASL.External.CertBlob = nil
726 net.SASL.External.PrivKeyBlob = nil
727 net.SASL.Mechanism = ""
728
729 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
730 return err
731 }
732
733 sendServicePRIVMSG(dc, "credentials reset")
734 return nil
735}
736
737func handleUserCreate(dc *downstreamConn, params []string) error {
738 fs := newFlagSet()
739 username := fs.String("username", "", "")
740 password := fs.String("password", "", "")
741 realname := fs.String("realname", "", "")
742 admin := fs.Bool("admin", false, "")
743
744 if err := fs.Parse(params); err != nil {
745 return err
746 }
747 if *username == "" {
748 return fmt.Errorf("flag -username is required")
749 }
750 if *password == "" {
751 return fmt.Errorf("flag -password is required")
752 }
753
754 hashed, err := bcrypt.GenerateFromPassword([]byte(*password), bcrypt.DefaultCost)
755 if err != nil {
756 return fmt.Errorf("failed to hash password: %v", err)
757 }
758
759 user := &User{
760 Username: *username,
761 Password: string(hashed),
762 Realname: *realname,
763 Admin: *admin,
764 }
765 if _, err := dc.srv.createUser(user); err != nil {
766 return fmt.Errorf("could not create user: %v", err)
767 }
768
769 sendServicePRIVMSG(dc, fmt.Sprintf("created user %q", *username))
770 return nil
771}
772
773func handleUserUpdate(dc *downstreamConn, params []string) error {
774 var password, realname *string
775 fs := newFlagSet()
776 fs.Var(stringPtrFlag{&password}, "password", "")
777 fs.Var(stringPtrFlag{&realname}, "realname", "")
778
779 if err := fs.Parse(params); err != nil {
780 return err
781 }
782
783 // copy the user record because we'll mutate it
784 record := dc.user.User
785
786 if password != nil {
787 hashed, err := bcrypt.GenerateFromPassword([]byte(*password), bcrypt.DefaultCost)
788 if err != nil {
789 return fmt.Errorf("failed to hash password: %v", err)
790 }
791 record.Password = string(hashed)
792 }
793 if realname != nil {
794 record.Realname = *realname
795 }
796
797 if err := dc.user.updateUser(&record); err != nil {
798 return err
799 }
800
801 sendServicePRIVMSG(dc, fmt.Sprintf("updated user %q", dc.user.Username))
802 return nil
803}
804
805func handleUserDelete(dc *downstreamConn, params []string) error {
806 if len(params) != 1 {
807 return fmt.Errorf("expected exactly one argument")
808 }
809 username := params[0]
810
811 u := dc.srv.getUser(username)
812 if u == nil {
813 return fmt.Errorf("unknown username %q", username)
814 }
815
816 u.stop()
817
818 if err := dc.srv.db.DeleteUser(u.ID); err != nil {
819 return fmt.Errorf("failed to delete user: %v", err)
820 }
821
822 sendServicePRIVMSG(dc, fmt.Sprintf("deleted user %q", username))
823 return nil
824}
825
826func handleServiceChannelStatus(dc *downstreamConn, params []string) error {
827 var defaultNetworkName string
828 if dc.network != nil {
829 defaultNetworkName = dc.network.GetName()
830 }
831
832 fs := newFlagSet()
833 networkName := fs.String("network", defaultNetworkName, "")
834
835 if err := fs.Parse(params); err != nil {
836 return err
837 }
838
839 n := 0
840
841 sendNetwork := func(net *network) {
842 var channels []*Channel
843 for _, entry := range net.channels.innerMap {
844 channels = append(channels, entry.value.(*Channel))
845 }
846
847 sort.Slice(channels, func(i, j int) bool {
848 return strings.ReplaceAll(channels[i].Name, "#", "") <
849 strings.ReplaceAll(channels[j].Name, "#", "")
850 })
851
852 for _, ch := range channels {
853 var uch *upstreamChannel
854 if net.conn != nil {
855 uch = net.conn.channels.Value(ch.Name)
856 }
857
858 name := ch.Name
859 if *networkName == "" {
860 name += "/" + net.GetName()
861 }
862
863 var status string
864 if uch != nil {
865 status = "joined"
866 } else if net.conn != nil {
867 status = "parted"
868 } else {
869 status = "disconnected"
870 }
871
872 if ch.Detached {
873 status += ", detached"
874 }
875
876 s := fmt.Sprintf("%v [%v]", name, status)
877 sendServicePRIVMSG(dc, s)
878
879 n++
880 }
881 }
882
883 if *networkName == "" {
884 dc.user.forEachNetwork(sendNetwork)
885 } else {
886 net := dc.user.getNetwork(*networkName)
887 if net == nil {
888 return fmt.Errorf("unknown network %q", *networkName)
889 }
890 sendNetwork(net)
891 }
892
893 if n == 0 {
894 sendServicePRIVMSG(dc, "No channel configured.")
895 }
896
897 return nil
898}
899
900type channelFlagSet struct {
901 *flag.FlagSet
902 RelayDetached, ReattachOn, DetachAfter, DetachOn *string
903}
904
905func newChannelFlagSet() *channelFlagSet {
906 fs := &channelFlagSet{FlagSet: newFlagSet()}
907 fs.Var(stringPtrFlag{&fs.RelayDetached}, "relay-detached", "")
908 fs.Var(stringPtrFlag{&fs.ReattachOn}, "reattach-on", "")
909 fs.Var(stringPtrFlag{&fs.DetachAfter}, "detach-after", "")
910 fs.Var(stringPtrFlag{&fs.DetachOn}, "detach-on", "")
911 return fs
912}
913
914func (fs *channelFlagSet) update(channel *Channel) error {
915 if fs.RelayDetached != nil {
916 filter, err := parseFilter(*fs.RelayDetached)
917 if err != nil {
918 return err
919 }
920 channel.RelayDetached = filter
921 }
922 if fs.ReattachOn != nil {
923 filter, err := parseFilter(*fs.ReattachOn)
924 if err != nil {
925 return err
926 }
927 channel.ReattachOn = filter
928 }
929 if fs.DetachAfter != nil {
930 dur, err := time.ParseDuration(*fs.DetachAfter)
931 if err != nil || dur < 0 {
932 return fmt.Errorf("unknown duration for -detach-after %q (duration format: 0, 300s, 22h30m, ...)", *fs.DetachAfter)
933 }
934 channel.DetachAfter = dur
935 }
936 if fs.DetachOn != nil {
937 filter, err := parseFilter(*fs.DetachOn)
938 if err != nil {
939 return err
940 }
941 channel.DetachOn = filter
942 }
943 return nil
944}
945
946func handleServiceChannelUpdate(dc *downstreamConn, params []string) error {
947 if len(params) < 1 {
948 return fmt.Errorf("expected at least one argument")
949 }
950 name := params[0]
951
952 fs := newChannelFlagSet()
953 if err := fs.Parse(params[1:]); err != nil {
954 return err
955 }
956
957 uc, upstreamName, err := dc.unmarshalEntity(name)
958 if err != nil {
959 return fmt.Errorf("unknown channel %q", name)
960 }
961
962 ch := uc.network.channels.Value(upstreamName)
963 if ch == nil {
964 return fmt.Errorf("unknown channel %q", name)
965 }
966
967 if err := fs.update(ch); err != nil {
968 return err
969 }
970
971 uc.updateChannelAutoDetach(upstreamName)
972
973 if err := dc.srv.db.StoreChannel(uc.network.ID, ch); err != nil {
974 return fmt.Errorf("failed to update channel: %v", err)
975 }
976
977 sendServicePRIVMSG(dc, fmt.Sprintf("updated channel %q", name))
978 return nil
979}
Note: See TracBrowser for help on using the repository browser.