Merge pull request #1120 from oragono/master+relaymsg

Initial RELAYMSG implementation
This commit is contained in:
Shivaram Lingamneni 2020-09-09 08:40:08 -07:00 committed by GitHub
commit 2d277566b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 190 additions and 22 deletions

View File

@ -143,6 +143,19 @@ server:
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i
motd-formatting: true
# relaying using the RELAYMSG command
relaymsg:
# is relaymsg enabled at all?
enabled: true
# which character(s) are reserved for relayed nicks?
separators: "/"
# can channel operators use RELAYMSG in their channels?
# our implementation of RELAYMSG makes it safe for chanops to use without the
# possibility of real users being silently spoofed
available-to-chanops: true
# addresses/CIDRs the PROXY command can be used from
# this should be restricted to localhost (127.0.0.1/8, ::1/128, and unix sockets),
# unless you have a good reason. you should also add these addresses to the
@ -515,6 +528,7 @@ oper-classes:
- "local_unban"
- "nofakelag"
- "roleplay"
- "relaymsg-anywhere"
# network operator
"network-oper":
@ -734,7 +748,7 @@ fakelag:
roleplay:
# are roleplay commands enabled at all? (channels and clients still have to
# opt in individually with the +E mode)
enabled: true
enabled: false
# require the "roleplay" oper capability to send roleplay messages?
require-oper: false

View File

@ -170,6 +170,19 @@ server:
# if this is true, the motd is escaped using formatting codes like $c, $b, and $i
motd-formatting: true
# relaying using the RELAYMSG command
relaymsg:
# is relaymsg enabled at all?
enabled: true
# which character(s) are reserved for relayed nicks?
separators: "/"
# can channel operators use RELAYMSG in their channels?
# our implementation of RELAYMSG makes it safe for chanops to use without the
# possibility of real users being silently spoofed
available-to-chanops: true
# addresses/CIDRs the PROXY command can be used from
# this should be restricted to localhost (127.0.0.1/8, ::1/128, and unix sockets),
# unless you have a good reason. you should also add these addresses to the
@ -543,6 +556,7 @@ oper-classes:
- "local_unban"
- "nofakelag"
- "roleplay"
- "relaymsg-anywhere"
# network operator
"network-oper":
@ -762,7 +776,7 @@ fakelag:
roleplay:
# are roleplay commands enabled at all? (channels and clients still have to
# opt in individually with the +E mode)
enabled: true
enabled: false
# require the "roleplay" oper capability to send roleplay messages?
require-oper: false

View File

