source: code/trunk/downstream.go@ 765

Last change on this file since 765 was 761, checked in by contact, 3 years ago

Handle upstream multi-line SASL

References: https://todo.sr.ht/~emersion/soju/173

File size: 74.7 KB
Line 
1package soju
2
3import (
4 "bytes"
5 "context"
6 "crypto/tls"
7 "encoding/base64"
8 "errors"
9 "fmt"
10 "io"
11 "net"
12 "strconv"
13 "strings"
14 "time"
15
16 "github.com/emersion/go-sasl"
17 "golang.org/x/crypto/bcrypt"
18 "gopkg.in/irc.v3"
19)
20
21type ircError struct {
22 Message *irc.Message
23}
24
25func (err ircError) Error() string {
26 return err.Message.String()
27}
28
29func newUnknownCommandError(cmd string) ircError {
30 return ircError{&irc.Message{
31 Command: irc.ERR_UNKNOWNCOMMAND,
32 Params: []string{
33 "*",
34 cmd,
35 "Unknown command",
36 },
37 }}
38}
39
40func newNeedMoreParamsError(cmd string) ircError {
41 return ircError{&irc.Message{
42 Command: irc.ERR_NEEDMOREPARAMS,
43 Params: []string{
44 "*",
45 cmd,
46 "Not enough parameters",
47 },
48 }}
49}
50
51func newChatHistoryError(subcommand string, target string) ircError {
52 return ircError{&irc.Message{
53 Command: "FAIL",
54 Params: []string{"CHATHISTORY", "MESSAGE_ERROR", subcommand, target, "Messages could not be retrieved"},
55 }}
56}
57
58// authError is an authentication error.
59type authError struct {
60 // Internal error cause. This will not be revealed to the user.
61 err error
62 // Error cause which can safely be sent to the user without compromising
63 // security.
64 reason string
65}
66
67func (err *authError) Error() string {
68 return err.err.Error()
69}
70
71func (err *authError) Unwrap() error {
72 return err.err
73}
74
75// authErrorReason returns the user-friendly reason of an authentication
76// failure.
77func authErrorReason(err error) string {
78 if authErr, ok := err.(*authError); ok {
79 return authErr.reason
80 } else {
81 return "Authentication failed"
82 }
83}
84
85func newInvalidUsernameOrPasswordError(err error) error {
86 return &authError{
87 err: err,
88 reason: "Invalid username or password",
89 }
90}
91
92func parseBouncerNetID(subcommand, s string) (int64, error) {
93 id, err := strconv.ParseInt(s, 10, 64)
94 if err != nil {
95 return 0, ircError{&irc.Message{
96 Command: "FAIL",
97 Params: []string{"BOUNCER", "INVALID_NETID", subcommand, s, "Invalid network ID"},
98 }}
99 }
100 return id, nil
101}
102
103func fillNetworkAddrAttrs(attrs irc.Tags, network *Network) {
104 u, err := network.URL()
105 if err != nil {
106 return
107 }
108
109 hasHostPort := true
110 switch u.Scheme {
111 case "ircs":
112 attrs["tls"] = irc.TagValue("1")
113 case "irc+insecure":
114 attrs["tls"] = irc.TagValue("0")
115 default: // e.g. unix://
116 hasHostPort = false
117 }
118 if host, port, err := net.SplitHostPort(u.Host); err == nil && hasHostPort {
119 attrs["host"] = irc.TagValue(host)
120 attrs["port"] = irc.TagValue(port)
121 } else if hasHostPort {
122 attrs["host"] = irc.TagValue(u.Host)
123 }
124}
125
126func getNetworkAttrs(network *network) irc.Tags {
127 state := "disconnected"
128 if uc := network.conn; uc != nil {
129 state = "connected"
130 }
131
132 attrs := irc.Tags{
133 "name": irc.TagValue(network.GetName()),
134 "state": irc.TagValue(state),
135 "nickname": irc.TagValue(GetNick(&network.user.User, &network.Network)),
136 }
137
138 if network.Username != "" {
139 attrs["username"] = irc.TagValue(network.Username)
140 }
141 if realname := GetRealname(&network.user.User, &network.Network); realname != "" {
142 attrs["realname"] = irc.TagValue(realname)
143 }
144
145 fillNetworkAddrAttrs(attrs, &network.Network)
146
147 return attrs
148}
149
150func networkAddrFromAttrs(attrs irc.Tags) string {
151 host, ok := attrs.GetTag("host")
152 if !ok {
153 return ""
154 }
155
156 addr := host
157 if port, ok := attrs.GetTag("port"); ok {
158 addr += ":" + port
159 }
160
161 if tlsStr, ok := attrs.GetTag("tls"); ok && tlsStr == "0" {
162 addr = "irc+insecure://" + tlsStr
163 }
164
165 return addr
166}
167
168func updateNetworkAttrs(record *Network, attrs irc.Tags, subcommand string) error {
169 addrAttrs := irc.Tags{}
170 fillNetworkAddrAttrs(addrAttrs, record)
171
172 updateAddr := false
173 for k, v := range attrs {
174 s := string(v)
175 switch k {
176 case "host", "port", "tls":
177 updateAddr = true
178 addrAttrs[k] = v
179 case "name":
180 record.Name = s
181 case "nickname":
182 record.Nick = s
183 case "username":
184 record.Username = s
185 case "realname":
186 record.Realname = s
187 case "pass":
188 record.Pass = s
189 default:
190 return ircError{&irc.Message{
191 Command: "FAIL",
192 Params: []string{"BOUNCER", "UNKNOWN_ATTRIBUTE", subcommand, k, "Unknown attribute"},
193 }}
194 }
195 }
196
197 if updateAddr {
198 record.Addr = networkAddrFromAttrs(addrAttrs)
199 if record.Addr == "" {
200 return ircError{&irc.Message{
201 Command: "FAIL",
202 Params: []string{"BOUNCER", "NEED_ATTRIBUTE", subcommand, "host", "Missing required host attribute"},
203 }}
204 }
205 }
206
207 return nil
208}
209
210// ' ' and ':' break the IRC message wire format, '@' and '!' break prefixes,
211// '*' and '?' break masks, '$' breaks server masks in PRIVMSG/NOTICE,
212// "*" is the reserved nickname for registration, ',' breaks lists
213const illegalNickChars = " :@!*?$,"
214
215// permanentDownstreamCaps is the list of always-supported downstream
216// capabilities.
217var permanentDownstreamCaps = map[string]string{
218 "batch": "",
219 "cap-notify": "",
220 "echo-message": "",
221 "invite-notify": "",
222 "message-tags": "",
223 "server-time": "",
224 "setname": "",
225
226 "soju.im/bouncer-networks": "",
227 "soju.im/bouncer-networks-notify": "",
228}
229
230// needAllDownstreamCaps is the list of downstream capabilities that
231// require support from all upstreams to be enabled
232var needAllDownstreamCaps = map[string]string{
233 "account-notify": "",
234 "account-tag": "",
235 "away-notify": "",
236 "extended-join": "",
237 "multi-prefix": "",
238
239 "draft/extended-monitor": "",
240}
241
242// passthroughIsupport is the set of ISUPPORT tokens that are directly passed
243// through from the upstream server to downstream clients.
244//
245// This is only effective in single-upstream mode.
246var passthroughIsupport = map[string]bool{
247 "AWAYLEN": true,
248 "BOT": true,
249 "CHANLIMIT": true,
250 "CHANMODES": true,
251 "CHANNELLEN": true,
252 "CHANTYPES": true,
253 "CLIENTTAGDENY": true,
254 "ELIST": true,
255 "EXCEPTS": true,
256 "EXTBAN": true,
257 "HOSTLEN": true,
258 "INVEX": true,
259 "KICKLEN": true,
260 "MAXLIST": true,
261 "MAXTARGETS": true,
262 "MODES": true,
263 "MONITOR": true,
264 "NAMELEN": true,
265 "NETWORK": true,
266 "NICKLEN": true,
267 "PREFIX": true,
268 "SAFELIST": true,
269 "TARGMAX": true,
270 "TOPICLEN": true,
271 "USERLEN": true,
272 "UTF8ONLY": true,
273 "WHOX": true,
274}
275
276type downstreamSASL struct {
277 server sasl.Server
278 plainUsername, plainPassword string
279 pendingResp bytes.Buffer
280}
281
282type downstreamConn struct {
283 conn
284
285 id uint64
286
287 registered bool
288 user *user
289 nick string
290 nickCM string
291 rawUsername string
292 networkName string
293 clientName string
294 realname string
295 hostname string
296 account string // RPL_LOGGEDIN/OUT state
297 password string // empty after authentication
298 network *network // can be nil
299 isMultiUpstream bool
300
301 negotiatingCaps bool
302 capVersion int
303 supportedCaps map[string]string
304 caps map[string]bool
305 sasl *downstreamSASL
306
307 lastBatchRef uint64
308
309 monitored casemapMap
310}
311
312func newDownstreamConn(srv *Server, ic ircConn, id uint64) *downstreamConn {
313 remoteAddr := ic.RemoteAddr().String()
314 logger := &prefixLogger{srv.Logger, fmt.Sprintf("downstream %q: ", remoteAddr)}
315 options := connOptions{Logger: logger}
316 dc := &downstreamConn{
317 conn: *newConn(srv, ic, &options),
318 id: id,
319 nick: "*",
320 nickCM: "*",
321 supportedCaps: make(map[string]string),
322 caps: make(map[string]bool),
323 monitored: newCasemapMap(0),
324 }
325 dc.hostname = remoteAddr
326 if host, _, err := net.SplitHostPort(dc.hostname); err == nil {
327 dc.hostname = host
328 }
329 for k, v := range permanentDownstreamCaps {
330 dc.supportedCaps[k] = v
331 }
332 dc.supportedCaps["sasl"] = "PLAIN"
333 // TODO: this is racy, we should only enable chathistory after
334 // authentication and then check that user.msgStore implements
335 // chatHistoryMessageStore
336 if srv.Config().LogPath != "" {
337 dc.supportedCaps["draft/chathistory"] = ""
338 }
339 return dc
340}
341
342func (dc *downstreamConn) prefix() *irc.Prefix {
343 return &irc.Prefix{
344 Name: dc.nick,
345 User: dc.user.Username,
346 Host: dc.hostname,
347 }
348}
349
350func (dc *downstreamConn) forEachNetwork(f func(*network)) {
351 if dc.network != nil {
352 f(dc.network)
353 } else if dc.isMultiUpstream {
354 dc.user.forEachNetwork(f)
355 }
356}
357
358func (dc *downstreamConn) forEachUpstream(f func(*upstreamConn)) {
359 if dc.network == nil && !dc.isMultiUpstream {
360 return
361 }
362 dc.user.forEachUpstream(func(uc *upstreamConn) {
363 if dc.network != nil && uc.network != dc.network {
364 return
365 }
366 f(uc)
367 })
368}
369
370// upstream returns the upstream connection, if any. If there are zero or if
371// there are multiple upstream connections, it returns nil.
372func (dc *downstreamConn) upstream() *upstreamConn {
373 if dc.network == nil {
374 return nil
375 }
376 return dc.network.conn
377}
378
379func isOurNick(net *network, nick string) bool {
380 // TODO: this doesn't account for nick changes
381 if net.conn != nil {
382 return net.casemap(nick) == net.conn.nickCM
383 }
384 // We're not currently connected to the upstream connection, so we don't
385 // know whether this name is our nickname. Best-effort: use the network's
386 // configured nickname and hope it was the one being used when we were
387 // connected.
388 return net.casemap(nick) == net.casemap(GetNick(&net.user.User, &net.Network))
389}
390
391// marshalEntity converts an upstream entity name (ie. channel or nick) into a
392// downstream entity name.
393//
394// This involves adding a "/<network>" suffix if the entity isn't the current
395// user.
396func (dc *downstreamConn) marshalEntity(net *network, name string) string {
397 if isOurNick(net, name) {
398 return dc.nick
399 }
400 name = partialCasemap(net.casemap, name)
401 if dc.network != nil {
402 if dc.network != net {
403 panic("soju: tried to marshal an entity for another network")
404 }
405 return name
406 }
407 return name + "/" + net.GetName()
408}
409
410func (dc *downstreamConn) marshalUserPrefix(net *network, prefix *irc.Prefix) *irc.Prefix {
411 if isOurNick(net, prefix.Name) {
412 return dc.prefix()
413 }
414 prefix.Name = partialCasemap(net.casemap, prefix.Name)
415 if dc.network != nil {
416 if dc.network != net {
417 panic("soju: tried to marshal a user prefix for another network")
418 }
419 return prefix
420 }
421 return &irc.Prefix{
422 Name: prefix.Name + "/" + net.GetName(),
423 User: prefix.User,
424 Host: prefix.Host,
425 }
426}
427
428// unmarshalEntityNetwork converts a downstream entity name (ie. channel or
429// nick) into an upstream entity name.
430//
431// This involves removing the "/<network>" suffix.
432func (dc *downstreamConn) unmarshalEntityNetwork(name string) (*network, string, error) {
433 if dc.network != nil {
434 return dc.network, name, nil
435 }
436 if !dc.isMultiUpstream {
437 return nil, "", ircError{&irc.Message{
438 Command: irc.ERR_NOSUCHCHANNEL,
439 Params: []string{dc.nick, name, "Cannot interact with channels and users on the bouncer connection. Did you mean to use a specific network?"},
440 }}
441 }
442
443 var net *network
444 if i := strings.LastIndexByte(name, '/'); i >= 0 {
445 network := name[i+1:]
446 name = name[:i]
447
448 for _, n := range dc.user.networks {
449 if network == n.GetName() {
450 net = n
451 break
452 }
453 }
454 }
455
456 if net == nil {
457 return nil, "", ircError{&irc.Message{
458 Command: irc.ERR_NOSUCHCHANNEL,
459 Params: []string{dc.nick, name, "Missing network suffix in name"},
460 }}
461 }
462
463 return net, name, nil
464}
465
466// unmarshalEntity is the same as unmarshalEntityNetwork, but returns the
467// upstream connection and fails if the upstream is disconnected.
468func (dc *downstreamConn) unmarshalEntity(name string) (*upstreamConn, string, error) {
469 net, name, err := dc.unmarshalEntityNetwork(name)
470 if err != nil {
471 return nil, "", err
472 }
473
474 if net.conn == nil {
475 return nil, "", ircError{&irc.Message{
476 Command: irc.ERR_NOSUCHCHANNEL,
477 Params: []string{dc.nick, name, "Disconnected from upstream network"},
478 }}
479 }
480
481 return net.conn, name, nil
482}
483
484func (dc *downstreamConn) unmarshalText(uc *upstreamConn, text string) string {
485 if dc.upstream() != nil {
486 return text
487 }
488 // TODO: smarter parsing that ignores URLs
489 return strings.ReplaceAll(text, "/"+uc.network.GetName(), "")
490}
491
492func (dc *downstreamConn) ReadMessage() (*irc.Message, error) {
493 msg, err := dc.conn.ReadMessage()
494 if err != nil {
495 return nil, err
496 }
497 dc.srv.metrics.downstreamInMessagesTotal.Inc()
498 return msg, nil
499}
500
501func (dc *downstreamConn) readMessages(ch chan<- event) error {
502 for {
503 msg, err := dc.ReadMessage()
504 if errors.Is(err, io.EOF) {
505 break
506 } else if err != nil {
507 return fmt.Errorf("failed to read IRC command: %v", err)
508 }
509
510 ch <- eventDownstreamMessage{msg, dc}
511 }
512
513 return nil
514}
515
516// SendMessage sends an outgoing message.
517//
518// This can only called from the user goroutine.
519func (dc *downstreamConn) SendMessage(msg *irc.Message) {
520 if !dc.caps["message-tags"] {
521 if msg.Command == "TAGMSG" {
522 return
523 }
524 msg = msg.Copy()
525 for name := range msg.Tags {
526 supported := false
527 switch name {
528 case "time":
529 supported = dc.caps["server-time"]
530 case "account":
531 supported = dc.caps["account"]
532 }
533 if !supported {
534 delete(msg.Tags, name)
535 }
536 }
537 }
538 if !dc.caps["batch"] && msg.Tags["batch"] != "" {
539 msg = msg.Copy()
540 delete(msg.Tags, "batch")
541 }
542 if msg.Command == "JOIN" && !dc.caps["extended-join"] {
543 msg.Params = msg.Params[:1]
544 }
545 if msg.Command == "SETNAME" && !dc.caps["setname"] {
546 return
547 }
548 if msg.Command == "AWAY" && !dc.caps["away-notify"] {
549 return
550 }
551 if msg.Command == "ACCOUNT" && !dc.caps["account-notify"] {
552 return
553 }
554
555 dc.srv.metrics.downstreamOutMessagesTotal.Inc()
556 dc.conn.SendMessage(context.TODO(), msg)
557}
558
559func (dc *downstreamConn) SendBatch(typ string, params []string, tags irc.Tags, f func(batchRef irc.TagValue)) {
560 dc.lastBatchRef++
561 ref := fmt.Sprintf("%v", dc.lastBatchRef)
562
563 if dc.caps["batch"] {
564 dc.SendMessage(&irc.Message{
565 Tags: tags,
566 Prefix: dc.srv.prefix(),
567 Command: "BATCH",
568 Params: append([]string{"+" + ref, typ}, params...),
569 })
570 }
571
572 f(irc.TagValue(ref))
573
574 if dc.caps["batch"] {
575 dc.SendMessage(&irc.Message{
576 Prefix: dc.srv.prefix(),
577 Command: "BATCH",
578 Params: []string{"-" + ref},
579 })
580 }
581}
582
583// sendMessageWithID sends an outgoing message with the specified internal ID.
584func (dc *downstreamConn) sendMessageWithID(msg *irc.Message, id string) {
585 dc.SendMessage(msg)
586
587 if id == "" || !dc.messageSupportsBacklog(msg) {
588 return
589 }
590
591 dc.sendPing(id)
592}
593
594// advanceMessageWithID advances history to the specified message ID without
595// sending a message. This is useful e.g. for self-messages when echo-message
596// isn't enabled.
597func (dc *downstreamConn) advanceMessageWithID(msg *irc.Message, id string) {
598 if id == "" || !dc.messageSupportsBacklog(msg) {
599 return
600 }
601
602 dc.sendPing(id)
603}
604
605// ackMsgID acknowledges that a message has been received.
606func (dc *downstreamConn) ackMsgID(id string) {
607 netID, entity, err := parseMsgID(id, nil)
608 if err != nil {
609 dc.logger.Printf("failed to ACK message ID %q: %v", id, err)
610 return
611 }
612
613 network := dc.user.getNetworkByID(netID)
614 if network == nil {
615 return
616 }
617
618 network.delivered.StoreID(entity, dc.clientName, id)
619}
620
621func (dc *downstreamConn) sendPing(msgID string) {
622 token := "soju-msgid-" + msgID
623 dc.SendMessage(&irc.Message{
624 Command: "PING",
625 Params: []string{token},
626 })
627}
628
629func (dc *downstreamConn) handlePong(token string) {
630 if !strings.HasPrefix(token, "soju-msgid-") {
631 dc.logger.Printf("received unrecognized PONG token %q", token)
632 return
633 }
634 msgID := strings.TrimPrefix(token, "soju-msgid-")
635 dc.ackMsgID(msgID)
636}
637
638// marshalMessage re-formats a message coming from an upstream connection so
639// that it's suitable for being sent on this downstream connection. Only
640// messages that may appear in logs are supported, except MODE messages which
641// may only appear in single-upstream mode.
642func (dc *downstreamConn) marshalMessage(msg *irc.Message, net *network) *irc.Message {
643 msg = msg.Copy()
644 msg.Prefix = dc.marshalUserPrefix(net, msg.Prefix)
645
646 if dc.network != nil {
647 return msg
648 }
649
650 switch msg.Command {
651 case "PRIVMSG", "NOTICE", "TAGMSG":
652 msg.Params[0] = dc.marshalEntity(net, msg.Params[0])
653 case "NICK":
654 // Nick change for another user
655 msg.Params[0] = dc.marshalEntity(net, msg.Params[0])
656 case "JOIN", "PART":
657 msg.Params[0] = dc.marshalEntity(net, msg.Params[0])
658 case "KICK":
659 msg.Params[0] = dc.marshalEntity(net, msg.Params[0])
660 msg.Params[1] = dc.marshalEntity(net, msg.Params[1])
661 case "TOPIC":
662 msg.Params[0] = dc.marshalEntity(net, msg.Params[0])
663 case "QUIT", "SETNAME":
664 // This space is intentionally left blank
665 default:
666 panic(fmt.Sprintf("unexpected %q message", msg.Command))
667 }
668
669 return msg
670}
671
672func (dc *downstreamConn) handleMessage(ctx context.Context, msg *irc.Message) error {
673 ctx, cancel := dc.conn.NewContext(ctx)
674 defer cancel()
675
676 ctx, cancel = context.WithTimeout(ctx, handleDownstreamMessageTimeout)
677 defer cancel()
678
679 switch msg.Command {
680 case "QUIT":
681 return dc.Close()
682 default:
683 if dc.registered {
684 return dc.handleMessageRegistered(ctx, msg)
685 } else {
686 return dc.handleMessageUnregistered(ctx, msg)
687 }
688 }
689}
690
691func (dc *downstreamConn) handleMessageUnregistered(ctx context.Context, msg *irc.Message) error {
692 switch msg.Command {
693 case "NICK":
694 var nick string
695 if err := parseMessageParams(msg, &nick); err != nil {
696 return err
697 }
698 if nick == "" || strings.ContainsAny(nick, illegalNickChars) {
699 return ircError{&irc.Message{
700 Command: irc.ERR_ERRONEUSNICKNAME,
701 Params: []string{dc.nick, nick, "contains illegal characters"},
702 }}
703 }
704 nickCM := casemapASCII(nick)
705 if nickCM == serviceNickCM {
706 return ircError{&irc.Message{
707 Command: irc.ERR_NICKNAMEINUSE,
708 Params: []string{dc.nick, nick, "Nickname reserved for bouncer service"},
709 }}
710 }
711 dc.nick = nick
712 dc.nickCM = nickCM
713 case "USER":
714 if err := parseMessageParams(msg, &dc.rawUsername, nil, nil, &dc.realname); err != nil {
715 return err
716 }
717 case "PASS":
718 if err := parseMessageParams(msg, &dc.password); err != nil {
719 return err
720 }
721 case "CAP":
722 var subCmd string
723 if err := parseMessageParams(msg, &subCmd); err != nil {
724 return err
725 }
726 if err := dc.handleCapCommand(subCmd, msg.Params[1:]); err != nil {
727 return err
728 }
729 case "AUTHENTICATE":
730 credentials, err := dc.handleAuthenticateCommand(msg)
731 if err != nil {
732 return err
733 } else if credentials == nil {
734 break
735 }
736
737 if err := dc.authenticate(ctx, credentials.plainUsername, credentials.plainPassword); err != nil {
738 dc.logger.Printf("SASL authentication error for user %q: %v", credentials.plainUsername, err)
739 dc.endSASL(&irc.Message{
740 Prefix: dc.srv.prefix(),
741 Command: irc.ERR_SASLFAIL,
742 Params: []string{dc.nick, authErrorReason(err)},
743 })
744 break
745 }
746
747 // Technically we should send RPL_LOGGEDIN here. However we use
748 // RPL_LOGGEDIN to mirror the upstream connection status. Let's
749 // see how many clients that breaks. See:
750 // https://github.com/ircv3/ircv3-specifications/pull/476
751 dc.endSASL(nil)
752 case "BOUNCER":
753 var subcommand string
754 if err := parseMessageParams(msg, &subcommand); err != nil {
755 return err
756 }
757
758 switch strings.ToUpper(subcommand) {
759 case "BIND":
760 var idStr string
761 if err := parseMessageParams(msg, nil, &idStr); err != nil {
762 return err
763 }
764
765 if dc.user == nil {
766 return ircError{&irc.Message{
767 Command: "FAIL",
768 Params: []string{"BOUNCER", "ACCOUNT_REQUIRED", "BIND", "Authentication needed to bind to bouncer network"},
769 }}
770 }
771
772 id, err := parseBouncerNetID(subcommand, idStr)
773 if err != nil {
774 return err
775 }
776
777 var match *network
778 dc.user.forEachNetwork(func(net *network) {
779 if net.ID == id {
780 match = net
781 }
782 })
783 if match == nil {
784 return ircError{&irc.Message{
785 Command: "FAIL",
786 Params: []string{"BOUNCER", "INVALID_NETID", idStr, "Unknown network ID"},
787 }}
788 }
789
790 dc.networkName = match.GetName()
791 }
792 default:
793 dc.logger.Printf("unhandled message: %v", msg)
794 return newUnknownCommandError(msg.Command)
795 }
796 if dc.rawUsername != "" && dc.nick != "*" && !dc.negotiatingCaps {
797 return dc.register(ctx)
798 }
799 return nil
800}
801
802func (dc *downstreamConn) handleCapCommand(cmd string, args []string) error {
803 cmd = strings.ToUpper(cmd)
804
805 switch cmd {
806 case "LS":
807 if len(args) > 0 {
808 var err error
809 if dc.capVersion, err = strconv.Atoi(args[0]); err != nil {
810 return err
811 }
812 }
813 if !dc.registered && dc.capVersion >= 302 {
814 // Let downstream show everything it supports, and trim
815 // down the available capabilities when upstreams are
816 // known.
817 for k, v := range needAllDownstreamCaps {
818 dc.supportedCaps[k] = v
819 }
820 }
821
822 caps := make([]string, 0, len(dc.supportedCaps))
823 for k, v := range dc.supportedCaps {
824 if dc.capVersion >= 302 && v != "" {
825 caps = append(caps, k+"="+v)
826 } else {
827 caps = append(caps, k)
828 }
829 }
830
831 // TODO: multi-line replies
832 dc.SendMessage(&irc.Message{
833 Prefix: dc.srv.prefix(),
834 Command: "CAP",
835 Params: []string{dc.nick, "LS", strings.Join(caps, " ")},
836 })
837
838 if dc.capVersion >= 302 {
839 // CAP version 302 implicitly enables cap-notify
840 dc.caps["cap-notify"] = true
841 }
842
843 if !dc.registered {
844 dc.negotiatingCaps = true
845 }
846 case "LIST":
847 var caps []string
848 for name, enabled := range dc.caps {
849 if enabled {
850 caps = append(caps, name)
851 }
852 }
853
854 // TODO: multi-line replies
855 dc.SendMessage(&irc.Message{
856 Prefix: dc.srv.prefix(),
857 Command: "CAP",
858 Params: []string{dc.nick, "LIST", strings.Join(caps, " ")},
859 })
860 case "REQ":
861 if len(args) == 0 {
862 return ircError{&irc.Message{
863 Command: err_invalidcapcmd,
864 Params: []string{dc.nick, cmd, "Missing argument in CAP REQ command"},
865 }}
866 }
867
868 // TODO: atomically ack/nak the whole capability set
869 caps := strings.Fields(args[0])
870 ack := true
871 for _, name := range caps {
872 name = strings.ToLower(name)
873 enable := !strings.HasPrefix(name, "-")
874 if !enable {
875 name = strings.TrimPrefix(name, "-")
876 }
877
878 if enable == dc.caps[name] {
879 continue
880 }
881
882 _, ok := dc.supportedCaps[name]
883 if !ok {
884 ack = false
885 break
886 }
887
888 if name == "cap-notify" && dc.capVersion >= 302 && !enable {
889 // cap-notify cannot be disabled with CAP version 302
890 ack = false
891 break
892 }
893
894 dc.caps[name] = enable
895 }
896
897 reply := "NAK"
898 if ack {
899 reply = "ACK"
900 }
901 dc.SendMessage(&irc.Message{
902 Prefix: dc.srv.prefix(),
903 Command: "CAP",
904 Params: []string{dc.nick, reply, args[0]},
905 })
906
907 if !dc.registered {
908 dc.negotiatingCaps = true
909 }
910 case "END":
911 dc.negotiatingCaps = false
912 default:
913 return ircError{&irc.Message{
914 Command: err_invalidcapcmd,
915 Params: []string{dc.nick, cmd, "Unknown CAP command"},
916 }}
917 }
918 return nil
919}
920
921func (dc *downstreamConn) handleAuthenticateCommand(msg *irc.Message) (result *downstreamSASL, err error) {
922 defer func() {
923 if err != nil {
924 dc.sasl = nil
925 }
926 }()
927
928 if !dc.caps["sasl"] {
929 return nil, ircError{&irc.Message{
930 Prefix: dc.srv.prefix(),
931 Command: irc.ERR_SASLFAIL,
932 Params: []string{dc.nick, "AUTHENTICATE requires the \"sasl\" capability to be enabled"},
933 }}
934 }
935 if len(msg.Params) == 0 {
936 return nil, ircError{&irc.Message{
937 Prefix: dc.srv.prefix(),
938 Command: irc.ERR_SASLFAIL,
939 Params: []string{dc.nick, "Missing AUTHENTICATE argument"},
940 }}
941 }
942 if msg.Params[0] == "*" {
943 return nil, ircError{&irc.Message{
944 Prefix: dc.srv.prefix(),
945 Command: irc.ERR_SASLABORTED,
946 Params: []string{dc.nick, "SASL authentication aborted"},
947 }}
948 }
949
950 var resp []byte
951 if dc.sasl == nil {
952 mech := strings.ToUpper(msg.Params[0])
953 var server sasl.Server
954 switch mech {
955 case "PLAIN":
956 server = sasl.NewPlainServer(sasl.PlainAuthenticator(func(identity, username, password string) error {
957 dc.sasl.plainUsername = username
958 dc.sasl.plainPassword = password
959 return nil
960 }))
961 default:
962 return nil, ircError{&irc.Message{
963 Prefix: dc.srv.prefix(),
964 Command: irc.ERR_SASLFAIL,
965 Params: []string{dc.nick, fmt.Sprintf("Unsupported SASL mechanism %q", mech)},
966 }}
967 }
968
969 dc.sasl = &downstreamSASL{server: server}
970 } else {
971 chunk := msg.Params[0]
972 if chunk == "+" {
973 chunk = ""
974 }
975
976 if dc.sasl.pendingResp.Len()+len(chunk) > 10*1024 {
977 return nil, ircError{&irc.Message{
978 Prefix: dc.srv.prefix(),
979 Command: irc.ERR_SASLFAIL,
980 Params: []string{dc.nick, "Response too long"},
981 }}
982 }
983
984 dc.sasl.pendingResp.WriteString(chunk)
985
986 if len(chunk) == maxSASLLength {
987 return nil, nil // Multi-line response, wait for the next command
988 }
989
990 resp, err = base64.StdEncoding.DecodeString(dc.sasl.pendingResp.String())
991 if err != nil {
992 return nil, ircError{&irc.Message{
993 Prefix: dc.srv.prefix(),
994 Command: irc.ERR_SASLFAIL,
995 Params: []string{dc.nick, "Invalid base64-encoded response"},
996 }}
997 }
998
999 dc.sasl.pendingResp.Reset()
1000 }
1001
1002 challenge, done, err := dc.sasl.server.Next(resp)
1003 if err != nil {
1004 return nil, err
1005 } else if done {
1006 return dc.sasl, nil
1007 } else {
1008 challengeStr := "+"
1009 if len(challenge) > 0 {
1010 challengeStr = base64.StdEncoding.EncodeToString(challenge)
1011 }
1012
1013 // TODO: multi-line messages
1014 dc.SendMessage(&irc.Message{
1015 Prefix: dc.srv.prefix(),
1016 Command: "AUTHENTICATE",
1017 Params: []string{challengeStr},
1018 })
1019 return nil, nil
1020 }
1021}
1022
1023func (dc *downstreamConn) endSASL(msg *irc.Message) {
1024 if dc.sasl == nil {
1025 return
1026 }
1027
1028 dc.sasl = nil
1029
1030 if msg != nil {
1031 dc.SendMessage(msg)
1032 } else {
1033 dc.SendMessage(&irc.Message{
1034 Prefix: dc.srv.prefix(),
1035 Command: irc.RPL_SASLSUCCESS,
1036 Params: []string{dc.nick, "SASL authentication successful"},
1037 })
1038 }
1039}
1040
1041func (dc *downstreamConn) setSupportedCap(name, value string) {
1042 prevValue, hasPrev := dc.supportedCaps[name]
1043 changed := !hasPrev || prevValue != value
1044 dc.supportedCaps[name] = value
1045
1046 if !dc.caps["cap-notify"] || !changed {
1047 return
1048 }
1049
1050 cap := name
1051 if value != "" && dc.capVersion >= 302 {
1052 cap = name + "=" + value
1053 }
1054
1055 dc.SendMessage(&irc.Message{
1056 Prefix: dc.srv.prefix(),
1057 Command: "CAP",
1058 Params: []string{dc.nick, "NEW", cap},
1059 })
1060}
1061
1062func (dc *downstreamConn) unsetSupportedCap(name string) {
1063 _, hasPrev := dc.supportedCaps[name]
1064 delete(dc.supportedCaps, name)
1065 delete(dc.caps, name)
1066
1067 if !dc.caps["cap-notify"] || !hasPrev {
1068 return
1069 }
1070
1071 dc.SendMessage(&irc.Message{
1072 Prefix: dc.srv.prefix(),
1073 Command: "CAP",
1074 Params: []string{dc.nick, "DEL", name},
1075 })
1076}
1077
1078func (dc *downstreamConn) updateSupportedCaps() {
1079 supportedCaps := make(map[string]bool)
1080 for cap := range needAllDownstreamCaps {
1081 supportedCaps[cap] = true
1082 }
1083 dc.forEachUpstream(func(uc *upstreamConn) {
1084 for cap, supported := range supportedCaps {
1085 supportedCaps[cap] = supported && uc.caps[cap]
1086 }
1087 })
1088
1089 for cap, supported := range supportedCaps {
1090 if supported {
1091 dc.setSupportedCap(cap, needAllDownstreamCaps[cap])
1092 } else {
1093 dc.unsetSupportedCap(cap)
1094 }
1095 }
1096
1097 if uc := dc.upstream(); uc != nil && uc.supportsSASL("PLAIN") {
1098 dc.setSupportedCap("sasl", "PLAIN")
1099 } else if dc.network != nil {
1100 dc.unsetSupportedCap("sasl")
1101 }
1102
1103 if uc := dc.upstream(); uc != nil && uc.caps["draft/account-registration"] {
1104 // Strip "before-connect", because we require downstreams to be fully
1105 // connected before attempting account registration.
1106 values := strings.Split(uc.supportedCaps["draft/account-registration"], ",")
1107 for i, v := range values {
1108 if v == "before-connect" {
1109 values = append(values[:i], values[i+1:]...)
1110 break
1111 }
1112 }
1113 dc.setSupportedCap("draft/account-registration", strings.Join(values, ","))
1114 } else {
1115 dc.unsetSupportedCap("draft/account-registration")
1116 }
1117
1118 if _, ok := dc.user.msgStore.(chatHistoryMessageStore); ok && dc.network != nil {
1119 dc.setSupportedCap("draft/event-playback", "")
1120 } else {
1121 dc.unsetSupportedCap("draft/event-playback")
1122 }
1123}
1124
1125func (dc *downstreamConn) updateNick() {
1126 if uc := dc.upstream(); uc != nil && uc.nick != dc.nick {
1127 dc.SendMessage(&irc.Message{
1128 Prefix: dc.prefix(),
1129 Command: "NICK",
1130 Params: []string{uc.nick},
1131 })
1132 dc.nick = uc.nick
1133 dc.nickCM = casemapASCII(dc.nick)
1134 }
1135}
1136
1137func (dc *downstreamConn) updateRealname() {
1138 if uc := dc.upstream(); uc != nil && uc.realname != dc.realname && dc.caps["setname"] {
1139 dc.SendMessage(&irc.Message{
1140 Prefix: dc.prefix(),
1141 Command: "SETNAME",
1142 Params: []string{uc.realname},
1143 })
1144 dc.realname = uc.realname
1145 }
1146}
1147
1148func (dc *downstreamConn) updateAccount() {
1149 var account string
1150 if dc.network == nil {
1151 account = dc.user.Username
1152 } else if uc := dc.upstream(); uc != nil {
1153 account = uc.account
1154 } else {
1155 return
1156 }
1157
1158 if dc.account == account || !dc.caps["sasl"] {
1159 return
1160 }
1161
1162 if account != "" {
1163 dc.SendMessage(&irc.Message{
1164 Prefix: dc.srv.prefix(),
1165 Command: irc.RPL_LOGGEDIN,
1166 Params: []string{dc.nick, dc.prefix().String(), account, "You are logged in as " + account},
1167 })
1168 } else {
1169 dc.SendMessage(&irc.Message{
1170 Prefix: dc.srv.prefix(),
1171 Command: irc.RPL_LOGGEDOUT,
1172 Params: []string{dc.nick, dc.prefix().String(), "You are logged out"},
1173 })
1174 }
1175
1176 dc.account = account
1177}
1178
1179func sanityCheckServer(ctx context.Context, addr string) error {
1180 ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
1181 defer cancel()
1182
1183 conn, err := new(tls.Dialer).DialContext(ctx, "tcp", addr)
1184 if err != nil {
1185 return err
1186 }
1187
1188 return conn.Close()
1189}
1190
1191func unmarshalUsername(rawUsername string) (username, client, network string) {
1192 username = rawUsername
1193
1194 i := strings.IndexAny(username, "/@")
1195 j := strings.LastIndexAny(username, "/@")
1196 if i >= 0 {
1197 username = rawUsername[:i]
1198 }
1199 if j >= 0 {
1200 if rawUsername[j] == '@' {
1201 client = rawUsername[j+1:]
1202 } else {
1203 network = rawUsername[j+1:]
1204 }
1205 }
1206 if i >= 0 && j >= 0 && i < j {
1207 if rawUsername[i] == '@' {
1208 client = rawUsername[i+1 : j]
1209 } else {
1210 network = rawUsername[i+1 : j]
1211 }
1212 }
1213
1214 return username, client, network
1215}
1216
1217func (dc *downstreamConn) authenticate(ctx context.Context, username, password string) error {
1218 username, clientName, networkName := unmarshalUsername(username)
1219
1220 u, err := dc.srv.db.GetUser(ctx, username)
1221 if err != nil {
1222 return newInvalidUsernameOrPasswordError(fmt.Errorf("user not found: %w", err))
1223 }
1224
1225 // Password auth disabled
1226 if u.Password == "" {
1227 return newInvalidUsernameOrPasswordError(fmt.Errorf("password auth disabled"))
1228 }
1229
1230 err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
1231 if err != nil {
1232 return newInvalidUsernameOrPasswordError(fmt.Errorf("wrong password"))
1233 }
1234
1235 dc.user = dc.srv.getUser(username)
1236 if dc.user == nil {
1237 return fmt.Errorf("user not active")
1238 }
1239 dc.clientName = clientName
1240 dc.networkName = networkName
1241 return nil
1242}
1243
1244func (dc *downstreamConn) register(ctx context.Context) error {
1245 if dc.registered {
1246 return fmt.Errorf("tried to register twice")
1247 }
1248
1249 if dc.sasl != nil {
1250 dc.endSASL(&irc.Message{
1251 Prefix: dc.srv.prefix(),
1252 Command: irc.ERR_SASLABORTED,
1253 Params: []string{dc.nick, "SASL authentication aborted"},
1254 })
1255 }
1256
1257 password := dc.password
1258 dc.password = ""
1259 if dc.user == nil {
1260 if password == "" {
1261 if dc.caps["sasl"] {
1262 return ircError{&irc.Message{
1263 Command: "FAIL",
1264 Params: []string{"*", "ACCOUNT_REQUIRED", "Authentication required"},
1265 }}
1266 } else {
1267 return ircError{&irc.Message{
1268 Command: irc.ERR_PASSWDMISMATCH,
1269 Params: []string{dc.nick, "Authentication required"},
1270 }}
1271 }
1272 }
1273
1274 if err := dc.authenticate(ctx, dc.rawUsername, password); err != nil {
1275 dc.logger.Printf("PASS authentication error for user %q: %v", dc.rawUsername, err)
1276 return ircError{&irc.Message{
1277 Command: irc.ERR_PASSWDMISMATCH,
1278 Params: []string{dc.nick, authErrorReason(err)},
1279 }}
1280 }
1281 }
1282
1283 if dc.clientName == "" && dc.networkName == "" {
1284 _, dc.clientName, dc.networkName = unmarshalUsername(dc.rawUsername)
1285 }
1286
1287 dc.registered = true
1288 dc.logger.Printf("registration complete for user %q", dc.user.Username)
1289 return nil
1290}
1291
1292func (dc *downstreamConn) loadNetwork(ctx context.Context) error {
1293 if dc.networkName == "" {
1294 return nil
1295 }
1296
1297 network := dc.user.getNetwork(dc.networkName)
1298 if network == nil {
1299 addr := dc.networkName
1300 if !strings.ContainsRune(addr, ':') {
1301 addr = addr + ":6697"
1302 }
1303
1304 dc.logger.Printf("trying to connect to new network %q", addr)
1305 if err := sanityCheckServer(ctx, addr); err != nil {
1306 dc.logger.Printf("failed to connect to %q: %v", addr, err)
1307 return ircError{&irc.Message{
1308 Command: irc.ERR_PASSWDMISMATCH,
1309 Params: []string{dc.nick, fmt.Sprintf("Failed to connect to %q", dc.networkName)},
1310 }}
1311 }
1312
1313 // Some clients only allow specifying the nickname (and use the
1314 // nickname as a username too). Strip the network name from the
1315 // nickname when auto-saving networks.
1316 nick, _, _ := unmarshalUsername(dc.nick)
1317
1318 dc.logger.Printf("auto-saving network %q", dc.networkName)
1319 var err error
1320 network, err = dc.user.createNetwork(ctx, &Network{
1321 Addr: dc.networkName,
1322 Nick: nick,
1323 Enabled: true,
1324 })
1325 if err != nil {
1326 return err
1327 }
1328 }
1329
1330 dc.network = network
1331 return nil
1332}
1333
1334func (dc *downstreamConn) welcome(ctx context.Context) error {
1335 if dc.user == nil || !dc.registered {
1336 panic("tried to welcome an unregistered connection")
1337 }
1338
1339 remoteAddr := dc.conn.RemoteAddr().String()
1340 dc.logger = &prefixLogger{dc.srv.Logger, fmt.Sprintf("user %q: downstream %q: ", dc.user.Username, remoteAddr)}
1341
1342 // TODO: doing this might take some time. We should do it in dc.register
1343 // instead, but we'll potentially be adding a new network and this must be
1344 // done in the user goroutine.
1345 if err := dc.loadNetwork(ctx); err != nil {
1346 return err
1347 }
1348
1349 if dc.network == nil && !dc.caps["soju.im/bouncer-networks"] && dc.srv.Config().MultiUpstream {
1350 dc.isMultiUpstream = true
1351 }
1352
1353 dc.updateSupportedCaps()
1354
1355 isupport := []string{
1356 fmt.Sprintf("CHATHISTORY=%v", chatHistoryLimit),
1357 "CASEMAPPING=ascii",
1358 }
1359
1360 if dc.network != nil {
1361 isupport = append(isupport, fmt.Sprintf("BOUNCER_NETID=%v", dc.network.ID))
1362 }
1363 if title := dc.srv.Config().Title; dc.network == nil && title != "" {
1364 isupport = append(isupport, "NETWORK="+encodeISUPPORT(title))
1365 }
1366 if dc.network == nil && !dc.isMultiUpstream {
1367 isupport = append(isupport, "WHOX")
1368 }
1369
1370 if uc := dc.upstream(); uc != nil {
1371 for k := range passthroughIsupport {
1372 v, ok := uc.isupport[k]
1373 if !ok {
1374 continue
1375 }
1376 if v != nil {
1377 isupport = append(isupport, fmt.Sprintf("%v=%v", k, *v))
1378 } else {
1379 isupport = append(isupport, k)
1380 }
1381 }
1382 }
1383
1384 dc.SendMessage(&irc.Message{
1385 Prefix: dc.srv.prefix(),
1386 Command: irc.RPL_WELCOME,
1387 Params: []string{dc.nick, "Welcome to soju, " + dc.nick},
1388 })
1389 dc.SendMessage(&irc.Message{
1390 Prefix: dc.srv.prefix(),
1391 Command: irc.RPL_YOURHOST,
1392 Params: []string{dc.nick, "Your host is " + dc.srv.Config().Hostname},
1393 })
1394 dc.SendMessage(&irc.Message{
1395 Prefix: dc.srv.prefix(),
1396 Command: irc.RPL_MYINFO,
1397 Params: []string{dc.nick, dc.srv.Config().Hostname, "soju", "aiwroO", "OovaimnqpsrtklbeI"},
1398 })
1399 for _, msg := range generateIsupport(dc.srv.prefix(), dc.nick, isupport) {
1400 dc.SendMessage(msg)
1401 }
1402 if uc := dc.upstream(); uc != nil {
1403 dc.SendMessage(&irc.Message{
1404 Prefix: dc.srv.prefix(),
1405 Command: irc.RPL_UMODEIS,
1406 Params: []string{dc.nick, "+" + string(uc.modes)},
1407 })
1408 }
1409 if dc.network == nil && !dc.isMultiUpstream && dc.user.Admin {
1410 dc.SendMessage(&irc.Message{
1411 Prefix: dc.srv.prefix(),
1412 Command: irc.RPL_UMODEIS,
1413 Params: []string{dc.nick, "+o"},
1414 })
1415 }
1416
1417 dc.updateNick()
1418 dc.updateRealname()
1419 dc.updateAccount()
1420
1421 if motd := dc.user.srv.Config().MOTD; motd != "" && dc.network == nil {
1422 for _, msg := range generateMOTD(dc.srv.prefix(), dc.nick, motd) {
1423 dc.SendMessage(msg)
1424 }
1425 } else {
1426 motdHint := "No MOTD"
1427 if dc.network != nil {
1428 motdHint = "Use /motd to read the message of the day"
1429 }
1430 dc.SendMessage(&irc.Message{
1431 Prefix: dc.srv.prefix(),
1432 Command: irc.ERR_NOMOTD,
1433 Params: []string{dc.nick, motdHint},
1434 })
1435 }
1436
1437 if dc.caps["soju.im/bouncer-networks-notify"] {
1438 dc.SendBatch("soju.im/bouncer-networks", nil, nil, func(batchRef irc.TagValue) {
1439 dc.user.forEachNetwork(func(network *network) {
1440 idStr := fmt.Sprintf("%v", network.ID)
1441 attrs := getNetworkAttrs(network)
1442 dc.SendMessage(&irc.Message{
1443 Tags: irc.Tags{"batch": batchRef},
1444 Prefix: dc.srv.prefix(),
1445 Command: "BOUNCER",
1446 Params: []string{"NETWORK", idStr, attrs.String()},
1447 })
1448 })
1449 })
1450 }
1451
1452 dc.forEachUpstream(func(uc *upstreamConn) {
1453 for _, entry := range uc.channels.innerMap {
1454 ch := entry.value.(*upstreamChannel)
1455 if !ch.complete {
1456 continue
1457 }
1458 record := uc.network.channels.Value(ch.Name)
1459 if record != nil && record.Detached {
1460 continue
1461 }
1462
1463 dc.SendMessage(&irc.Message{
1464 Prefix: dc.prefix(),
1465 Command: "JOIN",
1466 Params: []string{dc.marshalEntity(ch.conn.network, ch.Name)},
1467 })
1468
1469 forwardChannel(dc, ch)
1470 }
1471 })
1472
1473 dc.forEachNetwork(func(net *network) {
1474 if dc.caps["draft/chathistory"] || dc.user.msgStore == nil {
1475 return
1476 }
1477
1478 // Only send history if we're the first connected client with that name
1479 // for the network
1480 firstClient := true
1481 dc.user.forEachDownstream(func(c *downstreamConn) {
1482 if c != dc && c.clientName == dc.clientName && c.network == dc.network {
1483 firstClient = false
1484 }
1485 })
1486 if firstClient {
1487 net.delivered.ForEachTarget(func(target string) {
1488 lastDelivered := net.delivered.LoadID(target, dc.clientName)
1489 if lastDelivered == "" {
1490 return
1491 }
1492
1493 dc.sendTargetBacklog(ctx, net, target, lastDelivered)
1494
1495 // Fast-forward history to last message
1496 targetCM := net.casemap(target)
1497 lastID, err := dc.user.msgStore.LastMsgID(&net.Network, targetCM, time.Now())
1498 if err != nil {
1499 dc.logger.Printf("failed to get last message ID: %v", err)
1500 return
1501 }
1502 net.delivered.StoreID(target, dc.clientName, lastID)
1503 })
1504 }
1505 })
1506
1507 return nil
1508}
1509
1510// messageSupportsBacklog checks whether the provided message can be sent as
1511// part of an history batch.
1512func (dc *downstreamConn) messageSupportsBacklog(msg *irc.Message) bool {
1513 // Don't replay all messages, because that would mess up client
1514 // state. For instance we just sent the list of users, sending
1515 // PART messages for one of these users would be incorrect.
1516 switch msg.Command {
1517 case "PRIVMSG", "NOTICE":
1518 return true
1519 }
1520 return false
1521}
1522
1523func (dc *downstreamConn) sendTargetBacklog(ctx context.Context, net *network, target, msgID string) {
1524 if dc.caps["draft/chathistory"] || dc.user.msgStore == nil {
1525 return
1526 }
1527
1528 ch := net.channels.Value(target)
1529
1530 ctx, cancel := context.WithTimeout(ctx, backlogTimeout)
1531 defer cancel()
1532
1533 targetCM := net.casemap(target)
1534 history, err := dc.user.msgStore.LoadLatestID(ctx, &net.Network, targetCM, msgID, backlogLimit)
1535 if err != nil {
1536 dc.logger.Printf("failed to send backlog for %q: %v", target, err)
1537 return
1538 }
1539
1540 dc.SendBatch("chathistory", []string{dc.marshalEntity(net, target)}, nil, func(batchRef irc.TagValue) {
1541 for _, msg := range history {
1542 if ch != nil && ch.Detached {
1543 if net.detachedMessageNeedsRelay(ch, msg) {
1544 dc.relayDetachedMessage(net, msg)
1545 }
1546 } else {
1547 msg.Tags["batch"] = batchRef
1548 dc.SendMessage(dc.marshalMessage(msg, net))
1549 }
1550 }
1551 })
1552}
1553
1554func (dc *downstreamConn) relayDetachedMessage(net *network, msg *irc.Message) {
1555 if msg.Command != "PRIVMSG" && msg.Command != "NOTICE" {
1556 return
1557 }
1558
1559 sender := msg.Prefix.Name
1560 target, text := msg.Params[0], msg.Params[1]
1561 if net.isHighlight(msg) {
1562 sendServiceNOTICE(dc, fmt.Sprintf("highlight in %v: <%v> %v", dc.marshalEntity(net, target), sender, text))
1563 } else {
1564 sendServiceNOTICE(dc, fmt.Sprintf("message in %v: <%v> %v", dc.marshalEntity(net, target), sender, text))
1565 }
1566}
1567
1568func (dc *downstreamConn) runUntilRegistered() error {
1569 ctx, cancel := context.WithTimeout(context.TODO(), downstreamRegisterTimeout)
1570 defer cancel()
1571
1572 // Close the connection with an error if the deadline is exceeded
1573 go func() {
1574 <-ctx.Done()
1575 if err := ctx.Err(); err == context.DeadlineExceeded {
1576 dc.SendMessage(&irc.Message{
1577 Prefix: dc.srv.prefix(),
1578 Command: "ERROR",
1579 Params: []string{"Connection registration timed out"},
1580 })
1581 dc.Close()
1582 }
1583 }()
1584
1585 for !dc.registered {
1586 msg, err := dc.ReadMessage()
1587 if err != nil {
1588 return fmt.Errorf("failed to read IRC command: %w", err)
1589 }
1590
1591 err = dc.handleMessage(ctx, msg)
1592 if ircErr, ok := err.(ircError); ok {
1593 ircErr.Message.Prefix = dc.srv.prefix()
1594 dc.SendMessage(ircErr.Message)
1595 } else if err != nil {
1596 return fmt.Errorf("failed to handle IRC command %q: %v", msg, err)
1597 }
1598 }
1599
1600 return nil
1601}
1602
1603func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc.Message) error {
1604 switch msg.Command {
1605 case "CAP":
1606 var subCmd string
1607 if err := parseMessageParams(msg, &subCmd); err != nil {
1608 return err
1609 }
1610 if err := dc.handleCapCommand(subCmd, msg.Params[1:]); err != nil {
1611 return err
1612 }
1613 case "PING":
1614 var source, destination string
1615 if err := parseMessageParams(msg, &source); err != nil {
1616 return err
1617 }
1618 if len(msg.Params) > 1 {
1619 destination = msg.Params[1]
1620 }
1621 hostname := dc.srv.Config().Hostname
1622 if destination != "" && destination != hostname {
1623 return ircError{&irc.Message{
1624 Command: irc.ERR_NOSUCHSERVER,
1625 Params: []string{dc.nick, destination, "No such server"},
1626 }}
1627 }
1628 dc.SendMessage(&irc.Message{
1629 Prefix: dc.srv.prefix(),
1630 Command: "PONG",
1631 Params: []string{hostname, source},
1632 })
1633 return nil
1634 case "PONG":
1635 if len(msg.Params) == 0 {
1636 return newNeedMoreParamsError(msg.Command)
1637 }
1638 token := msg.Params[len(msg.Params)-1]
1639 dc.handlePong(token)
1640 case "USER":
1641 return ircError{&irc.Message{
1642 Command: irc.ERR_ALREADYREGISTERED,
1643 Params: []string{dc.nick, "You may not reregister"},
1644 }}
1645 case "NICK":
1646 var rawNick string
1647 if err := parseMessageParams(msg, &rawNick); err != nil {
1648 return err
1649 }
1650
1651 nick := rawNick
1652 var upstream *upstreamConn
1653 if dc.upstream() == nil {
1654 uc, unmarshaledNick, err := dc.unmarshalEntity(nick)
1655 if err == nil { // NICK nick/network: NICK only on a specific upstream
1656 upstream = uc
1657 nick = unmarshaledNick
1658 }
1659 }
1660
1661 if nick == "" || strings.ContainsAny(nick, illegalNickChars) {
1662 return ircError{&irc.Message{
1663 Command: irc.ERR_ERRONEUSNICKNAME,
1664 Params: []string{dc.nick, rawNick, "contains illegal characters"},
1665 }}
1666 }
1667 if casemapASCII(nick) == serviceNickCM {
1668 return ircError{&irc.Message{
1669 Command: irc.ERR_NICKNAMEINUSE,
1670 Params: []string{dc.nick, rawNick, "Nickname reserved for bouncer service"},
1671 }}
1672 }
1673
1674 var err error
1675 dc.forEachNetwork(func(n *network) {
1676 if err != nil || (upstream != nil && upstream.network != n) {
1677 return
1678 }
1679 n.Nick = nick
1680 err = dc.srv.db.StoreNetwork(ctx, dc.user.ID, &n.Network)
1681 })
1682 if err != nil {
1683 return err
1684 }
1685
1686 dc.forEachUpstream(func(uc *upstreamConn) {
1687 if upstream != nil && upstream != uc {
1688 return
1689 }
1690 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
1691 Command: "NICK",
1692 Params: []string{nick},
1693 })
1694 })
1695
1696 if dc.upstream() == nil && upstream == nil && dc.nick != nick {
1697 dc.SendMessage(&irc.Message{
1698 Prefix: dc.prefix(),
1699 Command: "NICK",
1700 Params: []string{nick},
1701 })
1702 dc.nick = nick
1703 dc.nickCM = casemapASCII(dc.nick)
1704 }
1705 case "SETNAME":
1706 var realname string
1707 if err := parseMessageParams(msg, &realname); err != nil {
1708 return err
1709 }
1710
1711 // If the client just resets to the default, just wipe the per-network
1712 // preference
1713 storeRealname := realname
1714 if realname == dc.user.Realname {
1715 storeRealname = ""
1716 }
1717
1718 var storeErr error
1719 var needUpdate []Network
1720 dc.forEachNetwork(func(n *network) {
1721 // We only need to call updateNetwork for upstreams that don't
1722 // support setname
1723 if uc := n.conn; uc != nil && uc.caps["setname"] {
1724 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
1725 Command: "SETNAME",
1726 Params: []string{realname},
1727 })
1728
1729 n.Realname = storeRealname
1730 if err := dc.srv.db.StoreNetwork(ctx, dc.user.ID, &n.Network); err != nil {
1731 dc.logger.Printf("failed to store network realname: %v", err)
1732 storeErr = err
1733 }
1734 return
1735 }
1736
1737 record := n.Network // copy network record because we'll mutate it
1738 record.Realname = storeRealname
1739 needUpdate = append(needUpdate, record)
1740 })
1741
1742 // Walk the network list as a second step, because updateNetwork
1743 // mutates the original list
1744 for _, record := range needUpdate {
1745 if _, err := dc.user.updateNetwork(ctx, &record); err != nil {
1746 dc.logger.Printf("failed to update network realname: %v", err)
1747 storeErr = err
1748 }
1749 }
1750 if storeErr != nil {
1751 return ircError{&irc.Message{
1752 Command: "FAIL",
1753 Params: []string{"SETNAME", "CANNOT_CHANGE_REALNAME", "Failed to update realname"},
1754 }}
1755 }
1756
1757 if dc.upstream() == nil {
1758 dc.SendMessage(&irc.Message{
1759 Prefix: dc.prefix(),
1760 Command: "SETNAME",
1761 Params: []string{realname},
1762 })
1763 }
1764 case "JOIN":
1765 var namesStr string
1766 if err := parseMessageParams(msg, &namesStr); err != nil {
1767 return err
1768 }
1769
1770 var keys []string
1771 if len(msg.Params) > 1 {
1772 keys = strings.Split(msg.Params[1], ",")
1773 }
1774
1775 for i, name := range strings.Split(namesStr, ",") {
1776 uc, upstreamName, err := dc.unmarshalEntity(name)
1777 if err != nil {
1778 return err
1779 }
1780
1781 var key string
1782 if len(keys) > i {
1783 key = keys[i]
1784 }
1785
1786 if !uc.isChannel(upstreamName) {
1787 dc.SendMessage(&irc.Message{
1788 Prefix: dc.srv.prefix(),
1789 Command: irc.ERR_NOSUCHCHANNEL,
1790 Params: []string{name, "Not a channel name"},
1791 })
1792 continue
1793 }
1794
1795 // Most servers ignore duplicate JOIN messages. We ignore them here
1796 // because some clients automatically send JOIN messages in bulk
1797 // when reconnecting to the bouncer. We don't want to flood the
1798 // upstream connection with these.
1799 if !uc.channels.Has(upstreamName) {
1800 params := []string{upstreamName}
1801 if key != "" {
1802 params = append(params, key)
1803 }
1804 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
1805 Command: "JOIN",
1806 Params: params,
1807 })
1808 }
1809
1810 ch := uc.network.channels.Value(upstreamName)
1811 if ch != nil {
1812 // Don't clear the channel key if there's one set
1813 // TODO: add a way to unset the channel key
1814 if key != "" {
1815 ch.Key = key
1816 }
1817 uc.network.attach(ch)
1818 } else {
1819 ch = &Channel{
1820 Name: upstreamName,
1821 Key: key,
1822 }
1823 uc.network.channels.SetValue(upstreamName, ch)
1824 }
1825 if err := dc.srv.db.StoreChannel(ctx, uc.network.ID, ch); err != nil {
1826 dc.logger.Printf("failed to create or update channel %q: %v", upstreamName, err)
1827 }
1828 }
1829 case "PART":
1830 var namesStr string
1831 if err := parseMessageParams(msg, &namesStr); err != nil {
1832 return err
1833 }
1834
1835 var reason string
1836 if len(msg.Params) > 1 {
1837 reason = msg.Params[1]
1838 }
1839
1840 for _, name := range strings.Split(namesStr, ",") {
1841 uc, upstreamName, err := dc.unmarshalEntity(name)
1842 if err != nil {
1843 return err
1844 }
1845
1846 if strings.EqualFold(reason, "detach") {
1847 ch := uc.network.channels.Value(upstreamName)
1848 if ch != nil {
1849 uc.network.detach(ch)
1850 } else {
1851 ch = &Channel{
1852 Name: name,
1853 Detached: true,
1854 }
1855 uc.network.channels.SetValue(upstreamName, ch)
1856 }
1857 if err := dc.srv.db.StoreChannel(ctx, uc.network.ID, ch); err != nil {
1858 dc.logger.Printf("failed to create or update channel %q: %v", upstreamName, err)
1859 }
1860 } else {
1861 params := []string{upstreamName}
1862 if reason != "" {
1863 params = append(params, reason)
1864 }
1865 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
1866 Command: "PART",
1867 Params: params,
1868 })
1869
1870 if err := uc.network.deleteChannel(ctx, upstreamName); err != nil {
1871 dc.logger.Printf("failed to delete channel %q: %v", upstreamName, err)
1872 }
1873 }
1874 }
1875 case "KICK":
1876 var channelStr, userStr string
1877 if err := parseMessageParams(msg, &channelStr, &userStr); err != nil {
1878 return err
1879 }
1880
1881 channels := strings.Split(channelStr, ",")
1882 users := strings.Split(userStr, ",")
1883
1884 var reason string
1885 if len(msg.Params) > 2 {
1886 reason = msg.Params[2]
1887 }
1888
1889 if len(channels) != 1 && len(channels) != len(users) {
1890 return ircError{&irc.Message{
1891 Command: irc.ERR_BADCHANMASK,
1892 Params: []string{dc.nick, channelStr, "Bad channel mask"},
1893 }}
1894 }
1895
1896 for i, user := range users {
1897 var channel string
1898 if len(channels) == 1 {
1899 channel = channels[0]
1900 } else {
1901 channel = channels[i]
1902 }
1903
1904 ucChannel, upstreamChannel, err := dc.unmarshalEntity(channel)
1905 if err != nil {
1906 return err
1907 }
1908
1909 ucUser, upstreamUser, err := dc.unmarshalEntity(user)
1910 if err != nil {
1911 return err
1912 }
1913
1914 if ucChannel != ucUser {
1915 return ircError{&irc.Message{
1916 Command: irc.ERR_USERNOTINCHANNEL,
1917 Params: []string{dc.nick, user, channel, "They are on another network"},
1918 }}
1919 }
1920 uc := ucChannel
1921
1922 params := []string{upstreamChannel, upstreamUser}
1923 if reason != "" {
1924 params = append(params, reason)
1925 }
1926 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
1927 Command: "KICK",
1928 Params: params,
1929 })
1930 }
1931 case "MODE":
1932 var name string
1933 if err := parseMessageParams(msg, &name); err != nil {
1934 return err
1935 }
1936
1937 var modeStr string
1938 if len(msg.Params) > 1 {
1939 modeStr = msg.Params[1]
1940 }
1941
1942 if casemapASCII(name) == dc.nickCM {
1943 if modeStr != "" {
1944 if uc := dc.upstream(); uc != nil {
1945 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
1946 Command: "MODE",
1947 Params: []string{uc.nick, modeStr},
1948 })
1949 } else {
1950 dc.SendMessage(&irc.Message{
1951 Prefix: dc.srv.prefix(),
1952 Command: irc.ERR_UMODEUNKNOWNFLAG,
1953 Params: []string{dc.nick, "Cannot change user mode in multi-upstream mode"},
1954 })
1955 }
1956 } else {
1957 var userMode string
1958 if uc := dc.upstream(); uc != nil {
1959 userMode = string(uc.modes)
1960 }
1961
1962 dc.SendMessage(&irc.Message{
1963 Prefix: dc.srv.prefix(),
1964 Command: irc.RPL_UMODEIS,
1965 Params: []string{dc.nick, "+" + userMode},
1966 })
1967 }
1968 return nil
1969 }
1970
1971 uc, upstreamName, err := dc.unmarshalEntity(name)
1972 if err != nil {
1973 return err
1974 }
1975
1976 if !uc.isChannel(upstreamName) {
1977 return ircError{&irc.Message{
1978 Command: irc.ERR_USERSDONTMATCH,
1979 Params: []string{dc.nick, "Cannot change mode for other users"},
1980 }}
1981 }
1982
1983 if modeStr != "" {
1984 params := []string{upstreamName, modeStr}
1985 params = append(params, msg.Params[2:]...)
1986 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
1987 Command: "MODE",
1988 Params: params,
1989 })
1990 } else {
1991 ch := uc.channels.Value(upstreamName)
1992 if ch == nil {
1993 return ircError{&irc.Message{
1994 Command: irc.ERR_NOSUCHCHANNEL,
1995 Params: []string{dc.nick, name, "No such channel"},
1996 }}
1997 }
1998
1999 if ch.modes == nil {
2000 // we haven't received the initial RPL_CHANNELMODEIS yet
2001 // ignore the request, we will broadcast the modes later when we receive RPL_CHANNELMODEIS
2002 return nil
2003 }
2004
2005 modeStr, modeParams := ch.modes.Format()
2006 params := []string{dc.nick, name, modeStr}
2007 params = append(params, modeParams...)
2008
2009 dc.SendMessage(&irc.Message{
2010 Prefix: dc.srv.prefix(),
2011 Command: irc.RPL_CHANNELMODEIS,
2012 Params: params,
2013 })
2014 if ch.creationTime != "" {
2015 dc.SendMessage(&irc.Message{
2016 Prefix: dc.srv.prefix(),
2017 Command: rpl_creationtime,
2018 Params: []string{dc.nick, name, ch.creationTime},
2019 })
2020 }
2021 }
2022 case "TOPIC":
2023 var channel string
2024 if err := parseMessageParams(msg, &channel); err != nil {
2025 return err
2026 }
2027
2028 uc, upstreamName, err := dc.unmarshalEntity(channel)
2029 if err != nil {
2030 return err
2031 }
2032
2033 if len(msg.Params) > 1 { // setting topic
2034 topic := msg.Params[1]
2035 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
2036 Command: "TOPIC",
2037 Params: []string{upstreamName, topic},
2038 })
2039 } else { // getting topic
2040 ch := uc.channels.Value(upstreamName)
2041 if ch == nil {
2042 return ircError{&irc.Message{
2043 Command: irc.ERR_NOSUCHCHANNEL,
2044 Params: []string{dc.nick, upstreamName, "No such channel"},
2045 }}
2046 }
2047 sendTopic(dc, ch)
2048 }
2049 case "LIST":
2050 network := dc.network
2051 if network == nil && len(msg.Params) > 0 {
2052 var err error
2053 network, msg.Params[0], err = dc.unmarshalEntityNetwork(msg.Params[0])
2054 if err != nil {
2055 return err
2056 }
2057 }
2058 if network == nil {
2059 dc.SendMessage(&irc.Message{
2060 Prefix: dc.srv.prefix(),
2061 Command: irc.RPL_LISTEND,
2062 Params: []string{dc.nick, "LIST without a network suffix is not supported in multi-upstream mode"},
2063 })
2064 return nil
2065 }
2066
2067 uc := network.conn
2068 if uc == nil {
2069 dc.SendMessage(&irc.Message{
2070 Prefix: dc.srv.prefix(),
2071 Command: irc.RPL_LISTEND,
2072 Params: []string{dc.nick, "Disconnected from upstream server"},
2073 })
2074 return nil
2075 }
2076
2077 uc.enqueueCommand(dc, msg)
2078 case "NAMES":
2079 if len(msg.Params) == 0 {
2080 dc.SendMessage(&irc.Message{
2081 Prefix: dc.srv.prefix(),
2082 Command: irc.RPL_ENDOFNAMES,
2083 Params: []string{dc.nick, "*", "End of /NAMES list"},
2084 })
2085 return nil
2086 }
2087
2088 channels := strings.Split(msg.Params[0], ",")
2089 for _, channel := range channels {
2090 uc, upstreamName, err := dc.unmarshalEntity(channel)
2091 if err != nil {
2092 return err
2093 }
2094
2095 ch := uc.channels.Value(upstreamName)
2096 if ch != nil {
2097 sendNames(dc, ch)
2098 } else {
2099 // NAMES on a channel we have not joined, ask upstream
2100 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
2101 Command: "NAMES",
2102 Params: []string{upstreamName},
2103 })
2104 }
2105 }
2106 // For WHOX docs, see:
2107 // - http://faerion.sourceforge.net/doc/irc/whox.var
2108 // - https://github.com/quakenet/snircd/blob/master/doc/readme.who
2109 // Note, many features aren't widely implemented, such as flags and mask2
2110 case "WHO":
2111 if len(msg.Params) == 0 {
2112 // TODO: support WHO without parameters
2113 dc.SendMessage(&irc.Message{
2114 Prefix: dc.srv.prefix(),
2115 Command: irc.RPL_ENDOFWHO,
2116 Params: []string{dc.nick, "*", "End of /WHO list"},
2117 })
2118 return nil
2119 }
2120
2121 // Clients will use the first mask to match RPL_ENDOFWHO
2122 endOfWhoToken := msg.Params[0]
2123
2124 // TODO: add support for WHOX mask2
2125 mask := msg.Params[0]
2126 var options string
2127 if len(msg.Params) > 1 {
2128 options = msg.Params[1]
2129 }
2130
2131 optionsParts := strings.SplitN(options, "%", 2)
2132 // TODO: add support for WHOX flags in optionsParts[0]
2133 var fields, whoxToken string
2134 if len(optionsParts) == 2 {
2135 optionsParts := strings.SplitN(optionsParts[1], ",", 2)
2136 fields = strings.ToLower(optionsParts[0])
2137 if len(optionsParts) == 2 && strings.Contains(fields, "t") {
2138 whoxToken = optionsParts[1]
2139 }
2140 }
2141
2142 // TODO: support mixed bouncer/upstream WHO queries
2143 maskCM := casemapASCII(mask)
2144 if dc.network == nil && maskCM == dc.nickCM {
2145 // TODO: support AWAY (H/G) in self WHO reply
2146 flags := "H"
2147 if dc.user.Admin {
2148 flags += "*"
2149 }
2150 info := whoxInfo{
2151 Token: whoxToken,
2152 Username: dc.user.Username,
2153 Hostname: dc.hostname,
2154 Server: dc.srv.Config().Hostname,
2155 Nickname: dc.nick,
2156 Flags: flags,
2157 Account: dc.user.Username,
2158 Realname: dc.realname,
2159 }
2160 dc.SendMessage(generateWHOXReply(dc.srv.prefix(), dc.nick, fields, &info))
2161 dc.SendMessage(&irc.Message{
2162 Prefix: dc.srv.prefix(),
2163 Command: irc.RPL_ENDOFWHO,
2164 Params: []string{dc.nick, endOfWhoToken, "End of /WHO list"},
2165 })
2166 return nil
2167 }
2168 if maskCM == serviceNickCM {
2169 info := whoxInfo{
2170 Token: whoxToken,
2171 Username: servicePrefix.User,
2172 Hostname: servicePrefix.Host,
2173 Server: dc.srv.Config().Hostname,
2174 Nickname: serviceNick,
2175 Flags: "H*",
2176 Account: serviceNick,
2177 Realname: serviceRealname,
2178 }
2179 dc.SendMessage(generateWHOXReply(dc.srv.prefix(), dc.nick, fields, &info))
2180 dc.SendMessage(&irc.Message{
2181 Prefix: dc.srv.prefix(),
2182 Command: irc.RPL_ENDOFWHO,
2183 Params: []string{dc.nick, endOfWhoToken, "End of /WHO list"},
2184 })
2185 return nil
2186 }
2187
2188 // TODO: properly support WHO masks
2189 uc, upstreamMask, err := dc.unmarshalEntity(mask)
2190 if err != nil {
2191 return err
2192 }
2193
2194 params := []string{upstreamMask}
2195 if options != "" {
2196 params = append(params, options)
2197 }
2198
2199 uc.enqueueCommand(dc, &irc.Message{
2200 Command: "WHO",
2201 Params: params,
2202 })
2203 case "WHOIS":
2204 if len(msg.Params) == 0 {
2205 return ircError{&irc.Message{
2206 Command: irc.ERR_NONICKNAMEGIVEN,
2207 Params: []string{dc.nick, "No nickname given"},
2208 }}
2209 }
2210
2211 var target, mask string
2212 if len(msg.Params) == 1 {
2213 target = ""
2214 mask = msg.Params[0]
2215 } else {
2216 target = msg.Params[0]
2217 mask = msg.Params[1]
2218 }
2219 // TODO: support multiple WHOIS users
2220 if i := strings.IndexByte(mask, ','); i >= 0 {
2221 mask = mask[:i]
2222 }
2223
2224 if dc.network == nil && casemapASCII(mask) == dc.nickCM {
2225 dc.SendMessage(&irc.Message{
2226 Prefix: dc.srv.prefix(),
2227 Command: irc.RPL_WHOISUSER,
2228 Params: []string{dc.nick, dc.nick, dc.user.Username, dc.hostname, "*", dc.realname},
2229 })
2230 dc.SendMessage(&irc.Message{
2231 Prefix: dc.srv.prefix(),
2232 Command: irc.RPL_WHOISSERVER,
2233 Params: []string{dc.nick, dc.nick, dc.srv.Config().Hostname, "soju"},
2234 })
2235 if dc.user.Admin {
2236 dc.SendMessage(&irc.Message{
2237 Prefix: dc.srv.prefix(),
2238 Command: irc.RPL_WHOISOPERATOR,
2239 Params: []string{dc.nick, dc.nick, "is a bouncer administrator"},
2240 })
2241 }
2242 dc.SendMessage(&irc.Message{
2243 Prefix: dc.srv.prefix(),
2244 Command: rpl_whoisaccount,
2245 Params: []string{dc.nick, dc.nick, dc.user.Username, "is logged in as"},
2246 })
2247 dc.SendMessage(&irc.Message{
2248 Prefix: dc.srv.prefix(),
2249 Command: irc.RPL_ENDOFWHOIS,
2250 Params: []string{dc.nick, dc.nick, "End of /WHOIS list"},
2251 })
2252 return nil
2253 }
2254 if casemapASCII(mask) == serviceNickCM {
2255 dc.SendMessage(&irc.Message{
2256 Prefix: dc.srv.prefix(),
2257 Command: irc.RPL_WHOISUSER,
2258 Params: []string{dc.nick, serviceNick, servicePrefix.User, servicePrefix.Host, "*", serviceRealname},
2259 })
2260 dc.SendMessage(&irc.Message{
2261 Prefix: dc.srv.prefix(),
2262 Command: irc.RPL_WHOISSERVER,
2263 Params: []string{dc.nick, serviceNick, dc.srv.Config().Hostname, "soju"},
2264 })
2265 dc.SendMessage(&irc.Message{
2266 Prefix: dc.srv.prefix(),
2267 Command: irc.RPL_WHOISOPERATOR,
2268 Params: []string{dc.nick, serviceNick, "is the bouncer service"},
2269 })
2270 dc.SendMessage(&irc.Message{
2271 Prefix: dc.srv.prefix(),
2272 Command: rpl_whoisaccount,
2273 Params: []string{dc.nick, serviceNick, serviceNick, "is logged in as"},
2274 })
2275 dc.SendMessage(&irc.Message{
2276 Prefix: dc.srv.prefix(),
2277 Command: irc.RPL_ENDOFWHOIS,
2278 Params: []string{dc.nick, serviceNick, "End of /WHOIS list"},
2279 })
2280 return nil
2281 }
2282
2283 // TODO: support WHOIS masks
2284 uc, upstreamNick, err := dc.unmarshalEntity(mask)
2285 if err != nil {
2286 return err
2287 }
2288
2289 var params []string
2290 if target != "" {
2291 if target == mask { // WHOIS nick nick
2292 params = []string{upstreamNick, upstreamNick}
2293 } else {
2294 params = []string{target, upstreamNick}
2295 }
2296 } else {
2297 params = []string{upstreamNick}
2298 }
2299
2300 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
2301 Command: "WHOIS",
2302 Params: params,
2303 })
2304 case "PRIVMSG", "NOTICE":
2305 var targetsStr, text string
2306 if err := parseMessageParams(msg, &targetsStr, &text); err != nil {
2307 return err
2308 }
2309 tags := copyClientTags(msg.Tags)
2310
2311 for _, name := range strings.Split(targetsStr, ",") {
2312 if name == "$"+dc.srv.Config().Hostname || (name == "$*" && dc.network == nil) {
2313 // "$" means a server mask follows. If it's the bouncer's
2314 // hostname, broadcast the message to all bouncer users.
2315 if !dc.user.Admin {
2316 return ircError{&irc.Message{
2317 Prefix: dc.srv.prefix(),
2318 Command: irc.ERR_BADMASK,
2319 Params: []string{dc.nick, name, "Permission denied to broadcast message to all bouncer users"},
2320 }}
2321 }
2322
2323 dc.logger.Printf("broadcasting bouncer-wide %v: %v", msg.Command, text)
2324
2325 broadcastTags := tags.Copy()
2326 broadcastTags["time"] = irc.TagValue(time.Now().UTC().Format(serverTimeLayout))
2327 broadcastMsg := &irc.Message{
2328 Tags: broadcastTags,
2329 Prefix: servicePrefix,
2330 Command: msg.Command,
2331 Params: []string{name, text},
2332 }
2333 dc.srv.forEachUser(func(u *user) {
2334 u.events <- eventBroadcast{broadcastMsg}
2335 })
2336 continue
2337 }
2338
2339 if dc.network == nil && casemapASCII(name) == dc.nickCM {
2340 dc.SendMessage(&irc.Message{
2341 Tags: msg.Tags.Copy(),
2342 Prefix: dc.prefix(),
2343 Command: msg.Command,
2344 Params: []string{name, text},
2345 })
2346 continue
2347 }
2348
2349 if msg.Command == "PRIVMSG" && casemapASCII(name) == serviceNickCM {
2350 if dc.caps["echo-message"] {
2351 echoTags := tags.Copy()
2352 echoTags["time"] = irc.TagValue(time.Now().UTC().Format(serverTimeLayout))
2353 dc.SendMessage(&irc.Message{
2354 Tags: echoTags,
2355 Prefix: dc.prefix(),
2356 Command: msg.Command,
2357 Params: []string{name, text},
2358 })
2359 }
2360 handleServicePRIVMSG(ctx, dc, text)
2361 continue
2362 }
2363
2364 uc, upstreamName, err := dc.unmarshalEntity(name)
2365 if err != nil {
2366 return err
2367 }
2368
2369 if msg.Command == "PRIVMSG" && uc.network.casemap(upstreamName) == "nickserv" {
2370 dc.handleNickServPRIVMSG(ctx, uc, text)
2371 }
2372
2373 unmarshaledText := text
2374 if uc.isChannel(upstreamName) {
2375 unmarshaledText = dc.unmarshalText(uc, text)
2376 }
2377 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
2378 Tags: tags,
2379 Command: msg.Command,
2380 Params: []string{upstreamName, unmarshaledText},
2381 })
2382
2383 echoTags := tags.Copy()
2384 echoTags["time"] = irc.TagValue(time.Now().UTC().Format(serverTimeLayout))
2385 if uc.account != "" {
2386 echoTags["account"] = irc.TagValue(uc.account)
2387 }
2388 echoMsg := &irc.Message{
2389 Tags: echoTags,
2390 Prefix: &irc.Prefix{Name: uc.nick},
2391 Command: msg.Command,
2392 Params: []string{upstreamName, text},
2393 }
2394 uc.produce(upstreamName, echoMsg, dc)
2395
2396 uc.updateChannelAutoDetach(upstreamName)
2397 }
2398 case "TAGMSG":
2399 var targetsStr string
2400 if err := parseMessageParams(msg, &targetsStr); err != nil {
2401 return err
2402 }
2403 tags := copyClientTags(msg.Tags)
2404
2405 for _, name := range strings.Split(targetsStr, ",") {
2406 if dc.network == nil && casemapASCII(name) == dc.nickCM {
2407 dc.SendMessage(&irc.Message{
2408 Tags: msg.Tags.Copy(),
2409 Prefix: dc.prefix(),
2410 Command: "TAGMSG",
2411 Params: []string{name},
2412 })
2413 continue
2414 }
2415
2416 if casemapASCII(name) == serviceNickCM {
2417 continue
2418 }
2419
2420 uc, upstreamName, err := dc.unmarshalEntity(name)
2421 if err != nil {
2422 return err
2423 }
2424 if _, ok := uc.caps["message-tags"]; !ok {
2425 continue
2426 }
2427
2428 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
2429 Tags: tags,
2430 Command: "TAGMSG",
2431 Params: []string{upstreamName},
2432 })
2433
2434 uc.updateChannelAutoDetach(upstreamName)
2435 }
2436 case "INVITE":
2437 var user, channel string
2438 if err := parseMessageParams(msg, &user, &channel); err != nil {
2439 return err
2440 }
2441
2442 ucChannel, upstreamChannel, err := dc.unmarshalEntity(channel)
2443 if err != nil {
2444 return err
2445 }
2446
2447 ucUser, upstreamUser, err := dc.unmarshalEntity(user)
2448 if err != nil {
2449 return err
2450 }
2451
2452 if ucChannel != ucUser {
2453 return ircError{&irc.Message{
2454 Command: irc.ERR_USERNOTINCHANNEL,
2455 Params: []string{dc.nick, user, channel, "They are on another network"},
2456 }}
2457 }
2458 uc := ucChannel
2459
2460 uc.SendMessageLabeled(ctx, dc.id, &irc.Message{
2461 Command: "INVITE",
2462 Params: []string{upstreamUser, upstreamChannel},
2463 })
2464 case "AUTHENTICATE":
2465 // Post-connection-registration AUTHENTICATE is unsupported in
2466 // multi-upstream mode, or if the upstream doesn't support SASL
2467 uc := dc.upstream()
2468 if uc == nil || !uc.caps["sasl"] {
2469 return ircError{&irc.Message{
2470 Command: irc.ERR_SASLFAIL,
2471 Params: []string{dc.nick, "Upstream network authentication not supported"},
2472 }}
2473 }
2474
2475 credentials, err := dc.handleAuthenticateCommand(msg)
2476 if err != nil {
2477 return err
2478 }
2479
2480 if credentials != nil {
2481 if uc.saslClient != nil {
2482 dc.endSASL(&irc.Message{
2483 Prefix: dc.srv.prefix(),
2484 Command: irc.ERR_SASLFAIL,
2485 Params: []string{dc.nick, "Another authentication attempt is already in progress"},
2486 })
2487 return nil
2488 }
2489
2490 uc.logger.Printf("starting post-registration SASL PLAIN authentication with username %q", credentials.plainUsername)
2491 uc.saslClient = sasl.NewPlainClient("", credentials.plainUsername, credentials.plainPassword)
2492 uc.enqueueCommand(dc, &irc.Message{
2493 Command: "AUTHENTICATE",
2494 Params: []string{"PLAIN"},
2495 })
2496 }
2497 case "REGISTER", "VERIFY":
2498 // Check number of params here, since we'll use that to save the
2499 // credentials on command success
2500 if (msg.Command == "REGISTER" && len(msg.Params) < 3) || (msg.Command == "VERIFY" && len(msg.Params) < 2) {
2501 return newNeedMoreParamsError(msg.Command)
2502 }
2503
2504 uc := dc.upstream()
2505 if uc == nil || !uc.caps["draft/account-registration"] {
2506 return ircError{&irc.Message{
2507 Command: "FAIL",
2508 Params: []string{msg.Command, "TEMPORARILY_UNAVAILABLE", "*", "Upstream network account registration not supported"},
2509 }}
2510 }
2511
2512 uc.logger.Printf("starting %v with account name %v", msg.Command, msg.Params[0])
2513 uc.enqueueCommand(dc, msg)
2514 case "MONITOR":
2515 // MONITOR is unsupported in multi-upstream mode
2516 uc := dc.upstream()
2517 if uc == nil {
2518 return newUnknownCommandError(msg.Command)
2519 }
2520 if _, ok := uc.isupport["MONITOR"]; !ok {
2521 return newUnknownCommandError(msg.Command)
2522 }
2523
2524 var subcommand string
2525 if err := parseMessageParams(msg, &subcommand); err != nil {
2526 return err
2527 }
2528
2529 switch strings.ToUpper(subcommand) {
2530 case "+", "-":
2531 var targets string
2532 if err := parseMessageParams(msg, nil, &targets); err != nil {
2533 return err
2534 }
2535 for _, target := range strings.Split(targets, ",") {
2536 if subcommand == "+" {
2537 // Hard limit, just to avoid having downstreams fill our map
2538 if len(dc.monitored.innerMap) >= 1000 {
2539 dc.SendMessage(&irc.Message{
2540 Prefix: dc.srv.prefix(),
2541 Command: irc.ERR_MONLISTFULL,
2542 Params: []string{dc.nick, "1000", target, "Bouncer monitor list is full"},
2543 })
2544 continue
2545 }
2546
2547 dc.monitored.SetValue(target, nil)
2548
2549 if uc.monitored.Has(target) {
2550 cmd := irc.RPL_MONOFFLINE
2551 if online := uc.monitored.Value(target); online {
2552 cmd = irc.RPL_MONONLINE
2553 }
2554
2555 dc.SendMessage(&irc.Message{
2556 Prefix: dc.srv.prefix(),
2557 Command: cmd,
2558 Params: []string{dc.nick, target},
2559 })
2560 }
2561 } else {
2562 dc.monitored.Delete(target)
2563 }
2564 }
2565 uc.updateMonitor()
2566 case "C": // clear
2567 dc.monitored = newCasemapMap(0)
2568 uc.updateMonitor()
2569 case "L": // list
2570 // TODO: be less lazy and pack the list
2571 for _, entry := range dc.monitored.innerMap {
2572 dc.SendMessage(&irc.Message{
2573 Prefix: dc.srv.prefix(),
2574 Command: irc.RPL_MONLIST,
2575 Params: []string{dc.nick, entry.originalKey},
2576 })
2577 }
2578 dc.SendMessage(&irc.Message{
2579 Prefix: dc.srv.prefix(),
2580 Command: irc.RPL_ENDOFMONLIST,
2581 Params: []string{dc.nick, "End of MONITOR list"},
2582 })
2583 case "S": // status
2584 // TODO: be less lazy and pack the lists
2585 for _, entry := range dc.monitored.innerMap {
2586 target := entry.originalKey
2587
2588 cmd := irc.RPL_MONOFFLINE
2589 if online := uc.monitored.Value(target); online {
2590 cmd = irc.RPL_MONONLINE
2591 }
2592
2593 dc.SendMessage(&irc.Message{
2594 Prefix: dc.srv.prefix(),
2595 Command: cmd,
2596 Params: []string{dc.nick, target},
2597 })
2598 }
2599 }
2600 case "CHATHISTORY":
2601 var subcommand string
2602 if err := parseMessageParams(msg, &subcommand); err != nil {
2603 return err
2604 }
2605 var target, limitStr string
2606 var boundsStr [2]string
2607 switch subcommand {
2608 case "AFTER", "BEFORE", "LATEST":
2609 if err := parseMessageParams(msg, nil, &target, &boundsStr[0], &limitStr); err != nil {
2610 return err
2611 }
2612 case "BETWEEN":
2613 if err := parseMessageParams(msg, nil, &target, &boundsStr[0], &boundsStr[1], &limitStr); err != nil {
2614 return err
2615 }
2616 case "TARGETS":
2617 if dc.network == nil {
2618 // Either an unbound bouncer network, in which case we should return no targets,
2619 // or a multi-upstream downstream, but we don't support CHATHISTORY TARGETS for those yet.
2620 dc.SendBatch("draft/chathistory-targets", nil, nil, func(batchRef irc.TagValue) {})
2621 return nil
2622 }
2623 if err := parseMessageParams(msg, nil, &boundsStr[0], &boundsStr[1], &limitStr); err != nil {
2624 return err
2625 }
2626 default:
2627 // TODO: support AROUND
2628 return ircError{&irc.Message{
2629 Command: "FAIL",
2630 Params: []string{"CHATHISTORY", "INVALID_PARAMS", subcommand, "Unknown command"},
2631 }}
2632 }
2633
2634 // We don't save history for our service
2635 if casemapASCII(target) == serviceNickCM {
2636 dc.SendBatch("chathistory", []string{target}, nil, func(batchRef irc.TagValue) {})
2637 return nil
2638 }
2639
2640 store, ok := dc.user.msgStore.(chatHistoryMessageStore)
2641 if !ok {
2642 return ircError{&irc.Message{
2643 Command: irc.ERR_UNKNOWNCOMMAND,
2644 Params: []string{dc.nick, "CHATHISTORY", "Unknown command"},
2645 }}
2646 }
2647
2648 network, entity, err := dc.unmarshalEntityNetwork(target)
2649 if err != nil {
2650 return err
2651 }
2652 entity = network.casemap(entity)
2653
2654 // TODO: support msgid criteria
2655 var bounds [2]time.Time
2656 bounds[0] = parseChatHistoryBound(boundsStr[0])
2657 if subcommand == "LATEST" && boundsStr[0] == "*" {
2658 bounds[0] = time.Now()
2659 } else if bounds[0].IsZero() {
2660 return ircError{&irc.Message{
2661 Command: "FAIL",
2662 Params: []string{"CHATHISTORY", "INVALID_PARAMS", subcommand, boundsStr[0], "Invalid first bound"},
2663 }}
2664 }
2665
2666 if boundsStr[1] != "" {
2667 bounds[1] = parseChatHistoryBound(boundsStr[1])
2668 if bounds[1].IsZero() {
2669 return ircError{&irc.Message{
2670 Command: "FAIL",
2671 Params: []string{"CHATHISTORY", "INVALID_PARAMS", subcommand, boundsStr[1], "Invalid second bound"},
2672 }}
2673 }
2674 }
2675
2676 limit, err := strconv.Atoi(limitStr)
2677 if err != nil || limit < 0 || limit > chatHistoryLimit {
2678 return ircError{&irc.Message{
2679 Command: "FAIL",
2680 Params: []string{"CHATHISTORY", "INVALID_PARAMS", subcommand, limitStr, "Invalid limit"},
2681 }}
2682 }
2683
2684 eventPlayback := dc.caps["draft/event-playback"]
2685
2686 var history []*irc.Message
2687 switch subcommand {
2688 case "BEFORE", "LATEST":
2689 history, err = store.LoadBeforeTime(ctx, &network.Network, entity, bounds[0], time.Time{}, limit, eventPlayback)
2690 case "AFTER":
2691 history, err = store.LoadAfterTime(ctx, &network.Network, entity, bounds[0], time.Now(), limit, eventPlayback)
2692 case "BETWEEN":
2693 if bounds[0].Before(bounds[1]) {
2694 history, err = store.LoadAfterTime(ctx, &network.Network, entity, bounds[0], bounds[1], limit, eventPlayback)
2695 } else {
2696 history, err = store.LoadBeforeTime(ctx, &network.Network, entity, bounds[0], bounds[1], limit, eventPlayback)
2697 }
2698 case "TARGETS":
2699 // TODO: support TARGETS in multi-upstream mode
2700 targets, err := store.ListTargets(ctx, &network.Network, bounds[0], bounds[1], limit, eventPlayback)
2701 if err != nil {
2702 dc.logger.Printf("failed fetching targets for chathistory: %v", err)
2703 return ircError{&irc.Message{
2704 Command: "FAIL",
2705 Params: []string{"CHATHISTORY", "MESSAGE_ERROR", subcommand, "Failed to retrieve targets"},
2706 }}
2707 }
2708
2709 dc.SendBatch("draft/chathistory-targets", nil, nil, func(batchRef irc.TagValue) {
2710 for _, target := range targets {
2711 if ch := network.channels.Value(target.Name); ch != nil && ch.Detached {
2712 continue
2713 }
2714
2715 dc.SendMessage(&irc.Message{
2716 Tags: irc.Tags{"batch": batchRef},
2717 Prefix: dc.srv.prefix(),
2718 Command: "CHATHISTORY",
2719 Params: []string{"TARGETS", target.Name, target.LatestMessage.UTC().Format(serverTimeLayout)},
2720 })
2721 }
2722 })
2723
2724 return nil
2725 }
2726 if err != nil {
2727 dc.logger.Printf("failed fetching %q messages for chathistory: %v", target, err)
2728 return newChatHistoryError(subcommand, target)
2729 }
2730
2731 dc.SendBatch("chathistory", []string{target}, nil, func(batchRef irc.TagValue) {
2732 for _, msg := range history {
2733 msg.Tags["batch"] = batchRef
2734 dc.SendMessage(dc.marshalMessage(msg, network))
2735 }
2736 })
2737 case "BOUNCER":
2738 var subcommand string
2739 if err := parseMessageParams(msg, &subcommand); err != nil {
2740 return err
2741 }
2742
2743 switch strings.ToUpper(subcommand) {
2744 case "BIND":
2745 return ircError{&irc.Message{
2746 Command: "FAIL",
2747 Params: []string{"BOUNCER", "REGISTRATION_IS_COMPLETED", "BIND", "Cannot bind to a network after registration"},
2748 }}
2749 case "LISTNETWORKS":
2750 dc.SendBatch("soju.im/bouncer-networks", nil, nil, func(batchRef irc.TagValue) {
2751 dc.user.forEachNetwork(func(network *network) {
2752 idStr := fmt.Sprintf("%v", network.ID)
2753 attrs := getNetworkAttrs(network)
2754 dc.SendMessage(&irc.Message{
2755 Tags: irc.Tags{"batch": batchRef},
2756 Prefix: dc.srv.prefix(),
2757 Command: "BOUNCER",
2758 Params: []string{"NETWORK", idStr, attrs.String()},
2759 })
2760 })
2761 })
2762 case "ADDNETWORK":
2763 var attrsStr string
2764 if err := parseMessageParams(msg, nil, &attrsStr); err != nil {
2765 return err
2766 }
2767 attrs := irc.ParseTags(attrsStr)
2768
2769 record := &Network{Nick: dc.nick, Enabled: true}
2770 if err := updateNetworkAttrs(record, attrs, subcommand); err != nil {
2771 return err
2772 }
2773
2774 if record.Nick == dc.user.Username {
2775 record.Nick = ""
2776 }
2777 if record.Realname == dc.user.Realname {
2778 record.Realname = ""
2779 }
2780
2781 network, err := dc.user.createNetwork(ctx, record)
2782 if err != nil {
2783 return ircError{&irc.Message{
2784 Command: "FAIL",
2785 Params: []string{"BOUNCER", "UNKNOWN_ERROR", subcommand, fmt.Sprintf("Failed to create network: %v", err)},
2786 }}
2787 }
2788
2789 dc.SendMessage(&irc.Message{
2790 Prefix: dc.srv.prefix(),
2791 Command: "BOUNCER",
2792 Params: []string{"ADDNETWORK", fmt.Sprintf("%v", network.ID)},
2793 })
2794 case "CHANGENETWORK":
2795 var idStr, attrsStr string
2796 if err := parseMessageParams(msg, nil, &idStr, &attrsStr); err != nil {
2797 return err
2798 }
2799 id, err := parseBouncerNetID(subcommand, idStr)
2800 if err != nil {
2801 return err
2802 }
2803 attrs := irc.ParseTags(attrsStr)
2804
2805 net := dc.user.getNetworkByID(id)
2806 if net == nil {
2807 return ircError{&irc.Message{
2808 Command: "FAIL",
2809 Params: []string{"BOUNCER", "INVALID_NETID", subcommand, idStr, "Invalid network ID"},
2810 }}
2811 }
2812
2813 record := net.Network // copy network record because we'll mutate it
2814 if err := updateNetworkAttrs(&record, attrs, subcommand); err != nil {
2815 return err
2816 }
2817
2818 if record.Nick == dc.user.Username {
2819 record.Nick = ""
2820 }
2821 if record.Realname == dc.user.Realname {
2822 record.Realname = ""
2823 }
2824
2825 _, err = dc.user.updateNetwork(ctx, &record)
2826 if err != nil {
2827 return ircError{&irc.Message{
2828 Command: "FAIL",
2829 Params: []string{"BOUNCER", "UNKNOWN_ERROR", subcommand, fmt.Sprintf("Failed to update network: %v", err)},
2830 }}
2831 }
2832
2833 dc.SendMessage(&irc.Message{
2834 Prefix: dc.srv.prefix(),
2835 Command: "BOUNCER",
2836 Params: []string{"CHANGENETWORK", idStr},
2837 })
2838 case "DELNETWORK":
2839 var idStr string
2840 if err := parseMessageParams(msg, nil, &idStr); err != nil {
2841 return err
2842 }
2843 id, err := parseBouncerNetID(subcommand, idStr)
2844 if err != nil {
2845 return err
2846 }
2847
2848 net := dc.user.getNetworkByID(id)
2849 if net == nil {
2850 return ircError{&irc.Message{
2851 Command: "FAIL",
2852 Params: []string{"BOUNCER", "INVALID_NETID", subcommand, idStr, "Invalid network ID"},
2853 }}
2854 }
2855
2856 if err := dc.user.deleteNetwork(ctx, net.ID); err != nil {
2857 return err
2858 }
2859
2860 dc.SendMessage(&irc.Message{
2861 Prefix: dc.srv.prefix(),
2862 Command: "BOUNCER",
2863 Params: []string{"DELNETWORK", idStr},
2864 })
2865 default:
2866 return ircError{&irc.Message{
2867 Command: "FAIL",
2868 Params: []string{"BOUNCER", "UNKNOWN_COMMAND", subcommand, "Unknown subcommand"},
2869 }}
2870 }
2871 default:
2872 dc.logger.Printf("unhandled message: %v", msg)
2873
2874 // Only forward unknown commands in single-upstream mode
2875 uc := dc.upstream()
2876 if uc == nil {
2877 return newUnknownCommandError(msg.Command)
2878 }
2879
2880 uc.SendMessageLabeled(ctx, dc.id, msg)
2881 }
2882 return nil
2883}
2884
2885func (dc *downstreamConn) handleNickServPRIVMSG(ctx context.Context, uc *upstreamConn, text string) {
2886 username, password, ok := parseNickServCredentials(text, uc.nick)
2887 if ok {
2888 uc.network.autoSaveSASLPlain(ctx, username, password)
2889 }
2890}
2891
2892func parseNickServCredentials(text, nick string) (username, password string, ok bool) {
2893 fields := strings.Fields(text)
2894 if len(fields) < 2 {
2895 return "", "", false
2896 }
2897 cmd := strings.ToUpper(fields[0])
2898 params := fields[1:]
2899 switch cmd {
2900 case "REGISTER":
2901 username = nick
2902 password = params[0]
2903 case "IDENTIFY":
2904 if len(params) == 1 {
2905 username = nick
2906 password = params[0]
2907 } else {
2908 username = params[0]
2909 password = params[1]
2910 }
2911 case "SET":
2912 if len(params) == 2 && strings.EqualFold(params[0], "PASSWORD") {
2913 username = nick
2914 password = params[1]
2915 }
2916 default:
2917 return "", "", false
2918 }
2919 return username, password, true
2920}
Note: See TracBrowser for help on using the repository browser.