source: code/trunk/service.go@ 605

Last change on this file since 605 was 605, checked in by contact, 4 years ago

Add "server status" command

Right now, it prints the number of active users and number of
downstream connections.

File size: 24.7 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 "quote": {
226 usage: "<name> <command>",
227 desc: "send a raw line to a network",
228 handle: handleServiceNetworkQuote,
229 },
230 },
231 },
232 "certfp": {
233 children: serviceCommandSet{
234 "generate": {
235 usage: "[-key-type rsa|ecdsa|ed25519] [-bits N] <network name>",
236 desc: "generate a new self-signed certificate, defaults to using RSA-3072 key",
237 handle: handleServiceCertfpGenerate,
238 },
239 "fingerprint": {
240 usage: "<network name>",
241 desc: "show fingerprints of certificate associated with the network",
242 handle: handleServiceCertfpFingerprints,
243 },
244 },
245 },
246 "sasl": {
247 children: serviceCommandSet{
248 "set-plain": {
249 usage: "<network name> <username> <password>",
250 desc: "set SASL PLAIN credentials",
251 handle: handleServiceSASLSetPlain,
252 },
253 "reset": {
254 usage: "<network name>",
255 desc: "disable SASL authentication and remove stored credentials",
256 handle: handleServiceSASLReset,
257 },
258 },
259 },
260 "user": {
261 children: serviceCommandSet{
262 "create": {
263 usage: "-username <username> -password <password> [-realname <realname>] [-admin]",
264 desc: "create a new soju user",
265 handle: handleUserCreate,
266 admin: true,
267 },
268 "update": {
269 usage: "[-password <password>] [-realname <realname>]",
270 desc: "update the current user",
271 handle: handleUserUpdate,
272 },
273 "delete": {
274 usage: "<username>",
275 desc: "delete a user",
276 handle: handleUserDelete,
277 admin: true,
278 },
279 },
280 },
281 "channel": {
282 children: serviceCommandSet{
283 "status": {
284 usage: "[-network name]",
285 desc: "show a list of saved channels and their current status",
286 handle: handleServiceChannelStatus,
287 },
288 "update": {
289 usage: "<name> [-relay-detached <default|none|highlight|message>] [-reattach-on <default|none|highlight|message>] [-detach-after <duration>] [-detach-on <default|none|highlight|message>]",
290 desc: "update a channel",
291 handle: handleServiceChannelUpdate,
292 },
293 },
294 },
295 "server": {
296 children: serviceCommandSet{
297 "status": {
298 desc: "show server statistics",
299 handle: handleServiceServerStatus,
300 admin: true,
301 },
302 },
303 admin: true,
304 },
305 }
306}
307
308func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, admin bool, l *[]string) {
309 for _, name := range cmds.Names() {
310 cmd := cmds[name]
311 if cmd.admin && !admin {
312 continue
313 }
314 words := append(prefix, name)
315 if len(cmd.children) == 0 {
316 s := strings.Join(words, " ")
317 *l = append(*l, s)
318 } else {
319 appendServiceCommandSetHelp(cmd.children, words, admin, l)
320 }
321 }
322}
323
324func handleServiceHelp(dc *downstreamConn, params []string) error {
325 if len(params) > 0 {
326 cmd, rest, err := serviceCommands.Get(params)
327 if err != nil {
328 return err
329 }
330 words := params[:len(params)-len(rest)]
331
332 if len(cmd.children) > 0 {
333 var l []string
334 appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
335 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
336 } else {
337 text := strings.Join(words, " ")
338 if cmd.usage != "" {
339 text += " " + cmd.usage
340 }
341 text += ": " + cmd.desc
342
343 sendServicePRIVMSG(dc, text)
344 }
345 } else {
346 var l []string
347 appendServiceCommandSetHelp(serviceCommands, nil, dc.user.Admin, &l)
348 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
349 }
350 return nil
351}
352
353func newFlagSet() *flag.FlagSet {
354 fs := flag.NewFlagSet("", flag.ContinueOnError)
355 fs.SetOutput(ioutil.Discard)
356 return fs
357}
358
359type stringSliceFlag []string
360
361func (v *stringSliceFlag) String() string {
362 return fmt.Sprint([]string(*v))
363}
364
365func (v *stringSliceFlag) Set(s string) error {
366 *v = append(*v, s)
367 return nil
368}
369
370// stringPtrFlag is a flag value populating a string pointer. This allows to
371// disambiguate between a flag that hasn't been set and a flag that has been
372// set to an empty string.
373type stringPtrFlag struct {
374 ptr **string
375}
376
377func (f stringPtrFlag) String() string {
378 if f.ptr == nil || *f.ptr == nil {
379 return ""
380 }
381 return **f.ptr
382}
383
384func (f stringPtrFlag) Set(s string) error {
385 *f.ptr = &s
386 return nil
387}
388
389type boolPtrFlag struct {
390 ptr **bool
391}
392
393func (f boolPtrFlag) String() string {
394 if f.ptr == nil || *f.ptr == nil {
395 return "<nil>"
396 }
397 return strconv.FormatBool(**f.ptr)
398}
399
400func (f boolPtrFlag) Set(s string) error {
401 v, err := strconv.ParseBool(s)
402 if err != nil {
403 return err
404 }
405 *f.ptr = &v
406 return nil
407}
408
409type networkFlagSet struct {
410 *flag.FlagSet
411 Addr, Name, Nick, Username, Pass, Realname *string
412 Enabled *bool
413 ConnectCommands []string
414}
415
416func newNetworkFlagSet() *networkFlagSet {
417 fs := &networkFlagSet{FlagSet: newFlagSet()}
418 fs.Var(stringPtrFlag{&fs.Addr}, "addr", "")
419 fs.Var(stringPtrFlag{&fs.Name}, "name", "")
420 fs.Var(stringPtrFlag{&fs.Nick}, "nick", "")
421 fs.Var(stringPtrFlag{&fs.Username}, "username", "")
422 fs.Var(stringPtrFlag{&fs.Pass}, "pass", "")
423 fs.Var(stringPtrFlag{&fs.Realname}, "realname", "")
424 fs.Var(boolPtrFlag{&fs.Enabled}, "enabled", "")
425 fs.Var((*stringSliceFlag)(&fs.ConnectCommands), "connect-command", "")
426 return fs
427}
428
429func (fs *networkFlagSet) update(network *Network) error {
430 if fs.Addr != nil {
431 if addrParts := strings.SplitN(*fs.Addr, "://", 2); len(addrParts) == 2 {
432 scheme := addrParts[0]
433 switch scheme {
434 case "ircs", "irc+insecure", "unix":
435 default:
436 return fmt.Errorf("unknown scheme %q (supported schemes: ircs, irc+insecure, unix)", scheme)
437 }
438 }
439 network.Addr = *fs.Addr
440 }
441 if fs.Name != nil {
442 network.Name = *fs.Name
443 }
444 if fs.Nick != nil {
445 network.Nick = *fs.Nick
446 }
447 if fs.Username != nil {
448 network.Username = *fs.Username
449 }
450 if fs.Pass != nil {
451 network.Pass = *fs.Pass
452 }
453 if fs.Realname != nil {
454 network.Realname = *fs.Realname
455 }
456 if fs.Enabled != nil {
457 network.Enabled = *fs.Enabled
458 }
459 if fs.ConnectCommands != nil {
460 if len(fs.ConnectCommands) == 1 && fs.ConnectCommands[0] == "" {
461 network.ConnectCommands = nil
462 } else {
463 for _, command := range fs.ConnectCommands {
464 _, err := irc.ParseMessage(command)
465 if err != nil {
466 return fmt.Errorf("flag -connect-command must be a valid raw irc command string: %q: %v", command, err)
467 }
468 }
469 network.ConnectCommands = fs.ConnectCommands
470 }
471 }
472 return nil
473}
474
475func handleServiceNetworkCreate(dc *downstreamConn, params []string) error {
476 fs := newNetworkFlagSet()
477 if err := fs.Parse(params); err != nil {
478 return err
479 }
480 if fs.Addr == nil {
481 return fmt.Errorf("flag -addr is required")
482 }
483
484 record := &Network{
485 Addr: *fs.Addr,
486 Nick: dc.nick,
487 Enabled: true,
488 }
489 if err := fs.update(record); err != nil {
490 return err
491 }
492
493 network, err := dc.user.createNetwork(record)
494 if err != nil {
495 return fmt.Errorf("could not create network: %v", err)
496 }
497
498 sendServicePRIVMSG(dc, fmt.Sprintf("created network %q", network.GetName()))
499 return nil
500}
501
502func handleServiceNetworkStatus(dc *downstreamConn, params []string) error {
503 n := 0
504 dc.user.forEachNetwork(func(net *network) {
505 var statuses []string
506 var details string
507 if uc := net.conn; uc != nil {
508 if dc.nick != uc.nick {
509 statuses = append(statuses, "connected as "+uc.nick)
510 } else {
511 statuses = append(statuses, "connected")
512 }
513 details = fmt.Sprintf("%v channels", uc.channels.Len())
514 } else if !net.Enabled {
515 statuses = append(statuses, "disabled")
516 } else {
517 statuses = append(statuses, "disconnected")
518 if net.lastError != nil {
519 details = net.lastError.Error()
520 }
521 }
522
523 if net == dc.network {
524 statuses = append(statuses, "current")
525 }
526
527 name := net.GetName()
528 if name != net.Addr {
529 name = fmt.Sprintf("%v (%v)", name, net.Addr)
530 }
531
532 s := fmt.Sprintf("%v [%v]", name, strings.Join(statuses, ", "))
533 if details != "" {
534 s += ": " + details
535 }
536 sendServicePRIVMSG(dc, s)
537
538 n++
539 })
540
541 if n == 0 {
542 sendServicePRIVMSG(dc, `No network configured, add one with "network create".`)
543 }
544
545 return nil
546}
547
548func handleServiceNetworkUpdate(dc *downstreamConn, params []string) error {
549 if len(params) < 1 {
550 return fmt.Errorf("expected at least one argument")
551 }
552
553 fs := newNetworkFlagSet()
554 if err := fs.Parse(params[1:]); err != nil {
555 return err
556 }
557
558 net := dc.user.getNetwork(params[0])
559 if net == nil {
560 return fmt.Errorf("unknown network %q", params[0])
561 }
562
563 record := net.Network // copy network record because we'll mutate it
564 if err := fs.update(&record); err != nil {
565 return err
566 }
567
568 network, err := dc.user.updateNetwork(&record)
569 if err != nil {
570 return fmt.Errorf("could not update network: %v", err)
571 }
572
573 sendServicePRIVMSG(dc, fmt.Sprintf("updated network %q", network.GetName()))
574 return nil
575}
576
577func handleServiceNetworkDelete(dc *downstreamConn, params []string) error {
578 if len(params) != 1 {
579 return fmt.Errorf("expected exactly one argument")
580 }
581
582 net := dc.user.getNetwork(params[0])
583 if net == nil {
584 return fmt.Errorf("unknown network %q", params[0])
585 }
586
587 if err := dc.user.deleteNetwork(net.ID); err != nil {
588 return err
589 }
590
591 sendServicePRIVMSG(dc, fmt.Sprintf("deleted network %q", net.GetName()))
592 return nil
593}
594
595func handleServiceNetworkQuote(dc *downstreamConn, params []string) error {
596 if len(params) != 2 {
597 return fmt.Errorf("expected exactly two arguments")
598 }
599
600 net := dc.user.getNetwork(params[0])
601 if net == nil {
602 return fmt.Errorf("unknown network %q", params[0])
603 }
604
605 uc := net.conn
606 if uc == nil {
607 return fmt.Errorf("network %q is not currently connected", params[0])
608 }
609
610 m, err := irc.ParseMessage(params[1])
611 if err != nil {
612 return fmt.Errorf("failed to parse command %q: %v", params[1], err)
613 }
614 uc.SendMessage(m)
615
616 sendServicePRIVMSG(dc, fmt.Sprintf("sent command to %q", net.GetName()))
617 return nil
618}
619
620func handleServiceCertfpGenerate(dc *downstreamConn, params []string) error {
621 fs := newFlagSet()
622 keyType := fs.String("key-type", "rsa", "key type to generate (rsa, ecdsa, ed25519)")
623 bits := fs.Int("bits", 3072, "size of key to generate, meaningful only for RSA")
624
625 if err := fs.Parse(params); err != nil {
626 return err
627 }
628
629 if len(fs.Args()) != 1 {
630 return errors.New("exactly one argument is required")
631 }
632
633 net := dc.user.getNetwork(fs.Arg(0))
634 if net == nil {
635 return fmt.Errorf("unknown network %q", fs.Arg(0))
636 }
637
638 var (
639 privKey crypto.PrivateKey
640 pubKey crypto.PublicKey
641 )
642 switch *keyType {
643 case "rsa":
644 key, err := rsa.GenerateKey(rand.Reader, *bits)
645 if err != nil {
646 return err
647 }
648 privKey = key
649 pubKey = key.Public()
650 case "ecdsa":
651 key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
652 if err != nil {
653 return err
654 }
655 privKey = key
656 pubKey = key.Public()
657 case "ed25519":
658 var err error
659 pubKey, privKey, err = ed25519.GenerateKey(rand.Reader)
660 if err != nil {
661 return err
662 }
663 }
664
665 // Using PKCS#8 allows easier extension for new key types.
666 privKeyBytes, err := x509.MarshalPKCS8PrivateKey(privKey)
667 if err != nil {
668 return err
669 }
670
671 notBefore := time.Now()
672 // Lets make a fair assumption nobody will use the same cert for more than 20 years...
673 notAfter := notBefore.Add(24 * time.Hour * 365 * 20)
674 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
675 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
676 if err != nil {
677 return err
678 }
679 cert := &x509.Certificate{
680 SerialNumber: serialNumber,
681 Subject: pkix.Name{CommonName: "soju auto-generated certificate"},
682 NotBefore: notBefore,
683 NotAfter: notAfter,
684 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
685 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
686 }
687 derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, pubKey, privKey)
688 if err != nil {
689 return err
690 }
691
692 net.SASL.External.CertBlob = derBytes
693 net.SASL.External.PrivKeyBlob = privKeyBytes
694 net.SASL.Mechanism = "EXTERNAL"
695
696 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
697 return err
698 }
699
700 sendServicePRIVMSG(dc, "certificate generated")
701
702 sha1Sum := sha1.Sum(derBytes)
703 sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
704 sha256Sum := sha256.Sum256(derBytes)
705 sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
706 sha512Sum := sha512.Sum512(derBytes)
707 sendServicePRIVMSG(dc, "SHA-512 fingerprint: "+hex.EncodeToString(sha512Sum[:]))
708
709 return nil
710}
711
712func handleServiceCertfpFingerprints(dc *downstreamConn, params []string) error {
713 if len(params) != 1 {
714 return fmt.Errorf("expected exactly one argument")
715 }
716
717 net := dc.user.getNetwork(params[0])
718 if net == nil {
719 return fmt.Errorf("unknown network %q", params[0])
720 }
721
722 sha1Sum := sha1.Sum(net.SASL.External.CertBlob)
723 sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
724 sha256Sum := sha256.Sum256(net.SASL.External.CertBlob)
725 sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
726 sha512Sum := sha512.Sum512(net.SASL.External.CertBlob)
727 sendServicePRIVMSG(dc, "SHA-512 fingerprint: "+hex.EncodeToString(sha512Sum[:]))
728 return nil
729}
730
731func handleServiceSASLSetPlain(dc *downstreamConn, params []string) error {
732 if len(params) != 3 {
733 return fmt.Errorf("expected exactly 3 arguments")
734 }
735
736 net := dc.user.getNetwork(params[0])
737 if net == nil {
738 return fmt.Errorf("unknown network %q", params[0])
739 }
740
741 net.SASL.Plain.Username = params[1]
742 net.SASL.Plain.Password = params[2]
743 net.SASL.Mechanism = "PLAIN"
744
745 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
746 return err
747 }
748
749 sendServicePRIVMSG(dc, "credentials saved")
750 return nil
751}
752
753func handleServiceSASLReset(dc *downstreamConn, params []string) error {
754 if len(params) != 1 {
755 return fmt.Errorf("expected exactly one argument")
756 }
757
758 net := dc.user.getNetwork(params[0])
759 if net == nil {
760 return fmt.Errorf("unknown network %q", params[0])
761 }
762
763 net.SASL.Plain.Username = ""
764 net.SASL.Plain.Password = ""
765 net.SASL.External.CertBlob = nil
766 net.SASL.External.PrivKeyBlob = nil
767 net.SASL.Mechanism = ""
768
769 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
770 return err
771 }
772
773 sendServicePRIVMSG(dc, "credentials reset")
774 return nil
775}
776
777func handleUserCreate(dc *downstreamConn, params []string) error {
778 fs := newFlagSet()
779 username := fs.String("username", "", "")
780 password := fs.String("password", "", "")
781 realname := fs.String("realname", "", "")
782 admin := fs.Bool("admin", false, "")
783
784 if err := fs.Parse(params); err != nil {
785 return err
786 }
787 if *username == "" {
788 return fmt.Errorf("flag -username is required")
789 }
790 if *password == "" {
791 return fmt.Errorf("flag -password is required")
792 }
793
794 hashed, err := bcrypt.GenerateFromPassword([]byte(*password), bcrypt.DefaultCost)
795 if err != nil {
796 return fmt.Errorf("failed to hash password: %v", err)
797 }
798
799 user := &User{
800 Username: *username,
801 Password: string(hashed),
802 Realname: *realname,
803 Admin: *admin,
804 }
805 if _, err := dc.srv.createUser(user); err != nil {
806 return fmt.Errorf("could not create user: %v", err)
807 }
808
809 sendServicePRIVMSG(dc, fmt.Sprintf("created user %q", *username))
810 return nil
811}
812
813func handleUserUpdate(dc *downstreamConn, params []string) error {
814 var password, realname *string
815 fs := newFlagSet()
816 fs.Var(stringPtrFlag{&password}, "password", "")
817 fs.Var(stringPtrFlag{&realname}, "realname", "")
818
819 if err := fs.Parse(params); err != nil {
820 return err
821 }
822
823 // copy the user record because we'll mutate it
824 record := dc.user.User
825
826 if password != nil {
827 hashed, err := bcrypt.GenerateFromPassword([]byte(*password), bcrypt.DefaultCost)
828 if err != nil {
829 return fmt.Errorf("failed to hash password: %v", err)
830 }
831 record.Password = string(hashed)
832 }
833 if realname != nil {
834 record.Realname = *realname
835 }
836
837 if err := dc.user.updateUser(&record); err != nil {
838 return err
839 }
840
841 sendServicePRIVMSG(dc, fmt.Sprintf("updated user %q", dc.user.Username))
842 return nil
843}
844
845func handleUserDelete(dc *downstreamConn, params []string) error {
846 if len(params) != 1 {
847 return fmt.Errorf("expected exactly one argument")
848 }
849 username := params[0]
850
851 u := dc.srv.getUser(username)
852 if u == nil {
853 return fmt.Errorf("unknown username %q", username)
854 }
855
856 u.stop()
857
858 if err := dc.srv.db.DeleteUser(u.ID); err != nil {
859 return fmt.Errorf("failed to delete user: %v", err)
860 }
861
862 sendServicePRIVMSG(dc, fmt.Sprintf("deleted user %q", username))
863 return nil
864}
865
866func handleServiceChannelStatus(dc *downstreamConn, params []string) error {
867 var defaultNetworkName string
868 if dc.network != nil {
869 defaultNetworkName = dc.network.GetName()
870 }
871
872 fs := newFlagSet()
873 networkName := fs.String("network", defaultNetworkName, "")
874
875 if err := fs.Parse(params); err != nil {
876 return err
877 }
878
879 n := 0
880
881 sendNetwork := func(net *network) {
882 var channels []*Channel
883 for _, entry := range net.channels.innerMap {
884 channels = append(channels, entry.value.(*Channel))
885 }
886
887 sort.Slice(channels, func(i, j int) bool {
888 return strings.ReplaceAll(channels[i].Name, "#", "") <
889 strings.ReplaceAll(channels[j].Name, "#", "")
890 })
891
892 for _, ch := range channels {
893 var uch *upstreamChannel
894 if net.conn != nil {
895 uch = net.conn.channels.Value(ch.Name)
896 }
897
898 name := ch.Name
899 if *networkName == "" {
900 name += "/" + net.GetName()
901 }
902
903 var status string
904 if uch != nil {
905 status = "joined"
906 } else if net.conn != nil {
907 status = "parted"
908 } else {
909 status = "disconnected"
910 }
911
912 if ch.Detached {
913 status += ", detached"
914 }
915
916 s := fmt.Sprintf("%v [%v]", name, status)
917 sendServicePRIVMSG(dc, s)
918
919 n++
920 }
921 }
922
923 if *networkName == "" {
924 dc.user.forEachNetwork(sendNetwork)
925 } else {
926 net := dc.user.getNetwork(*networkName)
927 if net == nil {
928 return fmt.Errorf("unknown network %q", *networkName)
929 }
930 sendNetwork(net)
931 }
932
933 if n == 0 {
934 sendServicePRIVMSG(dc, "No channel configured.")
935 }
936
937 return nil
938}
939
940type channelFlagSet struct {
941 *flag.FlagSet
942 RelayDetached, ReattachOn, DetachAfter, DetachOn *string
943}
944
945func newChannelFlagSet() *channelFlagSet {
946 fs := &channelFlagSet{FlagSet: newFlagSet()}
947 fs.Var(stringPtrFlag{&fs.RelayDetached}, "relay-detached", "")
948 fs.Var(stringPtrFlag{&fs.ReattachOn}, "reattach-on", "")
949 fs.Var(stringPtrFlag{&fs.DetachAfter}, "detach-after", "")
950 fs.Var(stringPtrFlag{&fs.DetachOn}, "detach-on", "")
951 return fs
952}
953
954func (fs *channelFlagSet) update(channel *Channel) error {
955 if fs.RelayDetached != nil {
956 filter, err := parseFilter(*fs.RelayDetached)
957 if err != nil {
958 return err
959 }
960 channel.RelayDetached = filter
961 }
962 if fs.ReattachOn != nil {
963 filter, err := parseFilter(*fs.ReattachOn)
964 if err != nil {
965 return err
966 }
967 channel.ReattachOn = filter
968 }
969 if fs.DetachAfter != nil {
970 dur, err := time.ParseDuration(*fs.DetachAfter)
971 if err != nil || dur < 0 {
972 return fmt.Errorf("unknown duration for -detach-after %q (duration format: 0, 300s, 22h30m, ...)", *fs.DetachAfter)
973 }
974 channel.DetachAfter = dur
975 }
976 if fs.DetachOn != nil {
977 filter, err := parseFilter(*fs.DetachOn)
978 if err != nil {
979 return err
980 }
981 channel.DetachOn = filter
982 }
983 return nil
984}
985
986func handleServiceChannelUpdate(dc *downstreamConn, params []string) error {
987 if len(params) < 1 {
988 return fmt.Errorf("expected at least one argument")
989 }
990 name := params[0]
991
992 fs := newChannelFlagSet()
993 if err := fs.Parse(params[1:]); err != nil {
994 return err
995 }
996
997 uc, upstreamName, err := dc.unmarshalEntity(name)
998 if err != nil {
999 return fmt.Errorf("unknown channel %q", name)
1000 }
1001
1002 ch := uc.network.channels.Value(upstreamName)
1003 if ch == nil {
1004 return fmt.Errorf("unknown channel %q", name)
1005 }
1006
1007 if err := fs.update(ch); err != nil {
1008 return err
1009 }
1010
1011 uc.updateChannelAutoDetach(upstreamName)
1012
1013 if err := dc.srv.db.StoreChannel(uc.network.ID, ch); err != nil {
1014 return fmt.Errorf("failed to update channel: %v", err)
1015 }
1016
1017 sendServicePRIVMSG(dc, fmt.Sprintf("updated channel %q", name))
1018 return nil
1019}
1020
1021func handleServiceServerStatus(dc *downstreamConn, params []string) error {
1022 stats := dc.user.srv.Stats()
1023 sendServicePRIVMSG(dc, fmt.Sprintf("%v users, %v downstreams", stats.Users, stats.Downstreams))
1024 return nil
1025}
Note: See TracBrowser for help on using the repository browser.