soju/downstream.go
2020-06-29 18:09:48 +02:00

1703 lines
41 KiB
Go

package soju
import (
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"net"
"strconv"
"strings"
"time"
"github.com/emersion/go-sasl"
"golang.org/x/crypto/bcrypt"
"gopkg.in/irc.v3"
)
type ircError struct {
Message *irc.Message
}
func (err ircError) Error() string {
return err.Message.String()
}
func newUnknownCommandError(cmd string) ircError {
return ircError{&irc.Message{
Command: irc.ERR_UNKNOWNCOMMAND,
Params: []string{
"*",
cmd,
"Unknown command",
},
}}
}
func newNeedMoreParamsError(cmd string) ircError {
return ircError{&irc.Message{
Command: irc.ERR_NEEDMOREPARAMS,
Params: []string{
"*",
cmd,
"Not enough parameters",
},
}}
}
func newChatHistoryError(subcommand string, target string) ircError {
return ircError{&irc.Message{
Command: "FAIL",
Params: []string{"CHATHISTORY", "MESSAGE_ERROR", subcommand, target, "Messages could not be retrieved"},
}}
}
var errAuthFailed = ircError{&irc.Message{
Command: irc.ERR_PASSWDMISMATCH,
Params: []string{"*", "Invalid username or password"},
}}
// permanentDownstreamCaps is the list of always-supported downstream
// capabilities.
var permanentDownstreamCaps = map[string]string{
"batch": "",
"cap-notify": "",
"echo-message": "",
"message-tags": "",
"sasl": "PLAIN",
"server-time": "",
}
// needAllDownstreamCaps is the list of downstream capabilities that
// require support from all upstreams to be enabled
var needAllDownstreamCaps = map[string]string{
"away-notify": "",
"multi-prefix": "",
}
type downstreamConn struct {
conn
id uint64
registered bool
user *user
nick string
rawUsername string
networkName string
clientName string
realname string
hostname string
password string // empty after authentication
network *network // can be nil
negociatingCaps bool
capVersion int
supportedCaps map[string]string
caps map[string]bool
saslServer sasl.Server
}
func newDownstreamConn(srv *Server, ic ircConn, remoteAddr string, id uint64) *downstreamConn {
logger := &prefixLogger{srv.Logger, fmt.Sprintf("downstream %q: ", remoteAddr)}
dc := &downstreamConn{
conn: *newConn(srv, ic, logger),
id: id,
supportedCaps: make(map[string]string),
caps: make(map[string]bool),
}
dc.hostname = remoteAddr
if host, _, err := net.SplitHostPort(dc.hostname); err == nil {
dc.hostname = host
}
for k, v := range permanentDownstreamCaps {
dc.supportedCaps[k] = v
}
if srv.LogPath != "" {
dc.supportedCaps["draft/chathistory"] = ""
}
return dc
}
func (dc *downstreamConn) prefix() *irc.Prefix {
return &irc.Prefix{
Name: dc.nick,
User: dc.user.Username,
Host: dc.hostname,
}
}
func (dc *downstreamConn) forEachNetwork(f func(*network)) {
if dc.network != nil {
f(dc.network)
} else {
dc.user.forEachNetwork(f)
}
}
func (dc *downstreamConn) forEachUpstream(f func(*upstreamConn)) {
dc.user.forEachUpstream(func(uc *upstreamConn) {
if dc.network != nil && uc.network != dc.network {
return
}
f(uc)
})
}
// upstream returns the upstream connection, if any. If there are zero or if
// there are multiple upstream connections, it returns nil.
func (dc *downstreamConn) upstream() *upstreamConn {
if dc.network == nil {
return nil
}
return dc.network.conn
}
func isOurNick(net *network, nick string) bool {
// TODO: this doesn't account for nick changes
if net.conn != nil {
return nick == net.conn.nick
}
// We're not currently connected to the upstream connection, so we don't
// know whether this name is our nickname. Best-effort: use the network's
// configured nickname and hope it was the one being used when we were
// connected.
return nick == net.Nick
}
// marshalEntity converts an upstream entity name (ie. channel or nick) into a
// downstream entity name.
//
// This involves adding a "/<network>" suffix if the entity isn't the current
// user.
func (dc *downstreamConn) marshalEntity(net *network, name string) string {
if isOurNick(net, name) {
return dc.nick
}
if dc.network != nil {
if dc.network != net {
panic("soju: tried to marshal an entity for another network")
}
return name
}
return name + "/" + net.GetName()
}
func (dc *downstreamConn) marshalUserPrefix(net *network, prefix *irc.Prefix) *irc.Prefix {
if isOurNick(net, prefix.Name) {
return dc.prefix()
}
if dc.network != nil {
if dc.network != net {
panic("soju: tried to marshal a user prefix for another network")
}
return prefix
}
return &irc.Prefix{
Name: prefix.Name + "/" + net.GetName(),
User: prefix.User,
Host: prefix.Host,
}
}
// unmarshalEntity converts a downstream entity name (ie. channel or nick) into
// an upstream entity name.
//
// This involves removing the "/<network>" suffix.
func (dc *downstreamConn) unmarshalEntity(name string) (*upstreamConn, string, error) {
if uc := dc.upstream(); uc != nil {
return uc, name, nil
}
var conn *upstreamConn
if i := strings.LastIndexByte(name, '/'); i >= 0 {
network := name[i+1:]
name = name[:i]
dc.forEachUpstream(func(uc *upstreamConn) {
if network != uc.network.GetName() {
return
}
conn = uc
})
}
if conn == nil {
return nil, "", ircError{&irc.Message{
Command: irc.ERR_NOSUCHCHANNEL,
Params: []string{name, "No such channel"},
}}
}
return conn, name, nil
}
func (dc *downstreamConn) unmarshalText(uc *upstreamConn, text string) string {
if dc.upstream() != nil {
return text
}
// TODO: smarter parsing that ignores URLs
return strings.ReplaceAll(text, "/"+uc.network.GetName(), "")
}
func (dc *downstreamConn) readMessages(ch chan<- event) error {
for {
msg, err := dc.ReadMessage()
if err == io.EOF {
break
} else if err != nil {
return fmt.Errorf("failed to read IRC command: %v", err)
}
ch <- eventDownstreamMessage{msg, dc}
}
return nil
}
// SendMessage sends an outgoing message.
//
// This can only called from the user goroutine.
func (dc *downstreamConn) SendMessage(msg *irc.Message) {
if !dc.caps["message-tags"] {
if msg.Command == "TAGMSG" {
return
}
msg = msg.Copy()
for name := range msg.Tags {
supported := false
switch name {
case "time":
supported = dc.caps["server-time"]
}
if !supported {
delete(msg.Tags, name)
}
}
}
dc.conn.SendMessage(msg)
}
// marshalMessage re-formats a message coming from an upstream connection so
// that it's suitable for being sent on this downstream connection. Only
// messages that may appear in logs are supported, except MODE.
func (dc *downstreamConn) marshalMessage(msg *irc.Message, net *network) *irc.Message {
msg = msg.Copy()
msg.Prefix = dc.marshalUserPrefix(net, msg.Prefix)
switch msg.Command {
case "PRIVMSG", "NOTICE", "TAGMSG":
msg.Params[0] = dc.marshalEntity(net, msg.Params[0])
case "NICK":
// Nick change for another user
msg.Params[0] = dc.marshalEntity(net, msg.Params[0])
case "JOIN", "PART":
msg.Params[0] = dc.marshalEntity(net, msg.Params[0])
case "KICK":
msg.Params[0] = dc.marshalEntity(net, msg.Params[0])
msg.Params[1] = dc.marshalEntity(net, msg.Params[1])
case "TOPIC":
msg.Params[0] = dc.marshalEntity(net, msg.Params[0])
case "QUIT":
// This space is intentionally left blank
default:
panic(fmt.Sprintf("unexpected %q message", msg.Command))
}
return msg
}
func (dc *downstreamConn) handleMessage(msg *irc.Message) error {
switch msg.Command {
case "QUIT":
return dc.Close()
default:
if dc.registered {
return dc.handleMessageRegistered(msg)
} else {
return dc.handleMessageUnregistered(msg)
}
}
}
func (dc *downstreamConn) handleMessageUnregistered(msg *irc.Message) error {
switch msg.Command {
case "NICK":
var nick string
if err := parseMessageParams(msg, &nick); err != nil {
return err
}
if nick == serviceNick {
return ircError{&irc.Message{
Command: irc.ERR_NICKNAMEINUSE,
Params: []string{dc.nick, nick, "Nickname reserved for bouncer service"},
}}
}
dc.nick = nick
case "USER":
if err := parseMessageParams(msg, &dc.rawUsername, nil, nil, &dc.realname); err != nil {
return err
}
case "PASS":
if err := parseMessageParams(msg, &dc.password); err != nil {
return err
}
case "CAP":
var subCmd string
if err := parseMessageParams(msg, &subCmd); err != nil {
return err
}
if err := dc.handleCapCommand(subCmd, msg.Params[1:]); err != nil {
return err
}
case "AUTHENTICATE":
if !dc.caps["sasl"] {
return ircError{&irc.Message{
Command: irc.ERR_SASLFAIL,
Params: []string{"*", "AUTHENTICATE requires the \"sasl\" capability to be enabled"},
}}
}
if len(msg.Params) == 0 {
return ircError{&irc.Message{
Command: irc.ERR_SASLFAIL,
Params: []string{"*", "Missing AUTHENTICATE argument"},
}}
}
if dc.nick == "" {
return ircError{&irc.Message{
Command: irc.ERR_SASLFAIL,
Params: []string{"*", "Expected NICK command before AUTHENTICATE"},
}}
}
var resp []byte
if dc.saslServer == nil {
mech := strings.ToUpper(msg.Params[0])
switch mech {
case "PLAIN":
dc.saslServer = sasl.NewPlainServer(sasl.PlainAuthenticator(func(identity, username, password string) error {
return dc.authenticate(username, password)
}))
default:
return ircError{&irc.Message{
Command: irc.ERR_SASLFAIL,
Params: []string{"*", fmt.Sprintf("Unsupported SASL mechanism %q", mech)},
}}
}
} else if msg.Params[0] == "*" {
dc.saslServer = nil
return ircError{&irc.Message{
Command: irc.ERR_SASLABORTED,
Params: []string{"*", "SASL authentication aborted"},
}}
} else if msg.Params[0] == "+" {
resp = nil
} else {
// TODO: multi-line messages
var err error
resp, err = base64.StdEncoding.DecodeString(msg.Params[0])
if err != nil {
dc.saslServer = nil
return ircError{&irc.Message{
Command: irc.ERR_SASLFAIL,
Params: []string{"*", "Invalid base64-encoded response"},
}}
}
}
challenge, done, err := dc.saslServer.Next(resp)
if err != nil {
dc.saslServer = nil
if ircErr, ok := err.(ircError); ok && ircErr.Message.Command == irc.ERR_PASSWDMISMATCH {
return ircError{&irc.Message{
Command: irc.ERR_SASLFAIL,
Params: []string{"*", ircErr.Message.Params[1]},
}}
}
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.ERR_SASLFAIL,
Params: []string{"*", "SASL error"},
})
return fmt.Errorf("SASL authentication failed: %v", err)
} else if done {
dc.saslServer = nil
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_LOGGEDIN,
Params: []string{dc.nick, dc.prefix().String(), dc.user.Username, "You are now logged in"},
})
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_SASLSUCCESS,
Params: []string{dc.nick, "SASL authentication successful"},
})
} else {
challengeStr := "+"
if len(challenge) > 0 {
challengeStr = base64.StdEncoding.EncodeToString(challenge)
}
// TODO: multi-line messages
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "AUTHENTICATE",
Params: []string{challengeStr},
})
}
default:
dc.logger.Printf("unhandled message: %v", msg)
return newUnknownCommandError(msg.Command)
}
if dc.rawUsername != "" && dc.nick != "" && !dc.negociatingCaps {
return dc.register()
}
return nil
}
func (dc *downstreamConn) handleCapCommand(cmd string, args []string) error {
cmd = strings.ToUpper(cmd)
replyTo := dc.nick
if !dc.registered {
replyTo = "*"
}
switch cmd {
case "LS":
if len(args) > 0 {
var err error
if dc.capVersion, err = strconv.Atoi(args[0]); err != nil {
return err
}
}
caps := make([]string, 0, len(dc.supportedCaps))
for k, v := range dc.supportedCaps {
if dc.capVersion >= 302 && v != "" {
caps = append(caps, k+"="+v)
} else {
caps = append(caps, k)
}
}
// TODO: multi-line replies
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "CAP",
Params: []string{replyTo, "LS", strings.Join(caps, " ")},
})
if dc.capVersion >= 302 {
// CAP version 302 implicitly enables cap-notify
dc.caps["cap-notify"] = true
}
if !dc.registered {
dc.negociatingCaps = true
}
case "LIST":
var caps []string
for name := range dc.caps {
caps = append(caps, name)
}
// TODO: multi-line replies
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "CAP",
Params: []string{replyTo, "LIST", strings.Join(caps, " ")},
})
case "REQ":
if len(args) == 0 {
return ircError{&irc.Message{
Command: err_invalidcapcmd,
Params: []string{replyTo, cmd, "Missing argument in CAP REQ command"},
}}
}
// TODO: atomically ack/nak the whole capability set
caps := strings.Fields(args[0])
ack := true
for _, name := range caps {
name = strings.ToLower(name)
enable := !strings.HasPrefix(name, "-")
if !enable {
name = strings.TrimPrefix(name, "-")
}
if enable == dc.caps[name] {
continue
}
_, ok := dc.supportedCaps[name]
if !ok {
ack = false
break
}
if name == "cap-notify" && dc.capVersion >= 302 && !enable {
// cap-notify cannot be disabled with CAP version 302
ack = false
break
}
dc.caps[name] = enable
}
reply := "NAK"
if ack {
reply = "ACK"
}
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "CAP",
Params: []string{replyTo, reply, args[0]},
})
case "END":
dc.negociatingCaps = false
default:
return ircError{&irc.Message{
Command: err_invalidcapcmd,
Params: []string{replyTo, cmd, "Unknown CAP command"},
}}
}
return nil
}
func (dc *downstreamConn) setSupportedCap(name, value string) {
prevValue, hasPrev := dc.supportedCaps[name]
changed := !hasPrev || prevValue != value
dc.supportedCaps[name] = value
if !dc.caps["cap-notify"] || !changed {
return
}
replyTo := dc.nick
if !dc.registered {
replyTo = "*"
}
cap := name
if value != "" && dc.capVersion >= 302 {
cap = name + "=" + value
}
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "CAP",
Params: []string{replyTo, "NEW", cap},
})
}
func (dc *downstreamConn) unsetSupportedCap(name string) {
_, hasPrev := dc.supportedCaps[name]
delete(dc.supportedCaps, name)
delete(dc.caps, name)
if !dc.caps["cap-notify"] || !hasPrev {
return
}
replyTo := dc.nick
if !dc.registered {
replyTo = "*"
}
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "CAP",
Params: []string{replyTo, "DEL", name},
})
}
func (dc *downstreamConn) updateSupportedCaps() {
supportedCaps := make(map[string]bool)
for cap := range needAllDownstreamCaps {
supportedCaps[cap] = true
}
dc.forEachUpstream(func(uc *upstreamConn) {
for cap, supported := range supportedCaps {
supportedCaps[cap] = supported && uc.caps[cap]
}
})
for cap, supported := range supportedCaps {
if supported {
dc.setSupportedCap(cap, needAllDownstreamCaps[cap])
} else {
dc.unsetSupportedCap(cap)
}
}
}
func (dc *downstreamConn) updateNick() {
if uc := dc.upstream(); uc != nil && uc.nick != dc.nick {
dc.SendMessage(&irc.Message{
Prefix: dc.prefix(),
Command: "NICK",
Params: []string{uc.nick},
})
dc.nick = uc.nick
}
}
func sanityCheckServer(addr string) error {
dialer := net.Dialer{Timeout: 30 * time.Second}
conn, err := tls.DialWithDialer(&dialer, "tcp", addr, nil)
if err != nil {
return err
}
return conn.Close()
}
func unmarshalUsername(rawUsername string) (username, client, network string) {
username = rawUsername
i := strings.IndexAny(username, "/@")
j := strings.LastIndexAny(username, "/@")
if i >= 0 {
username = rawUsername[:i]
}
if j >= 0 {
if rawUsername[j] == '@' {
client = rawUsername[j+1:]
} else {
network = rawUsername[j+1:]
}
}
if i >= 0 && j >= 0 && i < j {
if rawUsername[i] == '@' {
client = rawUsername[i+1 : j]
} else {
network = rawUsername[i+1 : j]
}
}
return username, client, network
}
func (dc *downstreamConn) authenticate(username, password string) error {
username, clientName, networkName := unmarshalUsername(username)
u, err := dc.srv.db.GetUser(username)
if err != nil {
dc.logger.Printf("failed authentication for %q: %v", username, err)
return errAuthFailed
}
// Password auth disabled
if u.Password == "" {
return errAuthFailed
}
err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
if err != nil {
dc.logger.Printf("failed authentication for %q: %v", username, err)
return errAuthFailed
}
dc.user = dc.srv.getUser(username)
if dc.user == nil {
dc.logger.Printf("failed authentication for %q: user not active", username)
return errAuthFailed
}
dc.clientName = clientName
dc.networkName = networkName
return nil
}
func (dc *downstreamConn) register() error {
if dc.registered {
return fmt.Errorf("tried to register twice")
}
password := dc.password
dc.password = ""
if dc.user == nil {
if err := dc.authenticate(dc.rawUsername, password); err != nil {
return err
}
}
if dc.clientName == "" && dc.networkName == "" {
_, dc.clientName, dc.networkName = unmarshalUsername(dc.rawUsername)
}
dc.registered = true
dc.logger.Printf("registration complete for user %q", dc.user.Username)
return nil
}
func (dc *downstreamConn) loadNetwork() error {
if dc.networkName == "" {
return nil
}
network := dc.user.getNetwork(dc.networkName)
if network == nil {
addr := dc.networkName
if !strings.ContainsRune(addr, ':') {
addr = addr + ":6697"
}
dc.logger.Printf("trying to connect to new network %q", addr)
if err := sanityCheckServer(addr); err != nil {
dc.logger.Printf("failed to connect to %q: %v", addr, err)
return ircError{&irc.Message{
Command: irc.ERR_PASSWDMISMATCH,
Params: []string{"*", fmt.Sprintf("Failed to connect to %q", dc.networkName)},
}}
}
dc.logger.Printf("auto-saving network %q", dc.networkName)
var err error
network, err = dc.user.createNetwork(&Network{
Addr: dc.networkName,
Nick: dc.nick,
})
if err != nil {
return err
}
}
dc.network = network
return nil
}
func (dc *downstreamConn) welcome() error {
if dc.user == nil || !dc.registered {
panic("tried to welcome an unregistered connection")
}
// TODO: doing this might take some time. We should do it in dc.register
// instead, but we'll potentially be adding a new network and this must be
// done in the user goroutine.
if err := dc.loadNetwork(); err != nil {
return err
}
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_WELCOME,
Params: []string{dc.nick, "Welcome to soju, " + dc.nick},
})
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_YOURHOST,
Params: []string{dc.nick, "Your host is " + dc.srv.Hostname},
})
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_CREATED,
Params: []string{dc.nick, "Who cares when the server was created?"},
})
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_MYINFO,
Params: []string{dc.nick, dc.srv.Hostname, "soju", "aiwroO", "OovaimnqpsrtklbeI"},
})
// TODO: RPL_ISUPPORT
// TODO: send CHATHISTORY in RPL_ISUPPORT when implemented
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.ERR_NOMOTD,
Params: []string{dc.nick, "No MOTD"},
})
dc.updateNick()
dc.forEachUpstream(func(uc *upstreamConn) {
for _, ch := range uc.channels {
if !ch.complete {
continue
}
if record, ok := uc.network.channels[ch.Name]; ok && record.Detached {
continue
}
dc.SendMessage(&irc.Message{
Prefix: dc.prefix(),
Command: "JOIN",
Params: []string{dc.marshalEntity(ch.conn.network, ch.Name)},
})
forwardChannel(dc, ch)
}
})
dc.forEachNetwork(func(net *network) {
// Only send history if we're the first connected client with that name
// for the network
if _, ok := net.offlineClients[dc.clientName]; ok {
dc.sendNetworkHistory(net)
delete(net.offlineClients, dc.clientName)
}
})
return nil
}
func (dc *downstreamConn) sendNetworkHistory(net *network) {
if dc.caps["draft/chathistory"] {
return
}
for target, history := range net.history {
if ch, ok := net.channels[target]; ok && ch.Detached {
continue
}
seq, ok := history.offlineClients[dc.clientName]
if !ok {
continue
}
delete(history.offlineClients, dc.clientName)
// If all clients have received history, no need to keep the
// ring buffer around
if len(history.offlineClients) == 0 {
delete(net.history, target)
}
consumer := history.ring.NewConsumer(seq)
batchRef := "history"
if dc.caps["batch"] {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "BATCH",
Params: []string{"+" + batchRef, "chathistory", dc.marshalEntity(net, target)},
})
}
for {
msg := consumer.Consume()
if msg == nil {
break
}
// Don't replay all messages, because that would mess up client
// state. For instance we just sent the list of users, sending
// PART messages for one of these users would be incorrect.
ignore := true
switch msg.Command {
case "PRIVMSG", "NOTICE":
ignore = false
}
if ignore {
continue
}
if dc.caps["batch"] {
msg = msg.Copy()
msg.Tags["batch"] = irc.TagValue(batchRef)
}
dc.SendMessage(dc.marshalMessage(msg, net))
}
if dc.caps["batch"] {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "BATCH",
Params: []string{"-" + batchRef},
})
}
}
}
func (dc *downstreamConn) runUntilRegistered() error {
for !dc.registered {
msg, err := dc.ReadMessage()
if err != nil {
return fmt.Errorf("failed to read IRC command: %v", err)
}
err = dc.handleMessage(msg)
if ircErr, ok := err.(ircError); ok {
ircErr.Message.Prefix = dc.srv.prefix()
dc.SendMessage(ircErr.Message)
} else if err != nil {
return fmt.Errorf("failed to handle IRC command %q: %v", msg, err)
}
}
return nil
}
func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
switch msg.Command {
case "CAP":
var subCmd string
if err := parseMessageParams(msg, &subCmd); err != nil {
return err
}
if err := dc.handleCapCommand(subCmd, msg.Params[1:]); err != nil {
return err
}
case "PING":
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "PONG",
Params: msg.Params,
})
return nil
case "USER":
return ircError{&irc.Message{
Command: irc.ERR_ALREADYREGISTERED,
Params: []string{dc.nick, "You may not reregister"},
}}
case "NICK":
var nick string
if err := parseMessageParams(msg, &nick); err != nil {
return err
}
var upstream *upstreamConn
if dc.upstream() == nil {
uc, unmarshaledNick, err := dc.unmarshalEntity(nick)
if err == nil { // NICK nick/network: NICK only on a specific upstream
upstream = uc
nick = unmarshaledNick
}
}
var err error
dc.forEachNetwork(func(n *network) {
if err != nil || (upstream != nil && upstream.network != n) {
return
}
n.Nick = nick
err = dc.srv.db.StoreNetwork(dc.user.Username, &n.Network)
})
if err != nil {
return err
}
dc.forEachUpstream(func(uc *upstreamConn) {
if upstream != nil && upstream != uc {
return
}
uc.SendMessageLabeled(dc.id, &irc.Message{
Command: "NICK",
Params: []string{nick},
})
})
if dc.upstream() == nil && dc.nick != nick {
dc.SendMessage(&irc.Message{
Prefix: dc.prefix(),
Command: "NICK",
Params: []string{nick},
})
dc.nick = nick
}
case "JOIN":
var namesStr string
if err := parseMessageParams(msg, &namesStr); err != nil {
return err
}
var keys []string
if len(msg.Params) > 1 {
keys = strings.Split(msg.Params[1], ",")
}
for i, name := range strings.Split(namesStr, ",") {
uc, upstreamName, err := dc.unmarshalEntity(name)
if err != nil {
return err
}
var key string
if len(keys) > i {
key = keys[i]
}
params := []string{upstreamName}
if key != "" {
params = append(params, key)
}
uc.SendMessageLabeled(dc.id, &irc.Message{
Command: "JOIN",
Params: params,
})
ch := &Channel{Name: upstreamName, Key: key, Detached: false}
if current, ok := uc.network.channels[ch.Name]; ok && key == "" {
// Don't clear the channel key if there's one set
// TODO: add a way to unset the channel key
ch.Key = current.Key
}
if err := uc.network.createUpdateChannel(ch); err != nil {
dc.logger.Printf("failed to create or update channel %q: %v", upstreamName, err)
}
}
case "PART":
var namesStr string
if err := parseMessageParams(msg, &namesStr); err != nil {
return err
}
var reason string
if len(msg.Params) > 1 {
reason = msg.Params[1]
}
for _, name := range strings.Split(namesStr, ",") {
uc, upstreamName, err := dc.unmarshalEntity(name)
if err != nil {
return err
}
if strings.EqualFold(reason, "detach") {
ch := &Channel{Name: upstreamName, Detached: true}
if err := uc.network.createUpdateChannel(ch); err != nil {
dc.logger.Printf("failed to detach channel %q: %v", upstreamName, err)
}
} else {
params := []string{upstreamName}
if reason != "" {
params = append(params, reason)
}
uc.SendMessageLabeled(dc.id, &irc.Message{
Command: "PART",
Params: params,
})
if err := uc.network.deleteChannel(upstreamName); err != nil {
dc.logger.Printf("failed to delete channel %q: %v", upstreamName, err)
}
}
}
case "KICK":
var channelStr, userStr string
if err := parseMessageParams(msg, &channelStr, &userStr); err != nil {
return err
}
channels := strings.Split(channelStr, ",")
users := strings.Split(userStr, ",")
var reason string
if len(msg.Params) > 2 {
reason = msg.Params[2]
}
if len(channels) != 1 && len(channels) != len(users) {
return ircError{&irc.Message{
Command: irc.ERR_BADCHANMASK,
Params: []string{dc.nick, channelStr, "Bad channel mask"},
}}
}
for i, user := range users {
var channel string
if len(channels) == 1 {
channel = channels[0]
} else {
channel = channels[i]
}
ucChannel, upstreamChannel, err := dc.unmarshalEntity(channel)
if err != nil {
return err
}
ucUser, upstreamUser, err := dc.unmarshalEntity(user)
if err != nil {
return err
}
if ucChannel != ucUser {
return ircError{&irc.Message{
Command: irc.ERR_USERNOTINCHANNEL,
Params: []string{dc.nick, user, channel, "They aren't on that channel"},
}}
}
uc := ucChannel
params := []string{upstreamChannel, upstreamUser}
if reason != "" {
params = append(params, reason)
}
uc.SendMessageLabeled(dc.id, &irc.Message{
Command: "KICK",
Params: params,
})
}
case "MODE":
var name string
if err := parseMessageParams(msg, &name); err != nil {
return err
}
var modeStr string
if len(msg.Params) > 1 {
modeStr = msg.Params[1]
}
if name == dc.nick {
if modeStr != "" {
dc.forEachUpstream(func(uc *upstreamConn) {
uc.SendMessageLabeled(dc.id, &irc.Message{
Command: "MODE",
Params: []string{uc.nick, modeStr},
})
})
} else {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_UMODEIS,
Params: []string{dc.nick, ""}, // TODO
})
}
return nil
}
uc, upstreamName, err := dc.unmarshalEntity(name)
if err != nil {
return err
}
if !uc.isChannel(upstreamName) {
return ircError{&irc.Message{
Command: irc.ERR_USERSDONTMATCH,
Params: []string{dc.nick, "Cannot change mode for other users"},
}}
}
if modeStr != "" {
params := []string{upstreamName, modeStr}
params = append(params, msg.Params[2:]...)
uc.SendMessageLabeled(dc.id, &irc.Message{
Command: "MODE",
Params: params,
})
} else {
ch, ok := uc.channels[upstreamName]
if !ok {
return ircError{&irc.Message{
Command: irc.ERR_NOSUCHCHANNEL,
Params: []string{dc.nick, name, "No such channel"},
}}
}
if ch.modes == nil {
// we haven't received the initial RPL_CHANNELMODEIS yet
// ignore the request, we will broadcast the modes later when we receive RPL_CHANNELMODEIS
return nil
}
modeStr, modeParams := ch.modes.Format()
params := []string{dc.nick, name, modeStr}
params = append(params, modeParams...)
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_CHANNELMODEIS,
Params: params,
})
if ch.creationTime != "" {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: rpl_creationtime,
Params: []string{dc.nick, name, ch.creationTime},
})
}
}
case "TOPIC":
var channel string
if err := parseMessageParams(msg, &channel); err != nil {
return err
}
uc, upstreamChannel, err := dc.unmarshalEntity(channel)
if err != nil {
return err
}
if len(msg.Params) > 1 { // setting topic
topic := msg.Params[1]
uc.SendMessageLabeled(dc.id, &irc.Message{
Command: "TOPIC",
Params: []string{upstreamChannel, topic},
})
} else { // getting topic
ch, ok := uc.channels[upstreamChannel]
if !ok {
return ircError{&irc.Message{
Command: irc.ERR_NOSUCHCHANNEL,
Params: []string{dc.nick, upstreamChannel, "No such channel"},
}}
}
sendTopic(dc, ch)
}
case "LIST":
// TODO: support ELIST when supported by all upstreams
pl := pendingLIST{
downstreamID: dc.id,
pendingCommands: make(map[int64]*irc.Message),
}
var upstream *upstreamConn
var upstreamChannels map[int64][]string
if len(msg.Params) > 0 {
uc, upstreamMask, err := dc.unmarshalEntity(msg.Params[0])
if err == nil && upstreamMask == "*" { // LIST */network: send LIST only to one network
upstream = uc
} else {
upstreamChannels = make(map[int64][]string)
channels := strings.Split(msg.Params[0], ",")
for _, channel := range channels {
uc, upstreamChannel, err := dc.unmarshalEntity(channel)
if err != nil {
return err
}
upstreamChannels[uc.network.ID] = append(upstreamChannels[uc.network.ID], upstreamChannel)
}
}
}
dc.user.pendingLISTs = append(dc.user.pendingLISTs, pl)
dc.forEachUpstream(func(uc *upstreamConn) {
if upstream != nil && upstream != uc {
return
}
var params []string
if upstreamChannels != nil {
if channels, ok := upstreamChannels[uc.network.ID]; ok {
params = []string{strings.Join(channels, ",")}
} else {
return
}
}
pl.pendingCommands[uc.network.ID] = &irc.Message{
Command: "LIST",
Params: params,
}
uc.trySendLIST(dc.id)
})
case "NAMES":
if len(msg.Params) == 0 {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_ENDOFNAMES,
Params: []string{dc.nick, "*", "End of /NAMES list"},
})
return nil
}
channels := strings.Split(msg.Params[0], ",")
for _, channel := range channels {
uc, upstreamChannel, err := dc.unmarshalEntity(channel)
if err != nil {
return err
}
ch, ok := uc.channels[upstreamChannel]
if ok {
sendNames(dc, ch)
} else {
// NAMES on a channel we have not joined, ask upstream
uc.SendMessageLabeled(dc.id, &irc.Message{
Command: "NAMES",
Params: []string{upstreamChannel},
})
}
}
case "WHO":
if len(msg.Params) == 0 {
// TODO: support WHO without parameters
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_ENDOFWHO,
Params: []string{dc.nick, "*", "End of /WHO list"},
})
return nil
}
// TODO: support WHO masks
entity := msg.Params[0]
if entity == dc.nick {
// TODO: support AWAY (H/G) in self WHO reply
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_WHOREPLY,
Params: []string{dc.nick, "*", dc.user.Username, dc.hostname, dc.srv.Hostname, dc.nick, "H", "0 " + dc.realname},
})
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_ENDOFWHO,
Params: []string{dc.nick, dc.nick, "End of /WHO list"},
})
return nil
}
if entity == serviceNick {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_WHOREPLY,
Params: []string{serviceNick, "*", servicePrefix.User, servicePrefix.Host, dc.srv.Hostname, serviceNick, "H", "0 " + serviceRealname},
})
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_ENDOFWHO,
Params: []string{dc.nick, serviceNick, "End of /WHO list"},
})
return nil
}
uc, upstreamName, err := dc.unmarshalEntity(entity)
if err != nil {
return err
}
var params []string
if len(msg.Params) == 2 {
params = []string{upstreamName, msg.Params[1]}
} else {
params = []string{upstreamName}
}
uc.SendMessageLabeled(dc.id, &irc.Message{
Command: "WHO",
Params: params,
})
case "WHOIS":
if len(msg.Params) == 0 {
return ircError{&irc.Message{
Command: irc.ERR_NONICKNAMEGIVEN,
Params: []string{dc.nick, "No nickname given"},
}}
}
var target, mask string
if len(msg.Params) == 1 {
target = ""
mask = msg.Params[0]
} else {
target = msg.Params[0]
mask = msg.Params[1]
}
// TODO: support multiple WHOIS users
if i := strings.IndexByte(mask, ','); i >= 0 {
mask = mask[:i]
}
if mask == dc.nick {
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_WHOISUSER,
Params: []string{dc.nick, dc.nick, dc.user.Username, dc.hostname, "*", dc.realname},
})
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_WHOISSERVER,
Params: []string{dc.nick, dc.nick, dc.srv.Hostname, "soju"},
})
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.RPL_ENDOFWHOIS,
Params: []string{dc.nick, dc.nick, "End of /WHOIS list"},
})
return nil
}
// TODO: support WHOIS masks
uc, upstreamNick, err := dc.unmarshalEntity(mask)
if err != nil {
return err
}
var params []string
if target != "" {
if target == mask { // WHOIS nick nick
params = []string{upstreamNick, upstreamNick}
} else {
params = []string{target, upstreamNick}
}
} else {
params = []string{upstreamNick}
}
uc.SendMessageLabeled(dc.id, &irc.Message{
Command: "WHOIS",
Params: params,
})
case "PRIVMSG":
var targetsStr, text string
if err := parseMessageParams(msg, &targetsStr, &text); err != nil {
return err
}
tags := copyClientTags(msg.Tags)
for _, name := range strings.Split(targetsStr, ",") {
if name == serviceNick {
handleServicePRIVMSG(dc, text)
continue
}
uc, upstreamName, err := dc.unmarshalEntity(name)
if err != nil {
return err
}
if upstreamName == "NickServ" {
dc.handleNickServPRIVMSG(uc, text)
}
unmarshaledText := text
if uc.isChannel(upstreamName) {
unmarshaledText = dc.unmarshalText(uc, text)
}
uc.SendMessageLabeled(dc.id, &irc.Message{
Tags: tags,
Command: "PRIVMSG",
Params: []string{upstreamName, unmarshaledText},
})
echoTags := tags.Copy()
echoTags["time"] = irc.TagValue(time.Now().UTC().Format(serverTimeLayout))
echoMsg := &irc.Message{
Tags: echoTags,
Prefix: &irc.Prefix{
Name: uc.nick,
User: uc.username,
},
Command: "PRIVMSG",
Params: []string{upstreamName, text},
}
uc.produce(upstreamName, echoMsg, dc)
}
case "NOTICE":
var targetsStr, text string
if err := parseMessageParams(msg, &targetsStr, &text); err != nil {
return err
}
tags := copyClientTags(msg.Tags)
for _, name := range strings.Split(targetsStr, ",") {
uc, upstreamName, err := dc.unmarshalEntity(name)
if err != nil {
return err
}
unmarshaledText := text
if uc.isChannel(upstreamName) {
unmarshaledText = dc.unmarshalText(uc, text)
}
uc.SendMessageLabeled(dc.id, &irc.Message{
Tags: tags,
Command: "NOTICE",
Params: []string{upstreamName, unmarshaledText},
})
}
case "TAGMSG":
var targetsStr string
if err := parseMessageParams(msg, &targetsStr); err != nil {
return err
}
tags := copyClientTags(msg.Tags)
for _, name := range strings.Split(targetsStr, ",") {
uc, upstreamName, err := dc.unmarshalEntity(name)
if err != nil {
return err
}
uc.SendMessageLabeled(dc.id, &irc.Message{
Tags: tags,
Command: "TAGMSG",
Params: []string{upstreamName},
})
}
case "INVITE":
var user, channel string
if err := parseMessageParams(msg, &user, &channel); err != nil {
return err
}
ucChannel, upstreamChannel, err := dc.unmarshalEntity(channel)
if err != nil {
return err
}
ucUser, upstreamUser, err := dc.unmarshalEntity(user)
if err != nil {
return err
}
if ucChannel != ucUser {
return ircError{&irc.Message{
Command: irc.ERR_USERNOTINCHANNEL,
Params: []string{dc.nick, user, channel, "They aren't on that channel"},
}}
}
uc := ucChannel
uc.SendMessageLabeled(dc.id, &irc.Message{
Command: "INVITE",
Params: []string{upstreamUser, upstreamChannel},
})
case "CHATHISTORY":
var subcommand string
if err := parseMessageParams(msg, &subcommand); err != nil {
return err
}
var target, criteria, limitStr string
if err := parseMessageParams(msg, nil, &target, &criteria, &limitStr); err != nil {
return ircError{&irc.Message{
Command: "FAIL",
Params: []string{"CHATHISTORY", "NEED_MORE_PARAMS", subcommand, "Missing parameters"},
}}
}
if dc.srv.LogPath == "" {
return ircError{&irc.Message{
Command: irc.ERR_UNKNOWNCOMMAND,
Params: []string{dc.nick, subcommand, "Unknown command"},
}}
}
uc, entity, err := dc.unmarshalEntity(target)
if err != nil {
return err
}
// TODO: support msgid criteria
criteriaParts := strings.SplitN(criteria, "=", 2)
if len(criteriaParts) != 2 || criteriaParts[0] != "timestamp" {
return ircError{&irc.Message{
Command: "FAIL",
Params: []string{"CHATHISTORY", "UNKNOWN_CRITERIA", criteria, "Unknown criteria"},
}}
}
timestamp, err := time.Parse(serverTimeLayout, criteriaParts[1])
if err != nil {
return ircError{&irc.Message{
Command: "FAIL",
Params: []string{"CHATHISTORY", "INVALID_CRITERIA", criteria, "Invalid criteria"},
}}
}
limit, err := strconv.Atoi(limitStr)
if err != nil || limit < 0 || limit > dc.srv.HistoryLimit {
return ircError{&irc.Message{
Command: "FAIL",
Params: []string{"CHATHISTORY", "INVALID_LIMIT", limitStr, "Invalid limit"},
}}
}
switch subcommand {
case "BEFORE":
batchRef := "history"
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "BATCH",
Params: []string{"+" + batchRef, "chathistory", target},
})
history := make([]*irc.Message, limit)
remaining := limit
tries := 0
for remaining > 0 {
buf, err := parseMessagesBefore(uc.network, entity, timestamp, remaining)
if err != nil {
dc.logger.Printf("failed parsing log messages for chathistory: %v", err)
return newChatHistoryError(subcommand, target)
}
if len(buf) == 0 {
tries++
if tries >= 100 {
break
}
} else {
tries = 0
}
copy(history[remaining-len(buf):], buf)
remaining -= len(buf)
year, month, day := timestamp.Date()
timestamp = time.Date(year, month, day, 0, 0, 0, 0, timestamp.Location()).Add(-1)
}
for _, m := range history[remaining:] {
m.Tags["batch"] = irc.TagValue(batchRef)
dc.SendMessage(dc.marshalMessage(m, uc.network))
}
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "BATCH",
Params: []string{"-" + batchRef},
})
default:
// TODO: support AFTER, LATEST, BETWEEN
return ircError{&irc.Message{
Command: "FAIL",
Params: []string{"CHATHISTORY", "UNKNOWN_COMMAND", subcommand, "Unknown command"},
}}
}
default:
dc.logger.Printf("unhandled message: %v", msg)
return newUnknownCommandError(msg.Command)
}
return nil
}
func (dc *downstreamConn) handleNickServPRIVMSG(uc *upstreamConn, text string) {
username, password, ok := parseNickServCredentials(text, uc.nick)
if !ok {
return
}
// User may have e.g. EXTERNAL mechanism configured. We do not want to
// automatically erase the key pair or any other credentials.
if uc.network.SASL.Mechanism != "" && uc.network.SASL.Mechanism != "PLAIN" {
return
}
dc.logger.Printf("auto-saving NickServ credentials with username %q", username)
n := uc.network
n.SASL.Mechanism = "PLAIN"
n.SASL.Plain.Username = username
n.SASL.Plain.Password = password
if err := dc.srv.db.StoreNetwork(dc.user.Username, &n.Network); err != nil {
dc.logger.Printf("failed to save NickServ credentials: %v", err)
}
}
func parseNickServCredentials(text, nick string) (username, password string, ok bool) {
fields := strings.Fields(text)
if len(fields) < 2 {
return "", "", false
}
cmd := strings.ToUpper(fields[0])
params := fields[1:]
switch cmd {
case "REGISTER":
username = nick
password = params[0]
case "IDENTIFY":
if len(params) == 1 {
username = nick
password = params[0]
} else {
username = params[0]
password = params[1]
}
case "SET":
if len(params) == 2 && strings.EqualFold(params[0], "PASSWORD") {
username = nick
password = params[1]
}
default:
return "", "", false
}
return username, password, true
}