source: code/trunk/downstream.go@ 726

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

Return more descriptive auth failure errors

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