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
RevLine 
[117]1package soju
2
3import (
[307]4 "crypto"
5 "crypto/ecdsa"
6 "crypto/ed25519"
7 "crypto/elliptic"
8 "crypto/rand"
9 "crypto/rsa"
10 "crypto/sha1"
11 "crypto/sha256"
[575]12 "crypto/sha512"
[307]13 "crypto/x509"
14 "crypto/x509/pkix"
15 "encoding/hex"
16 "errors"
[120]17 "flag"
[117]18 "fmt"
[120]19 "io/ioutil"
[307]20 "math/big"
[339]21 "sort"
[542]22 "strconv"
[117]23 "strings"
[307]24 "time"
[566]25 "unicode"
[117]26
[252]27 "golang.org/x/crypto/bcrypt"
[117]28 "gopkg.in/irc.v3"
29)
30
31const serviceNick = "BouncerServ"
[478]32const serviceNickCM = "bouncerserv"
[343]33const serviceRealname = "soju bouncer service"
[117]34
[220]35var servicePrefix = &irc.Prefix{
36 Name: serviceNick,
37 User: serviceNick,
38 Host: serviceNick,
39}
40
[150]41type serviceCommandSet map[string]*serviceCommand
42
[117]43type serviceCommand struct {
[150]44 usage string
45 desc string
46 handle func(dc *downstreamConn, params []string) error
47 children serviceCommandSet
[328]48 admin bool
[117]49}
50
[218]51func sendServiceNOTICE(dc *downstreamConn, text string) {
52 dc.SendMessage(&irc.Message{
[220]53 Prefix: servicePrefix,
[218]54 Command: "NOTICE",
55 Params: []string{dc.nick, text},
56 })
57}
58
[117]59func sendServicePRIVMSG(dc *downstreamConn, text string) {
60 dc.SendMessage(&irc.Message{
[220]61 Prefix: servicePrefix,
[117]62 Command: "PRIVMSG",
63 Params: []string{dc.nick, text},
64 })
65}
66
[566]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
[117]120func handleServicePRIVMSG(dc *downstreamConn, text string) {
[566]121 words, err := splitWords(text)
[117]122 if err != nil {
[566]123 sendServicePRIVMSG(dc, fmt.Sprintf(`error: failed to parse command: %v`, err))
[117]124 return
125 }
126
[150]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))
[117]130 return
131 }
[328]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 }
[117]136
[334]137 if cmd.handle == nil {
138 if len(cmd.children) > 0 {
139 var l []string
[335]140 appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
[334]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
[117]151 if err := cmd.handle(dc, params); err != nil {
152 sendServicePRIVMSG(dc, fmt.Sprintf("error: %v", err))
153 }
154}
155
[150]156func (cmds serviceCommandSet) Get(params []string) (*serviceCommand, []string, error) {
157 if len(params) == 0 {
158 return nil, nil, fmt.Errorf("no command specified")
159 }
[117]160
[150]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
[339]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
[150]195var serviceCommands serviceCommandSet
196
[117]197func init() {
[150]198 serviceCommands = serviceCommandSet{
[117]199 "help": {
200 usage: "[command]",
201 desc: "print help message",
202 handle: handleServiceHelp,
203 },
[150]204 "network": {
205 children: serviceCommandSet{
206 "create": {
[542]207 usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-enabled enabled] [-connect-command command]...",
[150]208 desc: "add a new network",
[325]209 handle: handleServiceNetworkCreate,
[150]210 },
[151]211 "status": {
212 desc: "show a list of saved networks and their current status",
213 handle: handleServiceNetworkStatus,
214 },
[313]215 "update": {
[542]216 usage: "<name> [-addr addr] [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-enabled enabled] [-connect-command command]...",
[315]217 desc: "update a network",
[313]218 handle: handleServiceNetworkUpdate,
219 },
[202]220 "delete": {
221 usage: "<name>",
222 desc: "delete a network",
223 handle: handleServiceNetworkDelete,
224 },
[150]225 },
[120]226 },
[307]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 },
[363]241 "sasl": {
242 children: serviceCommandSet{
243 "set-plain": {
244 usage: "<network name> <username> <password>",
245 desc: "set SASL PLAIN credentials",
246 handle: handleServiceSASLSetPlain,
247 },
[364]248 "reset": {
249 usage: "<network name>",
250 desc: "disable SASL authentication and remove stored credentials",
251 handle: handleServiceSASLReset,
252 },
[363]253 },
254 },
[329]255 "user": {
256 children: serviceCommandSet{
257 "create": {
[568]258 usage: "-username <username> -password <password> [-realname <realname>] [-admin]",
[329]259 desc: "create a new soju user",
260 handle: handleUserCreate,
261 admin: true,
262 },
[568]263 "update": {
[570]264 usage: "[-password <password>] [-realname <realname>]",
[568]265 desc: "update the current user",
266 handle: handleUserUpdate,
267 },
[379]268 "delete": {
269 usage: "<username>",
270 desc: "delete a user",
271 handle: handleUserDelete,
272 admin: true,
273 },
[329]274 },
275 },
[436]276 "channel": {
277 children: serviceCommandSet{
[539]278 "status": {
279 usage: "[-network name]",
280 desc: "show a list of saved channels and their current status",
281 handle: handleServiceChannelStatus,
282 },
[436]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 },
[117]290 }
291}
292
[328]293func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, admin bool, l *[]string) {
[339]294 for _, name := range cmds.Names() {
295 cmd := cmds[name]
[328]296 if cmd.admin && !admin {
297 continue
298 }
[150]299 words := append(prefix, name)
300 if len(cmd.children) == 0 {
301 s := strings.Join(words, " ")
302 *l = append(*l, s)
303 } else {
[328]304 appendServiceCommandSetHelp(cmd.children, words, admin, l)
[150]305 }
306 }
307}
308
[117]309func handleServiceHelp(dc *downstreamConn, params []string) error {
310 if len(params) > 0 {
[150]311 cmd, rest, err := serviceCommands.Get(params)
312 if err != nil {
313 return err
[117]314 }
[150]315 words := params[:len(params)-len(rest)]
[117]316
[150]317 if len(cmd.children) > 0 {
318 var l []string
[328]319 appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
[150]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)
[117]329 }
330 } else {
331 var l []string
[328]332 appendServiceCommandSetHelp(serviceCommands, nil, dc.user.Admin, &l)
[117]333 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
334 }
335 return nil
336}
[120]337
[202]338func newFlagSet() *flag.FlagSet {
[120]339 fs := flag.NewFlagSet("", flag.ContinueOnError)
340 fs.SetOutput(ioutil.Discard)
[202]341 return fs
342}
343
[313]344type stringSliceFlag []string
[263]345
[313]346func (v *stringSliceFlag) String() string {
[263]347 return fmt.Sprint([]string(*v))
348}
349
[313]350func (v *stringSliceFlag) Set(s string) error {
[263]351 *v = append(*v, s)
352 return nil
353}
354
[313]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 {
[333]363 if f.ptr == nil || *f.ptr == nil {
[313]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
[542]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
[313]394type networkFlagSet struct {
395 *flag.FlagSet
396 Addr, Name, Nick, Username, Pass, Realname *string
[542]397 Enabled *bool
[315]398 ConnectCommands []string
[313]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", "")
[542]409 fs.Var(boolPtrFlag{&fs.Enabled}, "enabled", "")
[313]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 {
[358]419 case "ircs", "irc+insecure", "unix":
[313]420 default:
[358]421 return fmt.Errorf("unknown scheme %q (supported schemes: ircs, irc+insecure, unix)", scheme)
[313]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 }
[542]441 if fs.Enabled != nil {
442 network.Enabled = *fs.Enabled
443 }
[313]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
[325]460func handleServiceNetworkCreate(dc *downstreamConn, params []string) error {
[313]461 fs := newNetworkFlagSet()
[120]462 if err := fs.Parse(params); err != nil {
463 return err
464 }
[313]465 if fs.Addr == nil {
[150]466 return fmt.Errorf("flag -addr is required")
[120]467 }
468
[313]469 record := &Network{
[542]470 Addr: *fs.Addr,
471 Nick: dc.nick,
472 Enabled: true,
[269]473 }
[313]474 if err := fs.update(record); err != nil {
475 return err
[263]476 }
477
[313]478 network, err := dc.user.createNetwork(record)
[120]479 if err != nil {
480 return fmt.Errorf("could not create network: %v", err)
481 }
482
[202]483 sendServicePRIVMSG(dc, fmt.Sprintf("created network %q", network.GetName()))
[120]484 return nil
485}
[151]486
487func handleServiceNetworkStatus(dc *downstreamConn, params []string) error {
[546]488 n := 0
[151]489 dc.user.forEachNetwork(func(net *network) {
490 var statuses []string
491 var details string
[279]492 if uc := net.conn; uc != nil {
[271]493 if dc.nick != uc.nick {
494 statuses = append(statuses, "connected as "+uc.nick)
495 } else {
496 statuses = append(statuses, "connected")
497 }
[478]498 details = fmt.Sprintf("%v channels", uc.channels.Len())
[542]499 } else if !net.Enabled {
500 statuses = append(statuses, "disabled")
[151]501 } else {
502 statuses = append(statuses, "disconnected")
[219]503 if net.lastError != nil {
504 details = net.lastError.Error()
505 }
[151]506 }
507
508 if net == dc.network {
509 statuses = append(statuses, "current")
510 }
511
[224]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, ", "))
[151]518 if details != "" {
519 s += ": " + details
520 }
521 sendServicePRIVMSG(dc, s)
[546]522
523 n++
[151]524 })
[546]525
526 if n == 0 {
527 sendServicePRIVMSG(dc, `No network configured, add one with "network create".`)
528 }
529
[151]530 return nil
531}
[202]532
[313]533func handleServiceNetworkUpdate(dc *downstreamConn, params []string) error {
534 if len(params) < 1 {
[436]535 return fmt.Errorf("expected at least one argument")
[313]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
[202]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}
[252]579
[325]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
[421]656 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
[325]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[:]))
[575]666 sha512Sum := sha512.Sum512(derBytes)
667 sendServicePRIVMSG(dc, "SHA-512 fingerprint: "+hex.EncodeToString(sha512Sum[:]))
[325]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[:]))
[575]686 sha512Sum := sha512.Sum512(net.SASL.External.CertBlob)
687 sendServicePRIVMSG(dc, "SHA-512 fingerprint: "+hex.EncodeToString(sha512Sum[:]))
[325]688 return nil
689}
690
[364]691func handleServiceSASLSetPlain(dc *downstreamConn, params []string) error {
692 if len(params) != 3 {
693 return fmt.Errorf("expected exactly 3 arguments")
[325]694 }
695
696 net := dc.user.getNetwork(params[0])
697 if net == nil {
698 return fmt.Errorf("unknown network %q", params[0])
699 }
700
[364]701 net.SASL.Plain.Username = params[1]
702 net.SASL.Plain.Password = params[2]
703 net.SASL.Mechanism = "PLAIN"
[325]704
[421]705 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
[325]706 return err
707 }
708
[364]709 sendServicePRIVMSG(dc, "credentials saved")
[325]710 return nil
711}
712
[364]713func handleServiceSASLReset(dc *downstreamConn, params []string) error {
714 if len(params) != 1 {
715 return fmt.Errorf("expected exactly one argument")
[363]716 }
717
718 net := dc.user.getNetwork(params[0])
719 if net == nil {
720 return fmt.Errorf("unknown network %q", params[0])
721 }
722
[364]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 = ""
[363]728
[421]729 if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
[363]730 return err
731 }
732
[364]733 sendServicePRIVMSG(dc, "credentials reset")
[363]734 return nil
735}
736
[329]737func handleUserCreate(dc *downstreamConn, params []string) error {
738 fs := newFlagSet()
739 username := fs.String("username", "", "")
740 password := fs.String("password", "", "")
[568]741 realname := fs.String("realname", "", "")
[329]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),
[568]762 Realname: *realname,
[329]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}
[379]772
[568]773func handleUserUpdate(dc *downstreamConn, params []string) error {
[570]774 var password, realname *string
[568]775 fs := newFlagSet()
[570]776 fs.Var(stringPtrFlag{&password}, "password", "")
[569]777 fs.Var(stringPtrFlag{&realname}, "realname", "")
[568]778
779 if err := fs.Parse(params); err != nil {
780 return err
781 }
782
[572]783 // copy the user record because we'll mutate it
784 record := dc.user.User
785
[570]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 }
[572]791 record.Password = string(hashed)
[570]792 }
[569]793 if realname != nil {
[572]794 record.Realname = *realname
[568]795 }
796
[572]797 if err := dc.user.updateUser(&record); err != nil {
798 return err
799 }
800
[568]801 sendServicePRIVMSG(dc, fmt.Sprintf("updated user %q", dc.user.Username))
802 return nil
803}
804
[379]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
[508]818 if err := dc.srv.db.DeleteUser(u.ID); err != nil {
[379]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}
[436]825
[539]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
[546]839 n := 0
840
[539]841 sendNetwork := func(net *network) {
[573]842 var channels []*Channel
[539]843 for _, entry := range net.channels.innerMap {
[573]844 channels = append(channels, entry.value.(*Channel))
845 }
[539]846
[573]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 {
[539]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)
[546]878
879 n++
[539]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
[546]893 if n == 0 {
894 sendServicePRIVMSG(dc, "No channel configured.")
895 }
896
[539]897 return nil
898}
899
[436]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
[478]962 ch := uc.network.channels.Value(upstreamName)
963 if ch == nil {
[436]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.