source: code/trunk/service.go@ 543

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

Allow networks to be disabled

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