2020-03-13 17:13:03 +00:00
|
|
|
package soju
|
2020-02-06 15:18:19 +00:00
|
|
|
|
|
|
|
import (
|
2020-03-12 20:28:09 +00:00
|
|
|
"crypto/tls"
|
2020-03-16 15:16:27 +00:00
|
|
|
"encoding/base64"
|
2020-02-06 15:18:19 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net"
|
2020-03-16 14:05:24 +00:00
|
|
|
"strconv"
|
2020-02-07 10:46:44 +00:00
|
|
|
"strings"
|
2020-03-16 13:28:45 +00:00
|
|
|
"sync"
|
2020-03-12 20:28:09 +00:00
|
|
|
"time"
|
2020-02-06 15:18:19 +00:00
|
|
|
|
2020-03-16 15:16:27 +00:00
|
|
|
"github.com/emersion/go-sasl"
|
2020-03-11 18:09:32 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
2020-02-06 15:18:19 +00:00
|
|
|
"gopkg.in/irc.v3"
|
|
|
|
)
|
|
|
|
|
|
|
|
type ircError struct {
|
|
|
|
Message *irc.Message
|
|
|
|
}
|
|
|
|
|
2020-03-11 18:09:32 +00:00
|
|
|
func (err ircError) Error() string {
|
|
|
|
return err.Message.String()
|
|
|
|
}
|
|
|
|
|
2020-02-06 15:18:19 +00:00
|
|
|
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",
|
|
|
|
},
|
|
|
|
}}
|
|
|
|
}
|
|
|
|
|
2020-03-11 18:09:32 +00:00
|
|
|
var errAuthFailed = ircError{&irc.Message{
|
|
|
|
Command: irc.ERR_PASSWDMISMATCH,
|
|
|
|
Params: []string{"*", "Invalid username or password"},
|
|
|
|
}}
|
2020-02-06 15:18:19 +00:00
|
|
|
|
|
|
|
type downstreamConn struct {
|
2020-03-31 16:16:54 +00:00
|
|
|
id uint64
|
|
|
|
net net.Conn
|
|
|
|
irc *irc.Conn
|
|
|
|
srv *Server
|
|
|
|
logger Logger
|
|
|
|
outgoing chan *irc.Message
|
|
|
|
closed chan struct{}
|
2020-02-06 20:11:35 +00:00
|
|
|
|
2020-03-16 08:32:18 +00:00
|
|
|
registered bool
|
|
|
|
user *user
|
|
|
|
nick string
|
|
|
|
rawUsername string
|
2020-03-27 18:17:58 +00:00
|
|
|
networkName string
|
2020-03-28 16:25:48 +00:00
|
|
|
clientName string
|
2020-03-16 08:32:18 +00:00
|
|
|
realname string
|
2020-03-21 23:44:55 +00:00
|
|
|
hostname string
|
2020-03-16 08:32:18 +00:00
|
|
|
password string // empty after authentication
|
|
|
|
network *network // can be nil
|
2020-03-16 13:28:45 +00:00
|
|
|
|
2020-03-16 14:05:24 +00:00
|
|
|
negociatingCaps bool
|
|
|
|
capVersion int
|
|
|
|
caps map[string]bool
|
|
|
|
|
2020-03-16 15:16:27 +00:00
|
|
|
saslServer sasl.Server
|
|
|
|
|
2020-03-16 13:28:45 +00:00
|
|
|
lock sync.Mutex
|
|
|
|
ourMessages map[*irc.Message]struct{}
|
2020-02-06 15:18:19 +00:00
|
|
|
}
|
|
|
|
|
2020-03-23 02:18:54 +00:00
|
|
|
func newDownstreamConn(srv *Server, netConn net.Conn, id uint64) *downstreamConn {
|
2020-02-17 11:36:42 +00:00
|
|
|
dc := &downstreamConn{
|
2020-03-31 16:16:54 +00:00
|
|
|
id: id,
|
|
|
|
net: netConn,
|
|
|
|
irc: irc.NewConn(netConn),
|
|
|
|
srv: srv,
|
|
|
|
logger: &prefixLogger{srv.Logger, fmt.Sprintf("downstream %q: ", netConn.RemoteAddr())},
|
|
|
|
outgoing: make(chan *irc.Message, 64),
|
|
|
|
closed: make(chan struct{}),
|
|
|
|
caps: make(map[string]bool),
|
|
|
|
ourMessages: make(map[*irc.Message]struct{}),
|
2020-02-06 20:11:35 +00:00
|
|
|
}
|
2020-03-21 23:44:55 +00:00
|
|
|
dc.hostname = netConn.RemoteAddr().String()
|
|
|
|
if host, _, err := net.SplitHostPort(dc.hostname); err == nil {
|
|
|
|
dc.hostname = host
|
|
|
|
}
|
2020-02-06 20:52:04 +00:00
|
|
|
|
|
|
|
go func() {
|
2020-02-17 11:41:27 +00:00
|
|
|
if err := dc.writeMessages(); err != nil {
|
|
|
|
dc.logger.Printf("failed to write message: %v", err)
|
2020-02-06 20:52:04 +00:00
|
|
|
}
|
2020-02-17 11:36:42 +00:00
|
|
|
if err := dc.net.Close(); err != nil {
|
|
|
|
dc.logger.Printf("failed to close connection: %v", err)
|
2020-02-07 11:42:24 +00:00
|
|
|
} else {
|
2020-02-17 11:36:42 +00:00
|
|
|
dc.logger.Printf("connection closed")
|
2020-02-07 11:42:24 +00:00
|
|
|
}
|
2020-02-06 20:52:04 +00:00
|
|
|
}()
|
|
|
|
|
2020-03-20 09:42:17 +00:00
|
|
|
dc.logger.Printf("new connection")
|
2020-02-17 11:36:42 +00:00
|
|
|
return dc
|
2020-02-06 20:11:35 +00:00
|
|
|
}
|
|
|
|
|
2020-02-17 11:36:42 +00:00
|
|
|
func (dc *downstreamConn) prefix() *irc.Prefix {
|
2020-02-06 21:19:31 +00:00
|
|
|
return &irc.Prefix{
|
2020-02-17 11:36:42 +00:00
|
|
|
Name: dc.nick,
|
2020-03-28 16:28:28 +00:00
|
|
|
User: dc.user.Username,
|
2020-03-21 23:44:55 +00:00
|
|
|
Host: dc.hostname,
|
2020-02-06 21:19:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-12 18:17:06 +00:00
|
|
|
func (dc *downstreamConn) forEachNetwork(f func(*network)) {
|
|
|
|
if dc.network != nil {
|
|
|
|
f(dc.network)
|
|
|
|
} else {
|
|
|
|
dc.user.forEachNetwork(f)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-04 14:44:13 +00:00
|
|
|
func (dc *downstreamConn) forEachUpstream(f func(*upstreamConn)) {
|
|
|
|
dc.user.forEachUpstream(func(uc *upstreamConn) {
|
2020-03-04 17:22:58 +00:00
|
|
|
if dc.network != nil && uc.network != dc.network {
|
2020-03-04 14:44:13 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
f(uc)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-03-12 17:33:03 +00:00
|
|
|
// 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
|
|
|
|
}
|
2020-03-21 09:24:38 +00:00
|
|
|
return dc.network.upstream()
|
2020-03-12 17:33:03 +00:00
|
|
|
}
|
|
|
|
|
2020-03-20 02:05:14 +00:00
|
|
|
func (dc *downstreamConn) marshalEntity(uc *upstreamConn, entity string) string {
|
|
|
|
if uc.isChannel(entity) {
|
|
|
|
return dc.marshalChannel(uc, entity)
|
2020-03-18 02:14:36 +00:00
|
|
|
}
|
2020-03-20 02:05:14 +00:00
|
|
|
return dc.marshalNick(uc, entity)
|
2020-03-18 02:14:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (dc *downstreamConn) marshalChannel(uc *upstreamConn, name string) string {
|
2020-03-20 09:42:17 +00:00
|
|
|
if dc.network != nil {
|
2020-03-18 02:14:36 +00:00
|
|
|
return name
|
|
|
|
}
|
|
|
|
return name + "/" + uc.network.GetName()
|
|
|
|
}
|
|
|
|
|
2020-03-19 23:23:19 +00:00
|
|
|
func (dc *downstreamConn) unmarshalEntity(name string) (*upstreamConn, string, error) {
|
2020-03-12 17:33:03 +00:00
|
|
|
if uc := dc.upstream(); uc != nil {
|
|
|
|
return uc, name, nil
|
|
|
|
}
|
|
|
|
|
2020-03-19 23:23:19 +00:00
|
|
|
var conn *upstreamConn
|
2020-03-18 02:14:36 +00:00
|
|
|
if i := strings.LastIndexByte(name, '/'); i >= 0 {
|
2020-03-19 23:23:19 +00:00
|
|
|
network := name[i+1:]
|
2020-03-18 02:14:36 +00:00
|
|
|
name = name[:i]
|
|
|
|
|
|
|
|
dc.forEachUpstream(func(uc *upstreamConn) {
|
|
|
|
if network != uc.network.GetName() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
conn = uc
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-03-19 23:23:19 +00:00
|
|
|
if conn == nil {
|
2020-03-04 14:44:13 +00:00
|
|
|
return nil, "", ircError{&irc.Message{
|
|
|
|
Command: irc.ERR_NOSUCHCHANNEL,
|
|
|
|
Params: []string{name, "No such channel"},
|
|
|
|
}}
|
2020-02-19 17:25:19 +00:00
|
|
|
}
|
2020-03-19 23:23:19 +00:00
|
|
|
return conn, name, nil
|
2020-02-19 17:25:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (dc *downstreamConn) marshalNick(uc *upstreamConn, nick string) string {
|
|
|
|
if nick == uc.nick {
|
|
|
|
return dc.nick
|
|
|
|
}
|
2020-03-20 09:42:17 +00:00
|
|
|
if dc.network != nil {
|
2020-03-18 02:14:36 +00:00
|
|
|
return nick
|
|
|
|
}
|
|
|
|
return nick + "/" + uc.network.GetName()
|
2020-02-19 17:25:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (dc *downstreamConn) marshalUserPrefix(uc *upstreamConn, prefix *irc.Prefix) *irc.Prefix {
|
|
|
|
if prefix.Name == uc.nick {
|
|
|
|
return dc.prefix()
|
|
|
|
}
|
2020-03-20 09:42:17 +00:00
|
|
|
if dc.network != nil {
|
2020-03-18 02:14:36 +00:00
|
|
|
return prefix
|
|
|
|
}
|
|
|
|
return &irc.Prefix{
|
|
|
|
Name: prefix.Name + "/" + uc.network.GetName(),
|
|
|
|
User: prefix.User,
|
|
|
|
Host: prefix.Host,
|
|
|
|
}
|
2020-02-19 17:25:19 +00:00
|
|
|
}
|
|
|
|
|
2020-02-17 14:46:29 +00:00
|
|
|
func (dc *downstreamConn) isClosed() bool {
|
|
|
|
select {
|
|
|
|
case <-dc.closed:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 15:33:19 +00:00
|
|
|
func (dc *downstreamConn) readMessages(ch chan<- event) error {
|
2020-02-06 20:11:35 +00:00
|
|
|
for {
|
2020-02-17 11:36:42 +00:00
|
|
|
msg, err := dc.irc.ReadMessage()
|
2020-02-06 20:11:35 +00:00
|
|
|
if err == io.EOF {
|
|
|
|
break
|
|
|
|
} else if err != nil {
|
|
|
|
return fmt.Errorf("failed to read IRC command: %v", err)
|
|
|
|
}
|
|
|
|
|
2020-02-18 15:31:18 +00:00
|
|
|
if dc.srv.Debug {
|
|
|
|
dc.logger.Printf("received: %v", msg)
|
|
|
|
}
|
|
|
|
|
2020-03-27 15:33:19 +00:00
|
|
|
ch <- eventDownstreamMessage{msg, dc}
|
2020-02-06 20:11:35 +00:00
|
|
|
}
|
|
|
|
|
2020-02-07 11:42:24 +00:00
|
|
|
return nil
|
2020-02-06 20:11:35 +00:00
|
|
|
}
|
|
|
|
|
2020-02-17 11:41:27 +00:00
|
|
|
func (dc *downstreamConn) writeMessages() error {
|
2020-02-17 14:46:29 +00:00
|
|
|
for {
|
|
|
|
var err error
|
|
|
|
var closed bool
|
|
|
|
select {
|
2020-03-16 10:26:54 +00:00
|
|
|
case msg := <-dc.outgoing:
|
2020-02-18 15:31:18 +00:00
|
|
|
if dc.srv.Debug {
|
|
|
|
dc.logger.Printf("sent: %v", msg)
|
|
|
|
}
|
2020-02-17 14:46:29 +00:00
|
|
|
err = dc.irc.WriteMessage(msg)
|
|
|
|
case <-dc.closed:
|
|
|
|
closed = true
|
|
|
|
}
|
|
|
|
if err != nil {
|
2020-02-17 11:41:27 +00:00
|
|
|
return err
|
|
|
|
}
|
2020-02-17 14:46:29 +00:00
|
|
|
if closed {
|
|
|
|
break
|
|
|
|
}
|
2020-02-17 11:41:27 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-03-27 23:54:42 +00:00
|
|
|
// Close closes the connection. It is safe to call from any goroutine.
|
2020-02-17 11:36:42 +00:00
|
|
|
func (dc *downstreamConn) Close() error {
|
2020-02-17 14:46:29 +00:00
|
|
|
if dc.isClosed() {
|
2020-02-06 20:52:04 +00:00
|
|
|
return fmt.Errorf("downstream connection already closed")
|
|
|
|
}
|
2020-02-17 14:46:29 +00:00
|
|
|
close(dc.closed)
|
2020-02-07 11:42:24 +00:00
|
|
|
return nil
|
2020-02-06 15:18:19 +00:00
|
|
|
}
|
|
|
|
|
2020-03-27 23:54:42 +00:00
|
|
|
// SendMessage queues a new outgoing message. It is safe to call from any
|
|
|
|
// goroutine.
|
2020-02-17 11:36:42 +00:00
|
|
|
func (dc *downstreamConn) SendMessage(msg *irc.Message) {
|
2020-03-31 17:37:34 +00:00
|
|
|
// TODO: strip tags if the client doesn't support them (see runNetwork)
|
2020-03-16 10:26:54 +00:00
|
|
|
dc.outgoing <- msg
|
2020-02-17 11:27:48 +00:00
|
|
|
}
|
|
|
|
|
2020-02-17 11:36:42 +00:00
|
|
|
func (dc *downstreamConn) handleMessage(msg *irc.Message) error {
|
2020-02-06 15:18:19 +00:00
|
|
|
switch msg.Command {
|
2020-02-06 21:22:14 +00:00
|
|
|
case "QUIT":
|
2020-02-17 11:36:42 +00:00
|
|
|
return dc.Close()
|
2020-02-06 15:18:19 +00:00
|
|
|
default:
|
2020-02-17 11:36:42 +00:00
|
|
|
if dc.registered {
|
|
|
|
return dc.handleMessageRegistered(msg)
|
2020-02-06 15:18:19 +00:00
|
|
|
} else {
|
2020-02-17 11:36:42 +00:00
|
|
|
return dc.handleMessageUnregistered(msg)
|
2020-02-06 15:18:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-17 11:36:42 +00:00
|
|
|
func (dc *downstreamConn) handleMessageUnregistered(msg *irc.Message) error {
|
2020-02-06 15:18:19 +00:00
|
|
|
switch msg.Command {
|
|
|
|
case "NICK":
|
2020-03-18 11:23:08 +00:00
|
|
|
var nick string
|
|
|
|
if err := parseMessageParams(msg, &nick); err != nil {
|
2020-02-07 11:36:02 +00:00
|
|
|
return err
|
2020-02-06 15:18:19 +00:00
|
|
|
}
|
2020-03-18 11:23:08 +00:00
|
|
|
if nick == serviceNick {
|
|
|
|
return ircError{&irc.Message{
|
|
|
|
Command: irc.ERR_NICKNAMEINUSE,
|
|
|
|
Params: []string{dc.nick, nick, "Nickname reserved for bouncer service"},
|
|
|
|
}}
|
|
|
|
}
|
|
|
|
dc.nick = nick
|
2020-02-06 15:18:19 +00:00
|
|
|
case "USER":
|
2020-03-18 11:23:08 +00:00
|
|
|
if err := parseMessageParams(msg, &dc.rawUsername, nil, nil, &dc.realname); err != nil {
|
2020-02-07 11:36:02 +00:00
|
|
|
return err
|
2020-02-06 15:18:19 +00:00
|
|
|
}
|
2020-03-11 18:09:32 +00:00
|
|
|
case "PASS":
|
|
|
|
if err := parseMessageParams(msg, &dc.password); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-03-16 14:05:24 +00:00
|
|
|
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
|
|
|
|
}
|
2020-03-16 15:16:27 +00:00
|
|
|
case "AUTHENTICATE":
|
|
|
|
if !dc.caps["sasl"] {
|
|
|
|
return ircError{&irc.Message{
|
2020-03-19 13:51:45 +00:00
|
|
|
Command: irc.ERR_SASLFAIL,
|
2020-03-16 15:16:27 +00:00
|
|
|
Params: []string{"*", "AUTHENTICATE requires the \"sasl\" capability to be enabled"},
|
|
|
|
}}
|
|
|
|
}
|
|
|
|
if len(msg.Params) == 0 {
|
|
|
|
return ircError{&irc.Message{
|
2020-03-19 13:51:45 +00:00
|
|
|
Command: irc.ERR_SASLFAIL,
|
2020-03-16 15:16:27 +00:00
|
|
|
Params: []string{"*", "Missing AUTHENTICATE argument"},
|
|
|
|
}}
|
|
|
|
}
|
|
|
|
if dc.nick == "" {
|
|
|
|
return ircError{&irc.Message{
|
2020-03-19 13:51:45 +00:00
|
|
|
Command: irc.ERR_SASLFAIL,
|
2020-03-16 15:16:27 +00:00
|
|
|
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{
|
2020-03-19 13:51:45 +00:00
|
|
|
Command: irc.ERR_SASLFAIL,
|
2020-03-16 15:16:27 +00:00
|
|
|
Params: []string{"*", fmt.Sprintf("Unsupported SASL mechanism %q", mech)},
|
|
|
|
}}
|
|
|
|
}
|
|
|
|
} else if msg.Params[0] == "*" {
|
|
|
|
dc.saslServer = nil
|
|
|
|
return ircError{&irc.Message{
|
2020-03-19 13:51:45 +00:00
|
|
|
Command: irc.ERR_SASLABORTED,
|
2020-03-16 15:16:27 +00:00
|
|
|
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{
|
2020-03-19 13:51:45 +00:00
|
|
|
Command: irc.ERR_SASLFAIL,
|
2020-03-16 15:16:27 +00:00
|
|
|
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{
|
2020-03-19 13:51:45 +00:00
|
|
|
Command: irc.ERR_SASLFAIL,
|
2020-03-16 15:16:27 +00:00
|
|
|
Params: []string{"*", ircErr.Message.Params[1]},
|
|
|
|
}}
|
|
|
|
}
|
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
2020-03-19 13:51:45 +00:00
|
|
|
Command: irc.ERR_SASLFAIL,
|
2020-03-16 15:16:27 +00:00
|
|
|
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(),
|
2020-03-19 13:51:45 +00:00
|
|
|
Command: irc.RPL_LOGGEDIN,
|
2020-03-16 15:16:27 +00:00
|
|
|
Params: []string{dc.nick, dc.nick, dc.user.Username, "You are now logged in"},
|
|
|
|
})
|
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
2020-03-19 13:51:45 +00:00
|
|
|
Command: irc.RPL_SASLSUCCESS,
|
2020-03-16 15:16:27 +00:00
|
|
|
Params: []string{dc.nick, "SASL authentication successful"},
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
challengeStr := "+"
|
2020-03-21 07:44:03 +00:00
|
|
|
if len(challenge) > 0 {
|
2020-03-16 15:16:27 +00:00
|
|
|
challengeStr = base64.StdEncoding.EncodeToString(challenge)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: multi-line messages
|
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
|
|
|
Command: "AUTHENTICATE",
|
|
|
|
Params: []string{challengeStr},
|
|
|
|
})
|
|
|
|
}
|
2020-02-06 15:18:19 +00:00
|
|
|
default:
|
2020-02-17 11:36:42 +00:00
|
|
|
dc.logger.Printf("unhandled message: %v", msg)
|
2020-02-06 15:18:19 +00:00
|
|
|
return newUnknownCommandError(msg.Command)
|
|
|
|
}
|
2020-03-16 14:05:24 +00:00
|
|
|
if dc.rawUsername != "" && dc.nick != "" && !dc.negociatingCaps {
|
2020-02-17 11:36:42 +00:00
|
|
|
return dc.register()
|
2020-02-06 15:18:19 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-03-16 14:05:24 +00:00
|
|
|
func (dc *downstreamConn) handleCapCommand(cmd string, args []string) error {
|
2020-03-16 14:11:08 +00:00
|
|
|
cmd = strings.ToUpper(cmd)
|
|
|
|
|
2020-03-16 14:05:24 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-31 17:50:31 +00:00
|
|
|
caps := []string{"message-tags", "server-time"}
|
|
|
|
|
2020-03-16 15:16:27 +00:00
|
|
|
if dc.capVersion >= 302 {
|
2020-03-16 14:05:24 +00:00
|
|
|
caps = append(caps, "sasl=PLAIN")
|
|
|
|
} else {
|
|
|
|
caps = append(caps, "sasl")
|
2020-03-16 15:16:27 +00:00
|
|
|
}
|
2020-03-16 14:05:24 +00:00
|
|
|
|
|
|
|
// TODO: multi-line replies
|
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
|
|
|
Command: "CAP",
|
|
|
|
Params: []string{replyTo, "LS", strings.Join(caps, " ")},
|
|
|
|
})
|
|
|
|
|
|
|
|
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"},
|
|
|
|
}}
|
|
|
|
}
|
|
|
|
|
|
|
|
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, "-")
|
|
|
|
}
|
|
|
|
|
|
|
|
enabled := dc.caps[name]
|
|
|
|
if enable == enabled {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
switch name {
|
2020-03-31 17:50:31 +00:00
|
|
|
case "sasl", "message-tags", "server-time":
|
2020-03-16 15:16:27 +00:00
|
|
|
dc.caps[name] = enable
|
2020-03-16 14:05:24 +00:00
|
|
|
default:
|
|
|
|
ack = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-03-12 20:28:09 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2020-03-28 16:25:48 +00:00
|
|
|
func unmarshalUsername(rawUsername string) (username, client, network string) {
|
2020-03-16 15:16:27 +00:00
|
|
|
username = rawUsername
|
2020-03-28 16:25:48 +00:00
|
|
|
|
|
|
|
i := strings.IndexAny(username, "/@")
|
|
|
|
j := strings.LastIndexAny(username, "/@")
|
|
|
|
if i >= 0 {
|
|
|
|
username = rawUsername[:i]
|
|
|
|
}
|
|
|
|
if j >= 0 {
|
2020-03-31 17:02:02 +00:00
|
|
|
if rawUsername[j] == '@' {
|
|
|
|
client = rawUsername[j+1:]
|
|
|
|
} else {
|
|
|
|
network = rawUsername[j+1:]
|
|
|
|
}
|
2020-03-04 14:44:13 +00:00
|
|
|
}
|
2020-03-28 16:25:48 +00:00
|
|
|
if i >= 0 && j >= 0 && i < j {
|
2020-03-31 17:02:02 +00:00
|
|
|
if rawUsername[i] == '@' {
|
|
|
|
client = rawUsername[i+1 : j]
|
|
|
|
} else {
|
|
|
|
network = rawUsername[i+1 : j]
|
|
|
|
}
|
2020-03-04 14:44:13 +00:00
|
|
|
}
|
2020-03-28 16:25:48 +00:00
|
|
|
|
|
|
|
return username, client, network
|
2020-03-16 15:16:27 +00:00
|
|
|
}
|
2020-03-04 14:44:13 +00:00
|
|
|
|
2020-03-27 18:17:58 +00:00
|
|
|
func (dc *downstreamConn) authenticate(username, password string) error {
|
2020-03-28 16:25:48 +00:00
|
|
|
username, clientName, networkName := unmarshalUsername(username)
|
2020-03-27 18:17:58 +00:00
|
|
|
|
2020-03-27 21:38:38 +00:00
|
|
|
u, err := dc.srv.db.GetUser(username)
|
|
|
|
if err != nil {
|
|
|
|
dc.logger.Printf("failed authentication for %q: %v", username, err)
|
2020-03-27 18:17:58 +00:00
|
|
|
return errAuthFailed
|
|
|
|
}
|
|
|
|
|
2020-03-27 21:38:38 +00:00
|
|
|
err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
|
2020-03-27 18:17:58 +00:00
|
|
|
if err != nil {
|
|
|
|
dc.logger.Printf("failed authentication for %q: %v", username, err)
|
|
|
|
return errAuthFailed
|
|
|
|
}
|
|
|
|
|
2020-03-27 21:38:38 +00:00
|
|
|
dc.user = dc.srv.getUser(username)
|
|
|
|
if dc.user == nil {
|
|
|
|
dc.logger.Printf("failed authentication for %q: user not active", username)
|
|
|
|
return errAuthFailed
|
|
|
|
}
|
2020-03-28 16:25:48 +00:00
|
|
|
dc.clientName = clientName
|
2020-03-27 18:17:58 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-28 16:25:48 +00:00
|
|
|
if dc.clientName == "" && dc.networkName == "" {
|
|
|
|
_, dc.clientName, dc.networkName = unmarshalUsername(dc.rawUsername)
|
2020-03-27 18:17:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
dc.registered = true
|
2020-03-28 16:28:28 +00:00
|
|
|
dc.logger.Printf("registration complete for user %q", dc.user.Username)
|
2020-03-27 18:17:58 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dc *downstreamConn) loadNetwork() error {
|
|
|
|
if dc.networkName == "" {
|
2020-03-16 15:16:27 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-03-27 18:17:58 +00:00
|
|
|
network := dc.user.getNetwork(dc.networkName)
|
2020-03-16 15:16:27 +00:00
|
|
|
if network == nil {
|
2020-03-27 18:17:58 +00:00
|
|
|
addr := dc.networkName
|
2020-03-16 15:16:27 +00:00
|
|
|
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,
|
2020-03-27 18:17:58 +00:00
|
|
|
Params: []string{"*", fmt.Sprintf("Failed to connect to %q", dc.networkName)},
|
2020-03-16 15:16:27 +00:00
|
|
|
}}
|
|
|
|
}
|
|
|
|
|
2020-03-27 18:17:58 +00:00
|
|
|
dc.logger.Printf("auto-saving network %q", dc.networkName)
|
2020-03-16 15:16:27 +00:00
|
|
|
var err error
|
2020-03-18 23:57:14 +00:00
|
|
|
network, err = dc.user.createNetwork(&Network{
|
2020-03-27 18:17:58 +00:00
|
|
|
Addr: dc.networkName,
|
2020-03-18 23:57:14 +00:00
|
|
|
Nick: dc.nick,
|
|
|
|
})
|
2020-03-16 15:16:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
dc.network = network
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-03-27 18:17:58 +00:00
|
|
|
func (dc *downstreamConn) welcome() error {
|
|
|
|
if dc.user == nil || !dc.registered {
|
|
|
|
panic("tried to welcome an unregistered connection")
|
2020-02-07 10:36:42 +00:00
|
|
|
}
|
|
|
|
|
2020-03-27 18:17:58 +00:00
|
|
|
// 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
|
2020-03-04 14:44:13 +00:00
|
|
|
}
|
|
|
|
|
2020-03-28 16:36:09 +00:00
|
|
|
// Only send history if we're the first connected client with that name and
|
|
|
|
// network
|
|
|
|
sendHistory := true
|
|
|
|
dc.user.forEachDownstream(func(conn *downstreamConn) {
|
|
|
|
if dc.clientName == conn.clientName && dc.network == conn.network {
|
|
|
|
sendHistory = false
|
|
|
|
}
|
|
|
|
})
|
2020-02-07 10:56:36 +00:00
|
|
|
|
2020-02-17 11:36:42 +00:00
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
2020-02-06 15:18:19 +00:00
|
|
|
Command: irc.RPL_WELCOME,
|
2020-03-13 17:13:03 +00:00
|
|
|
Params: []string{dc.nick, "Welcome to soju, " + dc.nick},
|
2020-02-17 11:27:48 +00:00
|
|
|
})
|
2020-02-17 11:36:42 +00:00
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
2020-02-06 15:18:19 +00:00
|
|
|
Command: irc.RPL_YOURHOST,
|
2020-02-17 11:36:42 +00:00
|
|
|
Params: []string{dc.nick, "Your host is " + dc.srv.Hostname},
|
2020-02-17 11:27:48 +00:00
|
|
|
})
|
2020-02-17 11:36:42 +00:00
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
2020-02-06 15:18:19 +00:00
|
|
|
Command: irc.RPL_CREATED,
|
2020-02-17 11:36:42 +00:00
|
|
|
Params: []string{dc.nick, "Who cares when the server was created?"},
|
2020-02-17 11:27:48 +00:00
|
|
|
})
|
2020-02-17 11:36:42 +00:00
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
2020-02-06 15:18:19 +00:00
|
|
|
Command: irc.RPL_MYINFO,
|
2020-03-13 17:13:03 +00:00
|
|
|
Params: []string{dc.nick, dc.srv.Hostname, "soju", "aiwroO", "OovaimnqpsrtklbeI"},
|
2020-02-17 11:27:48 +00:00
|
|
|
})
|
2020-03-13 11:06:02 +00:00
|
|
|
// TODO: RPL_ISUPPORT
|
2020-02-17 11:36:42 +00:00
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
2020-02-06 15:18:19 +00:00
|
|
|
Command: irc.ERR_NOMOTD,
|
2020-02-17 11:36:42 +00:00
|
|
|
Params: []string{dc.nick, "No MOTD"},
|
2020-02-17 11:27:48 +00:00
|
|
|
})
|
2020-02-06 15:18:19 +00:00
|
|
|
|
2020-03-04 14:44:13 +00:00
|
|
|
dc.forEachUpstream(func(uc *upstreamConn) {
|
2020-02-06 21:29:24 +00:00
|
|
|
for _, ch := range uc.channels {
|
|
|
|
if ch.complete {
|
2020-03-20 21:53:05 +00:00
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.prefix(),
|
|
|
|
Command: "JOIN",
|
|
|
|
Params: []string{dc.marshalChannel(ch.conn, ch.Name)},
|
|
|
|
})
|
|
|
|
|
2020-02-17 11:36:42 +00:00
|
|
|
forwardChannel(dc, ch)
|
2020-02-06 21:29:24 +00:00
|
|
|
}
|
|
|
|
}
|
2020-03-25 09:53:08 +00:00
|
|
|
})
|
2020-02-07 15:43:14 +00:00
|
|
|
|
2020-03-25 09:53:08 +00:00
|
|
|
dc.forEachNetwork(func(net *network) {
|
2020-03-28 16:36:09 +00:00
|
|
|
dc.runNetwork(net, sendHistory)
|
2020-03-25 10:28:25 +00:00
|
|
|
})
|
2020-02-17 14:46:29 +00:00
|
|
|
|
2020-03-25 10:28:25 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// runNetwork starts listening for messages coming from the network's ring
|
|
|
|
// buffer.
|
|
|
|
//
|
|
|
|
// It panics if the network is not suitable for the downstream connection.
|
|
|
|
func (dc *downstreamConn) runNetwork(net *network, loadHistory bool) {
|
|
|
|
if dc.network != nil && net != dc.network {
|
|
|
|
panic("network not suitable for downstream connection")
|
|
|
|
}
|
|
|
|
|
|
|
|
var seqPtr *uint64
|
|
|
|
if loadHistory {
|
|
|
|
net.lock.Lock()
|
2020-03-28 16:36:09 +00:00
|
|
|
seq, ok := net.history[dc.clientName]
|
2020-03-25 10:28:25 +00:00
|
|
|
net.lock.Unlock()
|
|
|
|
if ok {
|
|
|
|
seqPtr = &seq
|
2020-02-07 15:43:14 +00:00
|
|
|
}
|
2020-03-25 10:28:25 +00:00
|
|
|
}
|
2020-02-17 14:46:29 +00:00
|
|
|
|
2020-03-31 17:37:34 +00:00
|
|
|
// TODO: can't be enabled/disabled on-the-fly
|
|
|
|
msgTagsEnabled := dc.caps["message-tags"]
|
2020-03-31 17:50:31 +00:00
|
|
|
serverTimeEnabled := dc.caps["server-time"]
|
2020-03-31 17:37:34 +00:00
|
|
|
|
2020-03-25 10:28:25 +00:00
|
|
|
consumer, ch := net.ring.NewConsumer(seqPtr)
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
var closed bool
|
|
|
|
select {
|
|
|
|
case <-ch:
|
|
|
|
uc := net.upstream()
|
|
|
|
if uc == nil {
|
|
|
|
dc.logger.Printf("ignoring messages for upstream %q: upstream is disconnected", net.Addr)
|
2020-02-17 14:46:29 +00:00
|
|
|
break
|
|
|
|
}
|
2020-03-31 16:16:54 +00:00
|
|
|
|
|
|
|
for {
|
|
|
|
msg := consumer.Peek()
|
|
|
|
if msg == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
dc.lock.Lock()
|
|
|
|
_, ours := dc.ourMessages[msg]
|
|
|
|
delete(dc.ourMessages, msg)
|
|
|
|
dc.lock.Unlock()
|
|
|
|
if ours {
|
|
|
|
// The message comes from our connection, don't echo it
|
|
|
|
// back
|
|
|
|
consumer.Consume()
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
msg = msg.Copy()
|
|
|
|
switch msg.Command {
|
|
|
|
case "PRIVMSG":
|
|
|
|
msg.Prefix = dc.marshalUserPrefix(uc, msg.Prefix)
|
|
|
|
msg.Params[0] = dc.marshalEntity(uc, msg.Params[0])
|
|
|
|
default:
|
|
|
|
panic("expected to consume a PRIVMSG message")
|
|
|
|
}
|
|
|
|
|
2020-03-31 17:37:34 +00:00
|
|
|
if !msgTagsEnabled {
|
2020-03-31 17:50:31 +00:00
|
|
|
for name := range msg.Tags {
|
|
|
|
supported := false
|
|
|
|
switch name {
|
|
|
|
case "time":
|
|
|
|
supported = serverTimeEnabled
|
|
|
|
}
|
|
|
|
if !supported {
|
|
|
|
delete(msg.Tags, name)
|
|
|
|
}
|
|
|
|
}
|
2020-03-31 17:37:34 +00:00
|
|
|
}
|
|
|
|
|
2020-03-31 16:16:54 +00:00
|
|
|
dc.SendMessage(msg)
|
|
|
|
consumer.Consume()
|
|
|
|
}
|
2020-03-25 10:28:25 +00:00
|
|
|
case <-dc.closed:
|
|
|
|
closed = true
|
2020-02-17 14:46:29 +00:00
|
|
|
}
|
2020-03-25 10:28:25 +00:00
|
|
|
if closed {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2020-02-17 14:46:29 +00:00
|
|
|
|
2020-03-28 16:36:09 +00:00
|
|
|
// TODO: close the consumer from the user goroutine, so we don't need
|
|
|
|
// that net.history lock
|
2020-03-25 10:28:25 +00:00
|
|
|
seq := consumer.Close()
|
2020-02-17 14:46:29 +00:00
|
|
|
|
2020-03-27 16:55:03 +00:00
|
|
|
net.lock.Lock()
|
2020-03-28 16:36:09 +00:00
|
|
|
net.history[dc.clientName] = seq
|
2020-03-27 16:55:03 +00:00
|
|
|
net.lock.Unlock()
|
2020-03-25 10:28:25 +00:00
|
|
|
}()
|
2020-02-06 15:18:19 +00:00
|
|
|
}
|
|
|
|
|
2020-03-16 11:44:59 +00:00
|
|
|
func (dc *downstreamConn) runUntilRegistered() error {
|
|
|
|
for !dc.registered {
|
|
|
|
msg, err := dc.irc.ReadMessage()
|
2020-03-16 13:30:49 +00:00
|
|
|
if err != nil {
|
2020-03-16 11:44:59 +00:00
|
|
|
return fmt.Errorf("failed to read IRC command: %v", err)
|
|
|
|
}
|
|
|
|
|
2020-03-16 14:10:16 +00:00
|
|
|
if dc.srv.Debug {
|
|
|
|
dc.logger.Printf("received: %v", msg)
|
|
|
|
}
|
|
|
|
|
2020-03-16 11:44:59 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-02-17 11:36:42 +00:00
|
|
|
func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
|
2020-02-06 15:18:19 +00:00
|
|
|
switch msg.Command {
|
2020-03-16 14:11:08 +00:00
|
|
|
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
|
|
|
|
}
|
2020-03-16 13:32:38 +00:00
|
|
|
case "PING":
|
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
|
|
|
Command: "PONG",
|
|
|
|
Params: msg.Params,
|
|
|
|
})
|
|
|
|
return nil
|
2020-02-07 11:19:42 +00:00
|
|
|
case "USER":
|
2020-02-06 15:18:19 +00:00
|
|
|
return ircError{&irc.Message{
|
|
|
|
Command: irc.ERR_ALREADYREGISTERED,
|
2020-02-17 11:36:42 +00:00
|
|
|
Params: []string{dc.nick, "You may not reregister"},
|
2020-02-06 15:18:19 +00:00
|
|
|
}}
|
2020-02-07 11:19:42 +00:00
|
|
|
case "NICK":
|
2020-03-12 18:17:06 +00:00
|
|
|
var nick string
|
|
|
|
if err := parseMessageParams(msg, &nick); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
dc.forEachNetwork(func(n *network) {
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
n.Nick = nick
|
|
|
|
err = dc.srv.db.StoreNetwork(dc.user.Username, &n.Network)
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-04 14:44:13 +00:00
|
|
|
dc.forEachUpstream(func(uc *upstreamConn) {
|
2020-02-17 15:17:31 +00:00
|
|
|
uc.SendMessage(msg)
|
2020-02-07 11:19:42 +00:00
|
|
|
})
|
2020-03-25 10:52:24 +00:00
|
|
|
case "JOIN":
|
|
|
|
var namesStr string
|
|
|
|
if err := parseMessageParams(msg, &namesStr); err != nil {
|
2020-02-07 12:36:32 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-25 10:52:24 +00:00
|
|
|
var keys []string
|
|
|
|
if len(msg.Params) > 1 {
|
|
|
|
keys = strings.Split(msg.Params[1], ",")
|
|
|
|
}
|
|
|
|
|
|
|
|
for i, name := range strings.Split(namesStr, ",") {
|
2020-03-25 10:32:44 +00:00
|
|
|
uc, upstreamName, err := dc.unmarshalEntity(name)
|
2020-03-12 17:33:03 +00:00
|
|
|
if err != nil {
|
2020-03-25 22:56:01 +00:00
|
|
|
return err
|
2020-03-12 17:33:03 +00:00
|
|
|
}
|
2020-03-25 10:32:44 +00:00
|
|
|
|
2020-03-25 10:52:24 +00:00
|
|
|
var key string
|
|
|
|
if len(keys) > i {
|
|
|
|
key = keys[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
params := []string{upstreamName}
|
|
|
|
if key != "" {
|
|
|
|
params = append(params, key)
|
|
|
|
}
|
2020-03-25 10:32:44 +00:00
|
|
|
uc.SendMessage(&irc.Message{
|
2020-03-25 10:52:24 +00:00
|
|
|
Command: "JOIN",
|
|
|
|
Params: params,
|
2020-03-25 10:32:44 +00:00
|
|
|
})
|
|
|
|
|
2020-03-25 10:52:24 +00:00
|
|
|
err = dc.srv.db.StoreChannel(uc.network.ID, &Channel{
|
|
|
|
Name: upstreamName,
|
|
|
|
Key: key,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
dc.logger.Printf("failed to create channel %q in DB: %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 {
|
2020-03-25 22:56:01 +00:00
|
|
|
return err
|
2020-03-25 10:52:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
params := []string{upstreamName}
|
|
|
|
if reason != "" {
|
|
|
|
params = append(params, reason)
|
|
|
|
}
|
|
|
|
uc.SendMessage(&irc.Message{
|
|
|
|
Command: "PART",
|
|
|
|
Params: params,
|
|
|
|
})
|
|
|
|
|
|
|
|
if err := dc.srv.db.DeleteChannel(uc.network.ID, upstreamName); err != nil {
|
|
|
|
dc.logger.Printf("failed to delete channel %q in DB: %v", upstreamName, err)
|
2020-03-12 17:33:03 +00:00
|
|
|
}
|
|
|
|
}
|
2020-03-25 22:46:36 +00:00
|
|
|
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.SendMessage(&irc.Message{
|
|
|
|
Command: "KICK",
|
|
|
|
Params: params,
|
|
|
|
})
|
|
|
|
}
|
2020-02-07 12:08:27 +00:00
|
|
|
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]
|
|
|
|
}
|
|
|
|
|
2020-03-20 23:48:19 +00:00
|
|
|
if name == dc.nick {
|
2020-02-07 12:08:27 +00:00
|
|
|
if modeStr != "" {
|
2020-03-04 14:44:13 +00:00
|
|
|
dc.forEachUpstream(func(uc *upstreamConn) {
|
2020-02-19 17:25:19 +00:00
|
|
|
uc.SendMessage(&irc.Message{
|
|
|
|
Command: "MODE",
|
|
|
|
Params: []string{uc.nick, modeStr},
|
|
|
|
})
|
2020-02-07 12:08:27 +00:00
|
|
|
})
|
|
|
|
} else {
|
2020-02-17 11:36:42 +00:00
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
2020-02-07 12:08:27 +00:00
|
|
|
Command: irc.RPL_UMODEIS,
|
2020-03-20 02:05:14 +00:00
|
|
|
Params: []string{dc.nick, ""}, // TODO
|
2020-02-17 11:27:48 +00:00
|
|
|
})
|
2020-02-07 12:08:27 +00:00
|
|
|
}
|
2020-03-20 23:48:19 +00:00
|
|
|
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.SendMessage(&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,
|
|
|
|
})
|
2020-03-26 04:51:47 +00:00
|
|
|
if ch.creationTime != "" {
|
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
|
|
|
Command: rpl_creationtime,
|
|
|
|
Params: []string{dc.nick, name, ch.creationTime},
|
|
|
|
})
|
|
|
|
}
|
2020-02-07 12:08:27 +00:00
|
|
|
}
|
2020-03-25 23:19:45 +00:00
|
|
|
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.SendMessage(&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)
|
|
|
|
}
|
Add LIST support
This commit adds support for downstream LIST messages from multiple
concurrent downstreams to multiple concurrent upstreams, including
support for multiple pending LIST requests from the same downstream.
Because a unique RPL_LISTEND message must be sent to the requesting
downstream, and that there might be multiple upstreams, each sending
their own RPL_LISTEND, a cache of RPL_LISTEND replies of some sort is
required to match RPL_LISTEND together in order to only send one back
downstream.
This commit adds a list of "pending LIST" structs, which each contain a
map of all upstreams that yet need to send a RPL_LISTEND, and the
corresponding LIST request associated with that response. This list of
pending LISTs is sorted according to the order that the requesting
downstreams sent the LIST messages in. Each pending set also stores the
id of the requesting downstream, in order to only forward the replies to
it and no other downstream. (This is important because LIST replies can
typically amount to several thousands messages on large servers.)
When a single downstream makes multiple LIST requests, only the first
one will be immediately sent to the upstream servers. The next ones will
be buffered until the first one is completed. Distinct downstreams can
make concurrent LIST requests without any request buffering.
Each RPL_LIST message is forwarded to the downstream of the first
matching pending LIST struct.
When an upstream sends an RPL_LISTEND message, the upstream is removed
from the first matching pending LIST struct, but that message is not
immediately forwarded downstream. If there are no remaining pending LIST
requests in that struct is then empty, that means all upstreams have
sent back all their RPL_LISTEND replies (which means they also sent all
their RPL_LIST replies); so a unique RPL_LISTEND is sent to downstream
and that pending LIST set is removed from the cache.
Upstreams are removed from the pending LIST structs in two other cases:
- when they are closed (to avoid stalling because of a disconnected
upstream that will never reply to the LIST message): they are removed
from all pending LIST structs
- when they reply with an ERR_UNKNOWNCOMMAND or RPL_TRYAGAIN LIST reply,
which is typically used when a user is not allowed to LIST because they
just joined the server: they are removed from the first pending LIST
struct, as if an RPL_LISTEND message was received
2020-03-26 01:40:30 +00:00
|
|
|
case "LIST":
|
|
|
|
// TODO: support ELIST when supported by all upstreams
|
|
|
|
|
|
|
|
pl := pendingLIST{
|
|
|
|
downstreamID: dc.id,
|
|
|
|
pendingCommands: make(map[int64]*irc.Message),
|
|
|
|
}
|
|
|
|
var upstreamChannels map[int64][]string
|
|
|
|
if len(msg.Params) > 0 {
|
|
|
|
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) {
|
|
|
|
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,
|
|
|
|
}
|
2020-03-28 00:03:00 +00:00
|
|
|
uc.trySendLIST(dc.id)
|
Add LIST support
This commit adds support for downstream LIST messages from multiple
concurrent downstreams to multiple concurrent upstreams, including
support for multiple pending LIST requests from the same downstream.
Because a unique RPL_LISTEND message must be sent to the requesting
downstream, and that there might be multiple upstreams, each sending
their own RPL_LISTEND, a cache of RPL_LISTEND replies of some sort is
required to match RPL_LISTEND together in order to only send one back
downstream.
This commit adds a list of "pending LIST" structs, which each contain a
map of all upstreams that yet need to send a RPL_LISTEND, and the
corresponding LIST request associated with that response. This list of
pending LISTs is sorted according to the order that the requesting
downstreams sent the LIST messages in. Each pending set also stores the
id of the requesting downstream, in order to only forward the replies to
it and no other downstream. (This is important because LIST replies can
typically amount to several thousands messages on large servers.)
When a single downstream makes multiple LIST requests, only the first
one will be immediately sent to the upstream servers. The next ones will
be buffered until the first one is completed. Distinct downstreams can
make concurrent LIST requests without any request buffering.
Each RPL_LIST message is forwarded to the downstream of the first
matching pending LIST struct.
When an upstream sends an RPL_LISTEND message, the upstream is removed
from the first matching pending LIST struct, but that message is not
immediately forwarded downstream. If there are no remaining pending LIST
requests in that struct is then empty, that means all upstreams have
sent back all their RPL_LISTEND replies (which means they also sent all
their RPL_LIST replies); so a unique RPL_LISTEND is sent to downstream
and that pending LIST set is removed from the cache.
Upstreams are removed from the pending LIST structs in two other cases:
- when they are closed (to avoid stalling because of a disconnected
upstream that will never reply to the LIST message): they are removed
from all pending LIST structs
- when they reply with an ERR_UNKNOWNCOMMAND or RPL_TRYAGAIN LIST reply,
which is typically used when a user is not allowed to LIST because they
just joined the server: they are removed from the first pending LIST
struct, as if an RPL_LISTEND message was received
2020-03-26 01:40:30 +00:00
|
|
|
})
|
2020-03-21 00:24:29 +00:00
|
|
|
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
|
2020-03-26 03:30:11 +00:00
|
|
|
uc.SendMessageLabeled(dc.id, &irc.Message{
|
2020-03-21 00:24:29 +00:00
|
|
|
Command: "NAMES",
|
|
|
|
Params: []string{upstreamChannel},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2020-03-19 23:23:19 +00:00
|
|
|
case "WHO":
|
|
|
|
if len(msg.Params) == 0 {
|
|
|
|
// TODO: support WHO without parameters
|
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
|
|
|
Command: irc.RPL_ENDOFWHO,
|
2020-03-21 00:24:29 +00:00
|
|
|
Params: []string{dc.nick, "*", "End of /WHO list"},
|
2020-03-19 23:23:19 +00:00
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: support WHO masks
|
|
|
|
entity := msg.Params[0]
|
|
|
|
|
2020-03-21 23:46:56 +00:00
|
|
|
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,
|
2020-03-28 16:28:28 +00:00
|
|
|
Params: []string{dc.nick, "*", dc.user.Username, dc.hostname, dc.srv.Hostname, dc.nick, "H", "0 " + dc.realname},
|
2020-03-21 23:46:56 +00:00
|
|
|
})
|
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
|
|
|
Command: irc.RPL_ENDOFWHO,
|
|
|
|
Params: []string{dc.nick, dc.nick, "End of /WHO list"},
|
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-03-19 23:23:19 +00:00
|
|
|
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}
|
|
|
|
}
|
|
|
|
|
2020-03-26 03:30:11 +00:00
|
|
|
uc.SendMessageLabeled(dc.id, &irc.Message{
|
2020-03-19 23:23:19 +00:00
|
|
|
Command: "WHO",
|
|
|
|
Params: params,
|
|
|
|
})
|
2020-03-20 01:15:23 +00:00
|
|
|
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]
|
|
|
|
}
|
|
|
|
|
2020-03-21 23:46:56 +00:00
|
|
|
if mask == dc.nick {
|
|
|
|
dc.SendMessage(&irc.Message{
|
|
|
|
Prefix: dc.srv.prefix(),
|
|
|
|
Command: irc.RPL_WHOISUSER,
|
2020-03-28 16:28:28 +00:00
|
|
|
Params: []string{dc.nick, dc.nick, dc.user.Username, dc.hostname, "*", dc.realname},
|
2020-03-21 23:46:56 +00:00
|
|
|
})
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-03-20 01:15:23 +00:00
|
|
|
// TODO: support WHOIS masks
|
|
|
|
uc, upstreamNick, err := dc.unmarshalEntity(mask)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var params []string
|
|
|
|
if target != "" {
|
|
|
|
params = []string{target, upstreamNick}
|
|
|
|
} else {
|
|
|
|
params = []string{upstreamNick}
|
|
|
|
}
|
|
|
|
|
2020-03-26 03:30:11 +00:00
|
|
|
uc.SendMessageLabeled(dc.id, &irc.Message{
|
2020-03-20 01:15:23 +00:00
|
|
|
Command: "WHOIS",
|
|
|
|
Params: params,
|
|
|
|
})
|
2020-02-17 14:56:18 +00:00
|
|
|
case "PRIVMSG":
|
|
|
|
var targetsStr, text string
|
|
|
|
if err := parseMessageParams(msg, &targetsStr, &text); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, name := range strings.Split(targetsStr, ",") {
|
2020-03-18 11:23:08 +00:00
|
|
|
if name == serviceNick {
|
|
|
|
handleServicePRIVMSG(dc, text)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-03-19 23:23:19 +00:00
|
|
|
uc, upstreamName, err := dc.unmarshalEntity(name)
|
2020-02-17 14:56:18 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-13 14:12:44 +00:00
|
|
|
if upstreamName == "NickServ" {
|
|
|
|
dc.handleNickServPRIVMSG(uc, text)
|
|
|
|
}
|
|
|
|
|
2020-02-19 17:25:19 +00:00
|
|
|
uc.SendMessage(&irc.Message{
|
2020-02-17 14:56:18 +00:00
|
|
|
Command: "PRIVMSG",
|
2020-02-19 17:25:19 +00:00
|
|
|
Params: []string{upstreamName, text},
|
2020-02-17 15:17:31 +00:00
|
|
|
})
|
2020-03-16 13:28:45 +00:00
|
|
|
|
2020-03-17 15:15:54 +00:00
|
|
|
echoMsg := &irc.Message{
|
|
|
|
Prefix: &irc.Prefix{
|
|
|
|
Name: uc.nick,
|
|
|
|
User: uc.username,
|
|
|
|
},
|
2020-03-17 15:17:39 +00:00
|
|
|
Command: "PRIVMSG",
|
2020-03-17 15:15:54 +00:00
|
|
|
Params: []string{upstreamName, text},
|
|
|
|
}
|
2020-03-16 13:28:45 +00:00
|
|
|
dc.lock.Lock()
|
2020-03-17 15:15:54 +00:00
|
|
|
dc.ourMessages[echoMsg] = struct{}{}
|
2020-03-16 13:28:45 +00:00
|
|
|
dc.lock.Unlock()
|
|
|
|
|
2020-03-25 09:53:08 +00:00
|
|
|
uc.network.ring.Produce(echoMsg)
|
2020-02-17 14:56:18 +00:00
|
|
|
}
|
2020-03-26 05:20:28 +00:00
|
|
|
case "NOTICE":
|
|
|
|
var targetsStr, text string
|
|
|
|
if err := parseMessageParams(msg, &targetsStr, &text); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, name := range strings.Split(targetsStr, ",") {
|
|
|
|
uc, upstreamName, err := dc.unmarshalEntity(name)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
uc.SendMessage(&irc.Message{
|
|
|
|
Command: "NOTICE",
|
|
|
|
Params: []string{upstreamName, text},
|
|
|
|
})
|
|
|
|
}
|
2020-03-26 05:03:07 +00:00
|
|
|
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
|
|
|
|
|
2020-03-26 03:30:11 +00:00
|
|
|
uc.SendMessageLabeled(dc.id, &irc.Message{
|
2020-03-26 05:03:07 +00:00
|
|
|
Command: "INVITE",
|
|
|
|
Params: []string{upstreamUser, upstreamChannel},
|
|
|
|
})
|
2020-02-06 15:18:19 +00:00
|
|
|
default:
|
2020-02-17 11:36:42 +00:00
|
|
|
dc.logger.Printf("unhandled message: %v", msg)
|
2020-02-06 15:18:19 +00:00
|
|
|
return newUnknownCommandError(msg.Command)
|
|
|
|
}
|
2020-02-07 11:19:42 +00:00
|
|
|
return nil
|
2020-02-06 15:18:19 +00:00
|
|
|
}
|
2020-03-13 14:12:44 +00:00
|
|
|
|
|
|
|
func (dc *downstreamConn) handleNickServPRIVMSG(uc *upstreamConn, text string) {
|
|
|
|
username, password, ok := parseNickServCredentials(text, uc.nick)
|
|
|
|
if !ok {
|
|
|
|
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
|
2020-03-28 09:40:33 +00:00
|
|
|
password = params[0]
|
2020-03-13 14:12:44 +00:00
|
|
|
} else {
|
|
|
|
username = params[0]
|
2020-03-28 09:40:33 +00:00
|
|
|
password = params[1]
|
|
|
|
}
|
|
|
|
case "SET":
|
|
|
|
if len(params) == 2 && strings.EqualFold(params[0], "PASSWORD") {
|
|
|
|
username = nick
|
|
|
|
password = params[1]
|
2020-03-13 14:12:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return username, password, true
|
|
|
|
}
|