source: code/trunk/service.go@ 362

Last change on this file since 362 was 358, checked in by contact, 5 years ago

Add unix as supported upstream URL scheme to service

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