2016-06-15 13:50:56 +02:00
|
|
|
// Copyright (c) 2012-2014 Jeremy Latt
|
|
|
|
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
|
|
|
|
// released under the MIT license
|
|
|
|
|
2014-03-13 01:38:11 +01:00
|
|
|
package irc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"strings"
|
|
|
|
|
2016-06-19 02:01:30 +02:00
|
|
|
"github.com/DanielOaks/girc-go/ircmsg"
|
2014-03-13 01:38:11 +01:00
|
|
|
)
|
|
|
|
|
2017-03-25 00:19:13 +01:00
|
|
|
// Capability represents an optional feature that a client may request from the server.
|
2014-03-13 01:38:11 +01:00
|
|
|
type Capability string
|
|
|
|
|
|
|
|
const (
|
2016-09-12 03:25:31 +02:00
|
|
|
AccountTag Capability = "account-tag"
|
2016-10-13 10:18:00 +02:00
|
|
|
AccountNotify Capability = "account-notify"
|
2016-09-12 03:56:20 +02:00
|
|
|
AwayNotify Capability = "away-notify"
|
2016-10-22 14:18:41 +02:00
|
|
|
CapNotify Capability = "cap-notify"
|
2016-10-23 03:28:31 +02:00
|
|
|
ChgHost Capability = "chghost"
|
2016-10-22 14:29:01 +02:00
|
|
|
EchoMessage Capability = "echo-message"
|
2016-08-14 03:59:33 +02:00
|
|
|
ExtendedJoin Capability = "extended-join"
|
2016-10-16 06:14:55 +02:00
|
|
|
InviteNotify Capability = "invite-notify"
|
2016-11-29 09:38:04 +01:00
|
|
|
MaxLine Capability = "draft/maxline"
|
2017-01-14 10:52:47 +01:00
|
|
|
MessageIDs Capability = "draft/message-ids"
|
2017-01-13 15:22:42 +01:00
|
|
|
MessageTags Capability = "draft/message-tags-0.2"
|
2016-08-14 03:59:33 +02:00
|
|
|
MultiPrefix Capability = "multi-prefix"
|
|
|
|
SASL Capability = "sasl"
|
|
|
|
ServerTime Capability = "server-time"
|
2017-03-09 10:07:35 +01:00
|
|
|
STS Capability = "draft/sts"
|
2016-08-14 03:59:33 +02:00
|
|
|
UserhostInNames Capability = "userhost-in-names"
|
2014-03-13 01:38:11 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2017-03-25 00:19:13 +01:00
|
|
|
// SupportedCapabilities are the caps we advertise.
|
2014-03-13 01:38:11 +01:00
|
|
|
SupportedCapabilities = CapabilitySet{
|
2016-10-22 14:18:41 +02:00
|
|
|
AccountTag: true,
|
|
|
|
AccountNotify: true,
|
|
|
|
AwayNotify: true,
|
|
|
|
CapNotify: true,
|
2016-10-23 03:28:31 +02:00
|
|
|
ChgHost: true,
|
2016-10-22 14:29:01 +02:00
|
|
|
EchoMessage: true,
|
2016-10-22 14:18:41 +02:00
|
|
|
ExtendedJoin: true,
|
|
|
|
InviteNotify: true,
|
2017-01-14 10:52:47 +01:00
|
|
|
MessageIDs: true,
|
2016-11-29 09:38:04 +01:00
|
|
|
// MaxLine is set during server startup
|
|
|
|
MessageTags: true,
|
|
|
|
MultiPrefix: true,
|
2016-10-22 14:18:41 +02:00
|
|
|
// SASL is set during server startup
|
2017-03-09 10:07:35 +01:00
|
|
|
ServerTime: true,
|
|
|
|
// STS is set during server startup
|
2016-08-14 03:59:33 +02:00
|
|
|
UserhostInNames: true,
|
2014-03-13 01:38:11 +01:00
|
|
|
}
|
2017-03-25 00:19:13 +01:00
|
|
|
// CapValues are the actual values we advertise to v3.2 clients.
|
2016-10-16 05:54:09 +02:00
|
|
|
CapValues = map[Capability]string{
|
|
|
|
SASL: "PLAIN,EXTERNAL",
|
|
|
|
}
|
2014-03-13 01:38:11 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
func (capability Capability) String() string {
|
|
|
|
return string(capability)
|
|
|
|
}
|
|
|
|
|
2017-03-25 00:19:13 +01:00
|
|
|
// CapState shows whether we're negotiating caps, finished, etc for connection registration.
|
2014-03-13 01:38:11 +01:00
|
|
|
type CapState uint
|
|
|
|
|
|
|
|
const (
|
|
|
|
CapNone CapState = iota
|
|
|
|
CapNegotiating CapState = iota
|
|
|
|
CapNegotiated CapState = iota
|
|
|
|
)
|
|
|
|
|
2016-10-16 05:54:09 +02:00
|
|
|
// CapVersion is used to select which max version of CAP the client supports.
|
|
|
|
type CapVersion uint
|
|
|
|
|
|
|
|
const (
|
|
|
|
// Cap301 refers to the base CAP spec.
|
|
|
|
Cap301 CapVersion = 301
|
|
|
|
// Cap302 refers to the IRCv3.2 CAP spec.
|
|
|
|
Cap302 CapVersion = 302
|
|
|
|
)
|
|
|
|
|
|
|
|
// CapabilitySet is used to track supported, enabled, and existing caps.
|
2014-03-13 01:38:11 +01:00
|
|
|
type CapabilitySet map[Capability]bool
|
|
|
|
|
2016-10-16 05:54:09 +02:00
|
|
|
func (set CapabilitySet) String(version CapVersion) string {
|
2014-03-13 01:38:11 +01:00
|
|
|
strs := make([]string, len(set))
|
|
|
|
index := 0
|
|
|
|
for capability := range set {
|
2016-10-16 05:54:09 +02:00
|
|
|
capString := string(capability)
|
|
|
|
if version == Cap302 {
|
|
|
|
val, exists := CapValues[capability]
|
|
|
|
if exists {
|
|
|
|
capString += "=" + val
|
|
|
|
}
|
|
|
|
}
|
|
|
|
strs[index] = capString
|
|
|
|
index++
|
2014-03-13 01:38:11 +01:00
|
|
|
}
|
|
|
|
return strings.Join(strs, " ")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (set CapabilitySet) DisableString() string {
|
|
|
|
parts := make([]string, len(set))
|
|
|
|
index := 0
|
|
|
|
for capability := range set {
|
2017-03-25 00:19:13 +01:00
|
|
|
parts[index] = "-" + capability.String()
|
|
|
|
index++
|
2014-03-13 01:38:11 +01:00
|
|
|
}
|
|
|
|
return strings.Join(parts, " ")
|
|
|
|
}
|
|
|
|
|
2016-06-19 02:01:30 +02:00
|
|
|
// CAP <subcmd> [<caps>]
|
|
|
|
func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
|
|
|
subCommand := strings.ToUpper(msg.Params[0])
|
|
|
|
capabilities := make(CapabilitySet)
|
|
|
|
var capString string
|
|
|
|
|
|
|
|
if len(msg.Params) > 1 {
|
|
|
|
capString = msg.Params[1]
|
|
|
|
strs := strings.Split(capString, " ")
|
|
|
|
for _, str := range strs {
|
|
|
|
if len(str) > 0 {
|
|
|
|
capabilities[Capability(str)] = true
|
2014-03-13 01:38:11 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-04-13 23:30:14 +02:00
|
|
|
|
2016-06-19 02:01:30 +02:00
|
|
|
switch subCommand {
|
|
|
|
case "LS":
|
|
|
|
if !client.registered {
|
|
|
|
client.capState = CapNegotiating
|
|
|
|
}
|
2016-10-16 05:54:09 +02:00
|
|
|
if len(msg.Params) > 1 && msg.Params[1] == "302" {
|
|
|
|
client.capVersion = 302
|
|
|
|
}
|
|
|
|
// weechat 1.4 has a bug here where it won't accept the CAP reply unless it contains
|
|
|
|
// the server.name source... otherwise it doesn't respond to the CAP message with
|
|
|
|
// anything and just hangs on connection.
|
|
|
|
//TODO(dan): limit number of caps and send it multiline in 3.2 style as appropriate.
|
|
|
|
client.Send(nil, server.name, "CAP", client.nick, subCommand, SupportedCapabilities.String(client.capVersion))
|
2016-04-13 23:30:14 +02:00
|
|
|
|
2016-06-19 02:01:30 +02:00
|
|
|
case "LIST":
|
2016-10-16 05:54:09 +02:00
|
|
|
client.Send(nil, server.name, "CAP", client.nick, subCommand, client.capabilities.String(Cap301)) // values not sent on LIST so force 3.1
|
2016-04-13 23:30:14 +02:00
|
|
|
|
2016-06-19 02:01:30 +02:00
|
|
|
case "REQ":
|
|
|
|
// make sure all capabilities actually exist
|
|
|
|
for capability := range capabilities {
|
2016-04-13 23:30:14 +02:00
|
|
|
if !SupportedCapabilities[capability] {
|
2016-10-11 15:51:46 +02:00
|
|
|
client.Send(nil, server.name, "CAP", client.nick, "NAK", capString)
|
2016-06-19 02:01:30 +02:00
|
|
|
return false
|
2016-04-13 23:30:14 +02:00
|
|
|
}
|
|
|
|
}
|
2016-06-19 02:01:30 +02:00
|
|
|
for capability := range capabilities {
|
2016-04-13 23:30:14 +02:00
|
|
|
client.capabilities[capability] = true
|
|
|
|
}
|
2016-10-11 15:51:46 +02:00
|
|
|
client.Send(nil, server.name, "CAP", client.nick, "ACK", capString)
|
2016-04-13 23:30:14 +02:00
|
|
|
|
2016-06-19 02:01:30 +02:00
|
|
|
case "END":
|
|
|
|
if !client.registered {
|
|
|
|
client.capState = CapNegotiated
|
|
|
|
server.tryRegister(client)
|
|
|
|
}
|
2016-04-13 23:30:14 +02:00
|
|
|
|
|
|
|
default:
|
2016-10-11 15:51:46 +02:00
|
|
|
client.Send(nil, server.name, ERR_INVALIDCAPCMD, client.nick, subCommand, "Invalid CAP subcommand")
|
2016-04-13 23:30:14 +02:00
|
|
|
}
|
2016-06-19 02:01:30 +02:00
|
|
|
return false
|
2016-04-13 23:30:14 +02:00
|
|
|
}
|