@ -93,6 +93,12 @@ CAPDEFS = [
url="https://ircv3.net/specs/extensions/multi-prefix-3.1.html",
standard="IRCv3",
),
CapDef(
identifier="Relaymsg",
name="draft/relaymsg",
url="https://github.com/ircv3/ircv3-specifications/pull/417",
standard="proposed IRCv3",
),
CapDef(
identifier="Rename",
name="draft/rename",

View File

@ -7,7 +7,7 @@ package caps
const (
// number of recognized capabilities:
numCapabs = 26
numCapabs = 27
// length of the uint64 array that represents the bitset:
bitsetLen = 1
)
@ -53,6 +53,10 @@ const (
// https://github.com/ircv3/ircv3-specifications/pull/398
Multiline Capability = iota
// Relaymsg is the proposed IRCv3 capability named "draft/relaymsg":
// https://github.com/ircv3/ircv3-specifications/pull/417
Relaymsg Capability = iota
// Rename is the proposed IRCv3 capability named "draft/rename":
// https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
Rename Capability = iota
@ -131,6 +135,7 @@ var (
"draft/event-playback",
"draft/languages",
"draft/multiline",
"draft/relaymsg",
"draft/rename",
"draft/resume-0.5",
"echo-message",

View File

@ -165,6 +165,10 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
return "", errNicknameInvalid, false
}
if config.isRelaymsgIdentifier(newNick) {
return "", errNicknameInvalid, false
}
if restrictedCasefoldedNicks.Has(newCfNick) || restrictedSkeletons.Has(newSkeleton) {
return "", errNicknameInvalid, false
}

View File

@ -252,6 +252,10 @@ func init() {
minParams: 2,
allowedInBatch: true,
},
"RELAYMSG": {
handler: relaymsgHandler,
minParams: 3,
},
"RENAME": {
handler: renameHandler,
minParams: 2,

View File

@ -502,6 +502,11 @@ type Config struct {
MOTD string
motdLines []string
MOTDFormatting bool `yaml:"motd-formatting"`
Relaymsg struct {
Enabled bool
Separators string
AvailableToChanops bool `yaml:"available-to-chanops"`
}
ProxyAllowedFrom []string `yaml:"proxy-allowed-from"`
proxyAllowedFromNets []net.IPNet
WebIRC []webircConfig `yaml:"webirc"`
@ -1106,6 +1111,17 @@ func LoadConfig(filename string) (config *Config, err error) {
}
config.Server.capValues[caps.Languages] = config.languageManager.CapValue()
if config.Server.Relaymsg.Enabled {
for _, char := range protocolBreakingNameCharacters {
if strings.ContainsRune(config.Server.Relaymsg.Separators, char) {
return nil, fmt.Errorf("RELAYMSG separators cannot include the characters %s", protocolBreakingNameCharacters)
}
}
config.Server.capValues[caps.Relaymsg] = config.Server.Relaymsg.Separators
} else {
config.Server.supportedCaps.Disable(caps.Relaymsg)
}
config.Debug.recoverFromErrors = utils.BoolDefaultTrue(config.Debug.RecoverFromErrors)
// process operator definitions, store them to config.operators
@ -1206,6 +1222,19 @@ func (config *Config) getOutputPath(filename string) string {
return filepath.Join(config.Server.OutputPath, filename)
}
func (config *Config) isRelaymsgIdentifier(nick string) bool {
if !config.Server.Relaymsg.Enabled {
return false
}
for _, char := range config.Server.Relaymsg.Separators {
if strings.ContainsRune(nick, char) {
return true
}
}
return false
}
// setISupport sets up our RPL_ISUPPORT reply.
func (config *Config) generateISupport() (err error) {
maxTargetsString := strconv.Itoa(maxTargets)

View File

@ -2013,6 +2013,16 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
if i == maxTargets {
break
}
config := server.Config()
if config.isRelaymsgIdentifier(targetString) {
if histType == history.Privmsg {
rb.Add(nil, server.name, ERR_NOSUCHNICK, client.Nick(), targetString, client.t("Relayed users cannot receive private messages"))
}
// TAGMSG/NOTICEs are intentionally silently dropped
continue
}
// each target gets distinct msgids
splitMsg := utils.MakeMessage(message)
dispatchMessageToTarget(client, clientOnlyTags, histType, msg.Command, targetString, splitMsg, rb)
@ -2421,6 +2431,72 @@ func rehashHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
return false
}
// RELAYMSG <channel> <spoofed nick> :<message>
func relaymsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (result bool) {
config := server.Config()
if !config.Server.Relaymsg.Enabled {
rb.Add(nil, server.name, "FAIL", "RELAYMSG", "NOT_ENABLED", client.t("RELAYMSG has been disabled"))
return false
}
channel := server.channels.Get(msg.Params[0])
if channel == nil {
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(msg.Params[0]), client.t("No such channel"))
return false
}
allowedToRelay := client.HasRoleCapabs("relaymsg-anywhere") || (config.Server.Relaymsg.AvailableToChanops && channel.ClientIsAtLeast(client, modes.ChannelOperator))
if !allowedToRelay {
rb.Add(nil, server.name, "FAIL", "RELAYMSG", "NOT_PRIVED", client.t("You cannot relay messages to this channel"))
return false
}
rawMessage := msg.Params[2]
if strings.TrimSpace(rawMessage) == "" {
rb.Add(nil, server.name, "FAIL", "RELAYMSG", "BLANK_MSG", client.t("The message must not be blank"))
return false
}
message := utils.MakeMessage(rawMessage)
nick := msg.Params[1]
_, err := CasefoldName(nick)
if err != nil {
rb.Add(nil, server.name, "FAIL", "RELAYMSG", "INVALID_NICK", client.t("Invalid nickname"))
return false
}
if !config.isRelaymsgIdentifier(nick) {
rb.Add(nil, server.name, "FAIL", "RELAYMSG", "INVALID_NICK", fmt.Sprintf(client.t("Relayed nicknames MUST contain a relaymsg separator from this set: %s"), config.Server.Relaymsg.Separators))
return false
}
channel.AddHistoryItem(history.Item{
Type: history.Privmsg,
Message: message,
Nick: nick,
}, "")
// send msg
channelName := channel.Name()
relayTags := map[string]string{
"relaymsg": client.Nick(),
}
for _, member := range channel.Members() {
for _, session := range member.Sessions() {
var tagsToUse map[string]string
if session.capabilities.Has(caps.Relaymsg) {
tagsToUse = relayTags
}
if session == rb.session {
rb.AddSplitMessageFromClient(nick, "*", tagsToUse, "PRIVMSG", channelName, message)
} else {
session.sendSplitMsgFromClientInternal(false, nick, "*", tagsToUse, "PRIVMSG", channelName, message)
}
}
}
return false
}
// RENAME <oldchan> <newchan> [<reason>]
func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
oldName, newName := msg.Params[0], msg.Params[1]

View File

@ -417,6 +417,16 @@ Replies to a PING. Used to check link connectivity.`,
text: `PRIVMSG <target>{,<target>} <text to be sent>
Sends the text to the given targets as a PRIVMSG.`,
},
"relaymsg": {
text: `RELAYMSG <channel> <spoofed nick> :<message>
This command lets channel operators relay messages to their
channel from other messaging systems using relay bots. The
spoofed nickname MUST contain a forwardslash.
For example:
RELAYMSG #ircv3 Mallory/D :Welp, we linked Discord...`,
},
"rename": {
text: `RENAME <channel> <newname> [<reason>]

View File

@ -82,7 +82,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
if rb.session == session {
rb.AddSplitMessageFromClient(source, "", nil, "PRIVMSG", targetString, splitMessage)
} else {
session.sendSplitMsgFromClientInternal(false, source, "", nil, "PRIVMSG", targetString, splitMessage)
session.sendSplitMsgFromClientInternal(false, source, "*", nil, "PRIVMSG", targetString, splitMessage)
}
}
}
@ -108,7 +108,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
cnick := client.Nick()
tnick := user.Nick()
for _, session := range user.Sessions() {
session.sendSplitMsgFromClientInternal(false, source, "", nil, "PRIVMSG", tnick, splitMessage)
session.sendSplitMsgFromClientInternal(false, source, "*", nil, "PRIVMSG", tnick, splitMessage)
}
if away, awayMessage := user.Away(); away {
//TODO(dan): possibly implement cooldown of away notifications to users

View File

@ -485,6 +485,10 @@ func (server *Server) applyConfig(config *Config) (err error) {
return fmt.Errorf("UTF-8 enforcement cannot be changed after launching the server, rehash aborted")
} else if oldConfig.Accounts.Multiclient.AlwaysOn != config.Accounts.Multiclient.AlwaysOn {
return fmt.Errorf("Default always-on setting cannot be changed after launching the server, rehash aborted")
} else if oldConfig.Server.Relaymsg.Enabled != config.Server.Relaymsg.Enabled {
return fmt.Errorf("Cannot enable or disable relaying after launching the server, rehash aborted")
} else if oldConfig.Server.Relaymsg.Separators != config.Server.Relaymsg.Separators {
return fmt.Errorf("Cannot change relaying separators after launching the server, rehash aborted")
}
}

View File

@ -19,6 +19,16 @@ import (
const (
precisUTF8MappingToken = "rfc8265"
// space can't be used
// , is used as a separator
// * is used in mask matching
// ? is used in mask matching
// . denotes a server name
// ! separates nickname from username
// @ separates username from hostname
// : means trailing
protocolBreakingNameCharacters = " ,*?.!@:"
)
var (
@ -138,18 +148,10 @@ func CasefoldName(name string) (string, error) {
return "", errStringIsEmpty
}
// space can't be used
// , is used as a separator
// * is used in mask matching
// ? is used in mask matching
// . denotes a server name
// ! separates nickname from username
// @ separates username from hostname
// : means trailing
// # is a channel prefix
// ~&@%+ are channel membership prefixes
// - I feel like disallowing
if strings.ContainsAny(lowered, " ,*?.!@:") || strings.ContainsAny(string(lowered[0]), "#~&@%+-") {
if strings.ContainsAny(lowered, protocolBreakingNameCharacters) || strings.ContainsAny(string(lowered[0]), "#~&@%+-") {
return "", errInvalidCharacter
}