2020-03-18 11:23:08 +00:00
|
|
|
package soju
|
|
|
|
|
|
|
|
import (
|
2020-05-29 11:10:54 +00:00
|
|
|
"crypto"
|
|
|
|
"crypto/ecdsa"
|
|
|
|
"crypto/ed25519"
|
|
|
|
"crypto/elliptic"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/sha1"
|
|
|
|
"crypto/sha256"
|
|
|
|
"crypto/x509"
|
|
|
|
"crypto/x509/pkix"
|
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
2020-03-18 23:57:14 +00:00
|
|
|
"flag"
|
2020-03-18 11:23:08 +00:00
|
|
|
"fmt"
|
2020-03-18 23:57:14 +00:00
|
|
|
"io/ioutil"
|
2020-05-29 11:10:54 +00:00
|
|
|
"math/big"
|
2020-06-24 10:08:35 +00:00
|
|
|
"sort"
|
2020-03-18 11:23:08 +00:00
|
|
|
"strings"
|
2020-05-29 11:10:54 +00:00
|
|
|
"time"
|
2020-03-18 11:23:08 +00:00
|
|
|
|
|
|
|
"github.com/google/shlex"
|
2020-04-08 12:20:00 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
2020-03-18 11:23:08 +00:00
|
|
|
"gopkg.in/irc.v3"
|
|
|
|
)
|
|
|
|
|
|
|
|
const serviceNick = "BouncerServ"
|
Implement casemapping
TL;DR: supports for casemapping, now logs are saved in
casemapped/canonical/tolower form
(eg. in the #channel directory instead of #Channel... or something)
== What is casemapping? ==
see <https://modern.ircdocs.horse/#casemapping-parameter>
== Casemapping and multi-upstream ==
Since each upstream does not necessarily use the same casemapping, and
since casemappings cannot coexist [0],
1. soju must also update the database accordingly to upstreams'
casemapping, otherwise it will end up inconsistent,
2. soju must "normalize" entity names and expose only one casemapping
that is a subset of all supported casemappings (here, ascii).
[0] On some upstreams, "emersion[m]" and "emersion{m}" refer to the same
user (upstreams that advertise rfc1459 for example), while on others
(upstreams that advertise ascii) they don't.
Once upstream's casemapping is known (default to rfc1459), entity names
in map keys are made into casemapped form, for upstreamConn,
upstreamChannel and network.
downstreamConn advertises "CASEMAPPING=ascii", and always casemap map
keys with ascii.
Some functions require the caller to casemap their argument (to avoid
needless calls to casemapping functions).
== Message forwarding and casemapping ==
downstream message handling (joins and parts basically):
When relaying entity names from downstreams to upstreams, soju uses the
upstream casemapping, in order to not get in the way of the user. This
does not brings any issue, as long as soju replies with the ascii
casemapping in mind (solves point 1.).
marshalEntity/marshalUserPrefix:
When relaying entity names from upstreams with non-ascii casemappings,
soju *partially* casemap them: it only change the case of characters
which are not ascii letters. ASCII case is thus kept intact, while
special symbols like []{} are the same every time soju sends them to
downstreams (solves point 2.).
== Casemapping changes ==
Casemapping changes are not fully supported by this patch and will
result in loss of history. This is a limitation of the protocol and
should be solved by the RENAME spec.
2021-03-16 09:00:34 +00:00
|
|
|
const serviceNickCM = "bouncerserv"
|
2020-06-29 16:09:48 +00:00
|
|
|
const serviceRealname = "soju bouncer service"
|
2020-03-18 11:23:08 +00:00
|
|
|
|
2020-04-04 02:57:20 +00:00
|
|
|
var servicePrefix = &irc.Prefix{
|
|
|
|
Name: serviceNick,
|
|
|
|
User: serviceNick,
|
|
|
|
Host: serviceNick,
|
|
|
|
}
|
|
|
|
|
2020-03-25 19:58:07 +00:00
|
|
|
type serviceCommandSet map[string]*serviceCommand
|
|
|
|
|
2020-03-18 11:23:08 +00:00
|
|
|
type serviceCommand struct {
|
2020-03-25 19:58:07 +00:00
|
|
|
usage string
|
|
|
|
desc string
|
|
|
|
handle func(dc *downstreamConn, params []string) error
|
|
|
|
children serviceCommandSet
|
2020-06-06 23:27:07 +00:00
|
|
|
admin bool
|
2020-03-18 11:23:08 +00:00
|
|
|
}
|
|
|
|
|
2020-04-04 02:48:25 +00:00
|
|
|
func sendServiceNOTICE(dc *downstreamConn, text string) {
|
|
|
|
dc.SendMessage(&irc.Message{
|
2020-04-04 02:57:20 +00:00
|
|
|
Prefix: servicePrefix,
|
2020-04-04 02:48:25 +00:00
|
|
|
Command: "NOTICE",
|
|
|
|
Params: []string{dc.nick, text},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-03-18 11:23:08 +00:00
|
|
|
func sendServicePRIVMSG(dc *downstreamConn, text string) {
|
|
|
|
dc.SendMessage(&irc.Message{
|
2020-04-04 02:57:20 +00:00
|
|
|
Prefix: servicePrefix,
|
2020-03-18 11:23:08 +00:00
|
|
|
Command: "PRIVMSG",
|
|
|
|
Params: []string{dc.nick, text},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleServicePRIVMSG(dc *downstreamConn, text string) {
|
|
|
|
words, err := shlex.Split(text)
|
|
|
|
if err != nil {
|
|
|
|
sendServicePRIVMSG(dc, fmt.Sprintf("error: failed to parse command: %v", err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-03-25 19:58:07 +00:00
|
|
|
cmd, params, err := serviceCommands.Get(words)
|
|
|
|
if err != nil {
|
|
|
|
sendServicePRIVMSG(dc, fmt.Sprintf(`error: %v (type "help" for a list of commands)`, err))
|
2020-03-18 11:23:08 +00:00
|
|
|
return
|
|
|
|
}
|
2020-06-06 23:27:07 +00:00
|
|
|
if cmd.admin && !dc.user.Admin {
|
|
|
|
sendServicePRIVMSG(dc, fmt.Sprintf(`error: you must be an admin to use this command`))
|
|
|
|
return
|
|
|
|
}
|
2020-03-18 11:23:08 +00:00
|
|
|
|
2020-06-09 11:39:28 +00:00
|
|
|
if cmd.handle == nil {
|
|
|
|
if len(cmd.children) > 0 {
|
|
|
|
var l []string
|
2020-06-10 14:34:45 +00:00
|
|
|
appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
|
2020-06-09 11:39:28 +00:00
|
|
|
sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
|
|
|
|
} else {
|
|
|
|
// Pretend the command does not exist if it has neither children nor handler.
|
|
|
|
// This is obviously a bug but it is better to not die anyway.
|
|
|
|
dc.logger.Printf("command without handler and subcommands invoked:", words[0])
|
|
|
|
sendServicePRIVMSG(dc, fmt.Sprintf("command %q not found", words[0]))
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-03-18 11:23:08 +00:00
|
|
|
if err := cmd.handle(dc, params); err != nil {
|
|
|
|
sendServicePRIVMSG(dc, fmt.Sprintf("error: %v", err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-25 19:58:07 +00:00
|
|
|
func (cmds serviceCommandSet) Get(params []string) (*serviceCommand, []string, error) {
|
|
|
|
if len(params) == 0 {
|
|
|
|
return nil, nil, fmt.Errorf("no command specified")
|
|
|
|
}
|
|
|
|
|
|
|
|
name := params[0]
|
|
|
|
params = params[1:]
|
|
|
|
|
|
|
|
cmd, ok := cmds[name]
|
|
|
|
if !ok {
|
|
|
|
for k := range cmds {
|
|
|
|
if !strings.HasPrefix(k, name) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if cmd != nil {
|
|
|
|
return nil, params, fmt.Errorf("command %q is ambiguous", name)
|
|
|
|
}
|
|
|
|
cmd = cmds[k]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if cmd == nil {
|
|
|
|
return nil, params, fmt.Errorf("command %q not found", name)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(params) == 0 || len(cmd.children) == 0 {
|
|
|
|
return cmd, params, nil
|
|
|
|
}
|
|
|
|
return cmd.children.Get(params)
|
|
|
|
}
|
|
|
|
|
2020-06-24 10:08:35 +00:00
|
|
|
func (cmds serviceCommandSet) Names() []string {
|
|
|
|
l := make([]string, 0, len(cmds))
|
|
|
|
for name := range cmds {
|
|
|
|
l = append(l, name)
|
|
|
|
}
|
|
|
|
sort.Strings(l)
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
2020-03-25 19:58:07 +00:00
|
|
|
var serviceCommands serviceCommandSet
|
2020-03-18 11:23:08 +00:00
|
|
|
|
|
|
|
func init() {
|
2020-03-25 19:58:07 +00:00
|
|
|
serviceCommands = serviceCommandSet{
|
2020-03-18 11:23:08 +00:00
|
|
|
"help": {
|
|
|
|
usage: "[command]",
|
|
|
|
desc: "print help message",
|
|
|
|
handle: handleServiceHelp,
|
|
|
|
},
|
2020-03-25 19:58:07 +00:00
|
|
|
"network": {
|
|
|
|
children: serviceCommandSet{
|
|
|
|
"create": {
|
2020-06-02 09:39:53 +00:00
|
|
|
usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-connect-command command]...",
|
2020-03-25 19:58:07 +00:00
|
|
|
desc: "add a new network",
|
2020-06-06 23:19:25 +00:00
|
|
|
handle: handleServiceNetworkCreate,
|
2020-03-25 19:58:07 +00:00
|
|
|
},
|
2020-03-25 21:57:48 +00:00
|
|
|
"status": {
|
|
|
|
desc: "show a list of saved networks and their current status",
|
|
|
|
handle: handleServiceNetworkStatus,
|
|
|
|
},
|
2020-06-02 09:39:53 +00:00
|
|
|
"update": {
|
2020-08-02 14:53:08 +00:00
|
|
|
usage: "<name> [-addr addr] [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-connect-command command]...",
|
2020-06-04 15:23:27 +00:00
|
|
|
desc: "update a network",
|
2020-06-02 09:39:53 +00:00
|
|
|
handle: handleServiceNetworkUpdate,
|
|
|
|
},
|
2020-04-01 13:40:20 +00:00
|
|
|
"delete": {
|
|
|
|
usage: "<name>",
|
|
|
|
desc: "delete a network",
|
|
|
|
handle: handleServiceNetworkDelete,
|
|
|
|
},
|
2020-03-25 19:58:07 +00:00
|
|
|
},
|
2020-03-18 23:57:14 +00:00
|
|
|
},
|
2020-05-29 11:10:54 +00:00
|
|
|
"certfp": {
|
|
|
|
children: serviceCommandSet{
|
|
|
|
"generate": {
|
|
|
|
usage: "[-key-type rsa|ecdsa|ed25519] [-bits N] <network name>",
|
|
|
|
desc: "generate a new self-signed certificate, defaults to using RSA-3072 key",
|
|
|
|
handle: handleServiceCertfpGenerate,
|
|
|
|
},
|
|
|
|
"fingerprint": {
|
|
|
|
usage: "<network name>",
|
|
|
|
desc: "show fingerprints of certificate associated with the network",
|
|
|
|
handle: handleServiceCertfpFingerprints,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2020-07-22 10:16:13 +00:00
|
|
|
"sasl": {
|
|
|
|
children: serviceCommandSet{
|
|
|
|
"set-plain": {
|
|
|
|
usage: "<network name> <username> <password>",
|
|
|
|
desc: "set SASL PLAIN credentials",
|
|
|
|
handle: handleServiceSASLSetPlain,
|
|
|
|
},
|
2020-07-22 10:20:52 +00:00
|
|
|
"reset": {
|
|
|
|
usage: "<network name>",
|
|
|
|
desc: "disable SASL authentication and remove stored credentials",
|
|
|
|
handle: handleServiceSASLReset,
|
|
|
|
},
|
2020-07-22 10:16:13 +00:00
|
|
|
},
|
|
|
|
},
|
2020-06-06 23:30:27 +00:00
|
|
|
"user": {
|
|
|
|
children: serviceCommandSet{
|
|
|
|
"create": {
|
|
|
|
usage: "-username <username> -password <password> [-admin]",
|
|
|
|
desc: "create a new soju user",
|
|
|
|
handle: handleUserCreate,
|
|
|
|
admin: true,
|
|
|
|
},
|
2020-08-10 13:03:07 +00:00
|
|
|
"delete": {
|
|
|
|
usage: "<username>",
|
|
|
|
desc: "delete a user",
|
|
|
|
handle: handleUserDelete,
|
|
|
|
admin: true,
|
|
|
|
},
|
2020-06-06 23:30:27 +00:00
|
|
|
},
|
|
|
|
admin: true,
|
|
|
|
},
|
2020-04-08 12:20:00 +00:00
|
|
|
"change-password": {
|
|
|
|
usage: "<new password>",
|
|
|
|
desc: "change your password",
|
|
|
|
handle: handlePasswordChange,
|
|
|
|
},
|
2020-11-30 21:16:44 +00:00
|
|
|
"channel": {
|
|
|
|
children: serviceCommandSet{
|
2021-05-25 17:22:22 +00:00
|
|
|
"status": {
|
|
|
|
usage: "[-network name]",
|
|
|
|
desc: "show a list of saved channels and their current status",
|
|
|
|
handle: handleServiceChannelStatus,
|
|
|
|
},
|
2020-11-30 21:16:44 +00:00
|
|
|
"update": {
|
|
|
|
usage: "<name> [-relay-detached <default|none|highlight|message>] [-reattach-on <default|none|highlight|message>] [-detach-after <duration>] [-detach-on <default|none|highlight|message>]",
|
|
|
|
desc: "update a channel",
|
|
|
|
handle: handleServiceChannelUpdate,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2020-03-18 11:23:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-06 23:27:07 +00:00
|
|
|
func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, admin bool, l *[]string) {
|
2020-06-24 10:08:35 +00:00
|
|
|
for _, name := range cmds.Names() {
|
|
|
|
cmd := cmds[name]
|
2020-06-06 23:27:07 +00:00
|
|
|
if cmd.admin && !admin {
|
|
|
|
continue
|
|
|
|
}
|
2020-03-25 19:58:07 +00:00
|
|
|
words := append(prefix, name)
|
|
|
|
if len(cmd.children) == 0 {
|
|
|
|
s := strings.Join(words, " ")
|
|
|
|
*l = append(*l, s)
|
|
|
|
} else {
|
2020-06-06 23:27:07 +00:00
|
|
|
appendServiceCommandSetHelp(cmd.children, words, admin, l)
|
2020-03-25 19:58:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-18 11:23:08 +00:00
|
|
|
func handleServiceHelp(dc *downstreamConn, params []string) error {
|
|
|
|
if len(params) > 0 {
|
2020-03-25 19:58:07 +00:00
|
|
|
cmd, rest, err := serviceCommands.Get(params)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2020-03-18 11:23:08 +00:00
|
|
|
}
|
2020-03-25 19:58:07 +00:00
|
|
|
words := params[:len(params)-len(rest)]
|
|
|
|
|
|
|
|
if len(cmd.children) > 0 {
|
|
|
|
var l []string
|
2020-06-06 23:27:07 +00:00
|
|
|
appendServiceCommandSetHelp(cmd.children, words, dc.user.Admin, &l)
|
2020-03-25 19:58:07 +00:00
|
|
|
sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
|
|
|
|
} else {
|
|
|
|
text := strings.Join(words, " ")
|
|
|
|
if cmd.usage != "" {
|
|
|
|
text += " " + cmd.usage
|
|
|
|
}
|
|
|
|
text += ": " + cmd.desc
|
|
|
|
|
|
|
|
sendServicePRIVMSG(dc, text)
|
2020-03-18 11:23:08 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
var l []string
|
2020-06-06 23:27:07 +00:00
|
|
|
appendServiceCommandSetHelp(serviceCommands, nil, dc.user.Admin, &l)
|
2020-03-18 11:23:08 +00:00
|
|
|
sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2020-03-18 23:57:14 +00:00
|
|
|
|
2020-04-01 13:40:20 +00:00
|
|
|
func newFlagSet() *flag.FlagSet {
|
2020-03-18 23:57:14 +00:00
|
|
|
fs := flag.NewFlagSet("", flag.ContinueOnError)
|
|
|
|
fs.SetOutput(ioutil.Discard)
|
2020-04-01 13:40:20 +00:00
|
|
|
return fs
|
|
|
|
}
|
|
|
|
|
2020-06-02 09:39:53 +00:00
|
|
|
type stringSliceFlag []string
|
2020-04-15 23:40:50 +00:00
|
|
|
|
2020-06-02 09:39:53 +00:00
|
|
|
func (v *stringSliceFlag) String() string {
|
2020-04-15 23:40:50 +00:00
|
|
|
return fmt.Sprint([]string(*v))
|
|
|
|
}
|
|
|
|
|
2020-06-02 09:39:53 +00:00
|
|
|
func (v *stringSliceFlag) Set(s string) error {
|
2020-04-15 23:40:50 +00:00
|
|
|
*v = append(*v, s)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-06-02 09:39:53 +00:00
|
|
|
// stringPtrFlag is a flag value populating a string pointer. This allows to
|
|
|
|
// disambiguate between a flag that hasn't been set and a flag that has been
|
|
|
|
// set to an empty string.
|
|
|
|
type stringPtrFlag struct {
|
|
|
|
ptr **string
|
|
|
|
}
|
2020-03-18 23:57:14 +00:00
|
|
|
|
2020-06-02 09:39:53 +00:00
|
|
|
func (f stringPtrFlag) String() string {
|
2020-06-10 12:13:19 +00:00
|
|
|
if f.ptr == nil || *f.ptr == nil {
|
2020-06-02 09:39:53 +00:00
|
|
|
return ""
|
2020-03-18 23:57:14 +00:00
|
|
|
}
|
2020-06-02 09:39:53 +00:00
|
|
|
return **f.ptr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f stringPtrFlag) Set(s string) error {
|
|
|
|
*f.ptr = &s
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type networkFlagSet struct {
|
|
|
|
*flag.FlagSet
|
|
|
|
Addr, Name, Nick, Username, Pass, Realname *string
|
2020-06-04 15:23:27 +00:00
|
|
|
ConnectCommands []string
|
2020-06-02 09:39:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func newNetworkFlagSet() *networkFlagSet {
|
|
|
|
fs := &networkFlagSet{FlagSet: newFlagSet()}
|
|
|
|
fs.Var(stringPtrFlag{&fs.Addr}, "addr", "")
|
|
|
|
fs.Var(stringPtrFlag{&fs.Name}, "name", "")
|
|
|
|
fs.Var(stringPtrFlag{&fs.Nick}, "nick", "")
|
|
|
|
fs.Var(stringPtrFlag{&fs.Username}, "username", "")
|
|
|
|
fs.Var(stringPtrFlag{&fs.Pass}, "pass", "")
|
|
|
|
fs.Var(stringPtrFlag{&fs.Realname}, "realname", "")
|
|
|
|
fs.Var((*stringSliceFlag)(&fs.ConnectCommands), "connect-command", "")
|
|
|
|
return fs
|
|
|
|
}
|
2020-03-18 23:57:14 +00:00
|
|
|
|
2020-06-02 09:39:53 +00:00
|
|
|
func (fs *networkFlagSet) update(network *Network) error {
|
|
|
|
if fs.Addr != nil {
|
|
|
|
if addrParts := strings.SplitN(*fs.Addr, "://", 2); len(addrParts) == 2 {
|
|
|
|
scheme := addrParts[0]
|
|
|
|
switch scheme {
|
2020-07-09 10:19:51 +00:00
|
|
|
case "ircs", "irc+insecure", "unix":
|
2020-06-02 09:39:53 +00:00
|
|
|
default:
|
2020-07-09 10:19:51 +00:00
|
|
|
return fmt.Errorf("unknown scheme %q (supported schemes: ircs, irc+insecure, unix)", scheme)
|
2020-06-02 09:39:53 +00:00
|
|
|
}
|
2020-04-27 16:02:33 +00:00
|
|
|
}
|
2020-06-02 09:39:53 +00:00
|
|
|
network.Addr = *fs.Addr
|
2020-04-27 16:02:33 +00:00
|
|
|
}
|
2020-06-02 09:39:53 +00:00
|
|
|
if fs.Name != nil {
|
|
|
|
network.Name = *fs.Name
|
|
|
|
}
|
|
|
|
if fs.Nick != nil {
|
|
|
|
network.Nick = *fs.Nick
|
|
|
|
}
|
|
|
|
if fs.Username != nil {
|
|
|
|
network.Username = *fs.Username
|
|
|
|
}
|
|
|
|
if fs.Pass != nil {
|
|
|
|
network.Pass = *fs.Pass
|
|
|
|
}
|
|
|
|
if fs.Realname != nil {
|
|
|
|
network.Realname = *fs.Realname
|
|
|
|
}
|
|
|
|
if fs.ConnectCommands != nil {
|
|
|
|
if len(fs.ConnectCommands) == 1 && fs.ConnectCommands[0] == "" {
|
|
|
|
network.ConnectCommands = nil
|
|
|
|
} else {
|
|
|
|
for _, command := range fs.ConnectCommands {
|
|
|
|
_, err := irc.ParseMessage(command)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("flag -connect-command must be a valid raw irc command string: %q: %v", command, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
network.ConnectCommands = fs.ConnectCommands
|
2020-04-15 23:40:50 +00:00
|
|
|
}
|
|
|
|
}
|
2020-06-02 09:39:53 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-04-15 23:40:50 +00:00
|
|
|
|
2020-06-06 23:19:25 +00:00
|
|
|
func handleServiceNetworkCreate(dc *downstreamConn, params []string) error {
|
2020-06-02 09:39:53 +00:00
|
|
|
fs := newNetworkFlagSet()
|
|
|
|
if err := fs.Parse(params); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if fs.Addr == nil {
|
|
|
|
return fmt.Errorf("flag -addr is required")
|
2020-03-18 23:57:14 +00:00
|
|
|
}
|
|
|
|
|
2020-06-02 09:39:53 +00:00
|
|
|
record := &Network{
|
|
|
|
Addr: *fs.Addr,
|
|
|
|
Nick: dc.nick,
|
|
|
|
}
|
|
|
|
if err := fs.update(record); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
network, err := dc.user.createNetwork(record)
|
2020-03-18 23:57:14 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not create network: %v", err)
|
|
|
|
}
|
|
|
|
|
2020-04-01 13:40:20 +00:00
|
|
|
sendServicePRIVMSG(dc, fmt.Sprintf("created network %q", network.GetName()))
|
2020-03-18 23:57:14 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-03-25 21:57:48 +00:00
|
|
|
|
|
|
|
func handleServiceNetworkStatus(dc *downstreamConn, params []string) error {
|
|
|
|
dc.user.forEachNetwork(func(net *network) {
|
|
|
|
var statuses []string
|
|
|
|
var details string
|
2020-04-30 08:25:16 +00:00
|
|
|
if uc := net.conn; uc != nil {
|
2020-04-28 14:27:53 +00:00
|
|
|
if dc.nick != uc.nick {
|
|
|
|
statuses = append(statuses, "connected as "+uc.nick)
|
|
|
|
} else {
|
|
|
|
statuses = append(statuses, "connected")
|
|
|
|
}
|
Implement casemapping
TL;DR: supports for casemapping, now logs are saved in
casemapped/canonical/tolower form
(eg. in the #channel directory instead of #Channel... or something)
== What is casemapping? ==
see <https://modern.ircdocs.horse/#casemapping-parameter>
== Casemapping and multi-upstream ==
Since each upstream does not necessarily use the same casemapping, and
since casemappings cannot coexist [0],
1. soju must also update the database accordingly to upstreams'
casemapping, otherwise it will end up inconsistent,
2. soju must "normalize" entity names and expose only one casemapping
that is a subset of all supported casemappings (here, ascii).
[0] On some upstreams, "emersion[m]" and "emersion{m}" refer to the same
user (upstreams that advertise rfc1459 for example), while on others
(upstreams that advertise ascii) they don't.
Once upstream's casemapping is known (default to rfc1459), entity names
in map keys are made into casemapped form, for upstreamConn,
upstreamChannel and network.
downstreamConn advertises "CASEMAPPING=ascii", and always casemap map
keys with ascii.
Some functions require the caller to casemap their argument (to avoid
needless calls to casemapping functions).
== Message forwarding and casemapping ==
downstream message handling (joins and parts basically):
When relaying entity names from downstreams to upstreams, soju uses the
upstream casemapping, in order to not get in the way of the user. This
does not brings any issue, as long as soju replies with the ascii
casemapping in mind (solves point 1.).
marshalEntity/marshalUserPrefix:
When relaying entity names from upstreams with non-ascii casemappings,
soju *partially* casemap them: it only change the case of characters
which are not ascii letters. ASCII case is thus kept intact, while
special symbols like []{} are the same every time soju sends them to
downstreams (solves point 2.).
== Casemapping changes ==
Casemapping changes are not fully supported by this patch and will
result in loss of history. This is a limitation of the protocol and
should be solved by the RENAME spec.
2021-03-16 09:00:34 +00:00
|
|
|
details = fmt.Sprintf("%v channels", uc.channels.Len())
|
2020-03-25 21:57:48 +00:00
|
|
|
} else {
|
|
|
|
statuses = append(statuses, "disconnected")
|
2020-04-04 02:51:48 +00:00
|
|
|
if net.lastError != nil {
|
|
|
|
details = net.lastError.Error()
|
|
|
|
}
|
2020-03-25 21:57:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if net == dc.network {
|
|
|
|
statuses = append(statuses, "current")
|
|
|
|
}
|
|
|
|
|
2020-04-05 13:20:13 +00:00
|
|
|
name := net.GetName()
|
|
|
|
if name != net.Addr {
|
|
|
|
name = fmt.Sprintf("%v (%v)", name, net.Addr)
|
|
|
|
}
|
|
|
|
|
|
|
|
s := fmt.Sprintf("%v [%v]", name, strings.Join(statuses, ", "))
|
2020-03-25 21:57:48 +00:00
|
|
|
if details != "" {
|
|
|
|
s += ": " + details
|
|
|
|
}
|
|
|
|
sendServicePRIVMSG(dc, s)
|
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|
2020-04-01 13:40:20 +00:00
|
|
|
|
2020-06-02 09:39:53 +00:00
|
|
|
func handleServiceNetworkUpdate(dc *downstreamConn, params []string) error {
|
|
|
|
if len(params) < 1 {
|
2020-11-30 21:16:44 +00:00
|
|
|
return fmt.Errorf("expected at least one argument")
|
2020-06-02 09:39:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fs := newNetworkFlagSet()
|
|
|
|
if err := fs.Parse(params[1:]); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
net := dc.user.getNetwork(params[0])
|
|
|
|
if net == nil {
|
|
|
|
return fmt.Errorf("unknown network %q", params[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
record := net.Network // copy network record because we'll mutate it
|
|
|
|
if err := fs.update(&record); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
network, err := dc.user.updateNetwork(&record)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not update network: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
sendServicePRIVMSG(dc, fmt.Sprintf("updated network %q", network.GetName()))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-04-01 13:40:20 +00:00
|
|
|
func handleServiceNetworkDelete(dc *downstreamConn, params []string) error {
|
|
|
|
if len(params) != 1 {
|
|
|
|
return fmt.Errorf("expected exactly one argument")
|
|
|
|
}
|
|
|
|
|
|
|
|
net := dc.user.getNetwork(params[0])
|
|
|
|
if net == nil {
|
|
|
|
return fmt.Errorf("unknown network %q", params[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := dc.user.deleteNetwork(net.ID); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
sendServicePRIVMSG(dc, fmt.Sprintf("deleted network %q", net.GetName()))
|
|
|
|
return nil
|
|
|
|
}
|
2020-04-08 12:20:00 +00:00
|
|
|
|
2020-06-06 23:19:25 +00:00
|
|
|
func handleServiceCertfpGenerate(dc *downstreamConn, params []string) error {
|
|
|
|
fs := newFlagSet()
|
|
|
|
keyType := fs.String("key-type", "rsa", "key type to generate (rsa, ecdsa, ed25519)")
|
|
|
|
bits := fs.Int("bits", 3072, "size of key to generate, meaningful only for RSA")
|
|
|
|
|
|
|
|
if err := fs.Parse(params); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(fs.Args()) != 1 {
|
|
|
|
return errors.New("exactly one argument is required")
|
|
|
|
}
|
|
|
|
|
|
|
|
net := dc.user.getNetwork(fs.Arg(0))
|
|
|
|
if net == nil {
|
|
|
|
return fmt.Errorf("unknown network %q", fs.Arg(0))
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
privKey crypto.PrivateKey
|
|
|
|
pubKey crypto.PublicKey
|
|
|
|
)
|
|
|
|
switch *keyType {
|
|
|
|
case "rsa":
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, *bits)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
privKey = key
|
|
|
|
pubKey = key.Public()
|
|
|
|
case "ecdsa":
|
|
|
|
key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
privKey = key
|
|
|
|
pubKey = key.Public()
|
|
|
|
case "ed25519":
|
|
|
|
var err error
|
|
|
|
pubKey, privKey, err = ed25519.GenerateKey(rand.Reader)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Using PKCS#8 allows easier extension for new key types.
|
|
|
|
privKeyBytes, err := x509.MarshalPKCS8PrivateKey(privKey)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
notBefore := time.Now()
|
|
|
|
// Lets make a fair assumption nobody will use the same cert for more than 20 years...
|
|
|
|
notAfter := notBefore.Add(24 * time.Hour * 365 * 20)
|
|
|
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
|
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
cert := &x509.Certificate{
|
|
|
|
SerialNumber: serialNumber,
|
|
|
|
Subject: pkix.Name{CommonName: "soju auto-generated certificate"},
|
|
|
|
NotBefore: notBefore,
|
|
|
|
NotAfter: notAfter,
|
|
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
|
|
|
}
|
|
|
|
derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, pubKey, privKey)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
net.SASL.External.CertBlob = derBytes
|
|
|
|
net.SASL.External.PrivKeyBlob = privKeyBytes
|
|
|
|
net.SASL.Mechanism = "EXTERNAL"
|
|
|
|
|
2020-10-24 13:14:23 +00:00
|
|
|
if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
|
2020-06-06 23:19:25 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
sendServicePRIVMSG(dc, "certificate generated")
|
|
|
|
|
|
|
|
sha1Sum := sha1.Sum(derBytes)
|
|
|
|
sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
|
|
|
|
sha256Sum := sha256.Sum256(derBytes)
|
|
|
|
sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleServiceCertfpFingerprints(dc *downstreamConn, params []string) error {
|
|
|
|
if len(params) != 1 {
|
|
|
|
return fmt.Errorf("expected exactly one argument")
|
|
|
|
}
|
|
|
|
|
|
|
|
net := dc.user.getNetwork(params[0])
|
|
|
|
if net == nil {
|
|
|
|
return fmt.Errorf("unknown network %q", params[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
sha1Sum := sha1.Sum(net.SASL.External.CertBlob)
|
|
|
|
sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
|
|
|
|
sha256Sum := sha256.Sum256(net.SASL.External.CertBlob)
|
|
|
|
sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-22 10:20:52 +00:00
|
|
|
func handleServiceSASLSetPlain(dc *downstreamConn, params []string) error {
|
|
|
|
if len(params) != 3 {
|
|
|
|
return fmt.Errorf("expected exactly 3 arguments")
|
2020-06-06 23:19:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
net := dc.user.getNetwork(params[0])
|
|
|
|
if net == nil {
|
|
|
|
return fmt.Errorf("unknown network %q", params[0])
|
|
|
|
}
|
|
|
|
|
2020-07-22 10:20:52 +00:00
|
|
|
net.SASL.Plain.Username = params[1]
|
|
|
|
net.SASL.Plain.Password = params[2]
|
|
|
|
net.SASL.Mechanism = "PLAIN"
|
2020-06-06 23:19:25 +00:00
|
|
|
|
2020-10-24 13:14:23 +00:00
|
|
|
if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
|
2020-06-06 23:19:25 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-07-22 10:20:52 +00:00
|
|
|
sendServicePRIVMSG(dc, "credentials saved")
|
2020-06-06 23:19:25 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-22 10:20:52 +00:00
|
|
|
func handleServiceSASLReset(dc *downstreamConn, params []string) error {
|
|
|
|
if len(params) != 1 {
|
|
|
|
return fmt.Errorf("expected exactly one argument")
|
2020-07-22 10:16:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
net := dc.user.getNetwork(params[0])
|
|
|
|
if net == nil {
|
|
|
|
return fmt.Errorf("unknown network %q", params[0])
|
|
|
|
}
|
|
|
|
|
2020-07-22 10:20:52 +00:00
|
|
|
net.SASL.Plain.Username = ""
|
|
|
|
net.SASL.Plain.Password = ""
|
|
|
|
net.SASL.External.CertBlob = nil
|
|
|
|
net.SASL.External.PrivKeyBlob = nil
|
|
|
|
net.SASL.Mechanism = ""
|
2020-07-22 10:16:13 +00:00
|
|
|
|
2020-10-24 13:14:23 +00:00
|
|
|
if err := dc.srv.db.StoreNetwork(dc.user.ID, &net.Network); err != nil {
|
2020-07-22 10:16:13 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-07-22 10:20:52 +00:00
|
|
|
sendServicePRIVMSG(dc, "credentials reset")
|
2020-07-22 10:16:13 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-04-08 12:20:00 +00:00
|
|
|
func handlePasswordChange(dc *downstreamConn, params []string) error {
|
|
|
|
if len(params) != 1 {
|
|
|
|
return fmt.Errorf("expected exactly one argument")
|
|
|
|
}
|
|
|
|
|
|
|
|
hashed, err := bcrypt.GenerateFromPassword([]byte(params[0]), bcrypt.DefaultCost)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to hash password: %v", err)
|
|
|
|
}
|
|
|
|
if err := dc.user.updatePassword(string(hashed)); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
sendServicePRIVMSG(dc, "password updated")
|
|
|
|
return nil
|
|
|
|
}
|
2020-06-06 23:30:27 +00:00
|
|
|
|
|
|
|
func handleUserCreate(dc *downstreamConn, params []string) error {
|
|
|
|
fs := newFlagSet()
|
|
|
|
username := fs.String("username", "", "")
|
|
|
|
password := fs.String("password", "", "")
|
|
|
|
admin := fs.Bool("admin", false, "")
|
|
|
|
|
|
|
|
if err := fs.Parse(params); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if *username == "" {
|
|
|
|
return fmt.Errorf("flag -username is required")
|
|
|
|
}
|
|
|
|
if *password == "" {
|
|
|
|
return fmt.Errorf("flag -password is required")
|
|
|
|
}
|
|
|
|
|
|
|
|
hashed, err := bcrypt.GenerateFromPassword([]byte(*password), bcrypt.DefaultCost)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to hash password: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
user := &User{
|
|
|
|
Username: *username,
|
|
|
|
Password: string(hashed),
|
|
|
|
Admin: *admin,
|
|
|
|
}
|
|
|
|
if _, err := dc.srv.createUser(user); err != nil {
|
|
|
|
return fmt.Errorf("could not create user: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
sendServicePRIVMSG(dc, fmt.Sprintf("created user %q", *username))
|
|
|
|
return nil
|
|
|
|
}
|
2020-08-10 13:03:07 +00:00
|
|
|
|
|
|
|
func handleUserDelete(dc *downstreamConn, params []string) error {
|
|
|
|
if len(params) != 1 {
|
|
|
|
return fmt.Errorf("expected exactly one argument")
|
|
|
|
}
|
|
|
|
username := params[0]
|
|
|
|
|
|
|
|
u := dc.srv.getUser(username)
|
|
|
|
if u == nil {
|
|
|
|
return fmt.Errorf("unknown username %q", username)
|
|
|
|
}
|
|
|
|
|
|
|
|
u.stop()
|
|
|
|
|
2021-04-22 07:21:41 +00:00
|
|
|
if err := dc.srv.db.DeleteUser(u.ID); err != nil {
|
2020-08-10 13:03:07 +00:00
|
|
|
return fmt.Errorf("failed to delete user: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
sendServicePRIVMSG(dc, fmt.Sprintf("deleted user %q", username))
|
|
|
|
return nil
|
|
|
|
}
|
2020-11-30 21:16:44 +00:00
|
|
|
|
2021-05-25 17:22:22 +00:00
|
|
|
func handleServiceChannelStatus(dc *downstreamConn, params []string) error {
|
|
|
|
var defaultNetworkName string
|
|
|
|
if dc.network != nil {
|
|
|
|
defaultNetworkName = dc.network.GetName()
|
|
|
|
}
|
|
|
|
|
|
|
|
fs := newFlagSet()
|
|
|
|
networkName := fs.String("network", defaultNetworkName, "")
|
|
|
|
|
|
|
|
if err := fs.Parse(params); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
sendNetwork := func(net *network) {
|
|
|
|
for _, entry := range net.channels.innerMap {
|
|
|
|
ch := entry.value.(*Channel)
|
|
|
|
|
|
|
|
var uch *upstreamChannel
|
|
|
|
if net.conn != nil {
|
|
|
|
uch = net.conn.channels.Value(ch.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
name := ch.Name
|
|
|
|
if *networkName == "" {
|
|
|
|
name += "/" + net.GetName()
|
|
|
|
}
|
|
|
|
|
|
|
|
var status string
|
|
|
|
if uch != nil {
|
|
|
|
status = "joined"
|
|
|
|
} else if net.conn != nil {
|
|
|
|
status = "parted"
|
|
|
|
} else {
|
|
|
|
status = "disconnected"
|
|
|
|
}
|
|
|
|
|
|
|
|
if ch.Detached {
|
|
|
|
status += ", detached"
|
|
|
|
}
|
|
|
|
|
|
|
|
s := fmt.Sprintf("%v [%v]", name, status)
|
|
|
|
sendServicePRIVMSG(dc, s)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if *networkName == "" {
|
|
|
|
dc.user.forEachNetwork(sendNetwork)
|
|
|
|
} else {
|
|
|
|
net := dc.user.getNetwork(*networkName)
|
|
|
|
if net == nil {
|
|
|
|
return fmt.Errorf("unknown network %q", *networkName)
|
|
|
|
}
|
|
|
|
sendNetwork(net)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-11-30 21:16:44 +00:00
|
|
|
type channelFlagSet struct {
|
|
|
|
*flag.FlagSet
|
|
|
|
RelayDetached, ReattachOn, DetachAfter, DetachOn *string
|
|
|
|
}
|
|
|
|
|
|
|
|
func newChannelFlagSet() *channelFlagSet {
|
|
|
|
fs := &channelFlagSet{FlagSet: newFlagSet()}
|
|
|
|
fs.Var(stringPtrFlag{&fs.RelayDetached}, "relay-detached", "")
|
|
|
|
fs.Var(stringPtrFlag{&fs.ReattachOn}, "reattach-on", "")
|
|
|
|
fs.Var(stringPtrFlag{&fs.DetachAfter}, "detach-after", "")
|
|
|
|
fs.Var(stringPtrFlag{&fs.DetachOn}, "detach-on", "")
|
|
|
|
return fs
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fs *channelFlagSet) update(channel *Channel) error {
|
|
|
|
if fs.RelayDetached != nil {
|
|
|
|
filter, err := parseFilter(*fs.RelayDetached)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
channel.RelayDetached = filter
|
|
|
|
}
|
|
|
|
if fs.ReattachOn != nil {
|
|
|
|
filter, err := parseFilter(*fs.ReattachOn)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
channel.ReattachOn = filter
|
|
|
|
}
|
|
|
|
if fs.DetachAfter != nil {
|
|
|
|
dur, err := time.ParseDuration(*fs.DetachAfter)
|
|
|
|
if err != nil || dur < 0 {
|
|
|
|
return fmt.Errorf("unknown duration for -detach-after %q (duration format: 0, 300s, 22h30m, ...)", *fs.DetachAfter)
|
|
|
|
}
|
|
|
|
channel.DetachAfter = dur
|
|
|
|
}
|
|
|
|
if fs.DetachOn != nil {
|
|
|
|
filter, err := parseFilter(*fs.DetachOn)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
channel.DetachOn = filter
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleServiceChannelUpdate(dc *downstreamConn, params []string) error {
|
|
|
|
if len(params) < 1 {
|
|
|
|
return fmt.Errorf("expected at least one argument")
|
|
|
|
}
|
|
|
|
name := params[0]
|
|
|
|
|
|
|
|
fs := newChannelFlagSet()
|
|
|
|
if err := fs.Parse(params[1:]); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
uc, upstreamName, err := dc.unmarshalEntity(name)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unknown channel %q", name)
|
|
|
|
}
|
|
|
|
|
Implement casemapping
TL;DR: supports for casemapping, now logs are saved in
casemapped/canonical/tolower form
(eg. in the #channel directory instead of #Channel... or something)
== What is casemapping? ==
see <https://modern.ircdocs.horse/#casemapping-parameter>
== Casemapping and multi-upstream ==
Since each upstream does not necessarily use the same casemapping, and
since casemappings cannot coexist [0],
1. soju must also update the database accordingly to upstreams'
casemapping, otherwise it will end up inconsistent,
2. soju must "normalize" entity names and expose only one casemapping
that is a subset of all supported casemappings (here, ascii).
[0] On some upstreams, "emersion[m]" and "emersion{m}" refer to the same
user (upstreams that advertise rfc1459 for example), while on others
(upstreams that advertise ascii) they don't.
Once upstream's casemapping is known (default to rfc1459), entity names
in map keys are made into casemapped form, for upstreamConn,
upstreamChannel and network.
downstreamConn advertises "CASEMAPPING=ascii", and always casemap map
keys with ascii.
Some functions require the caller to casemap their argument (to avoid
needless calls to casemapping functions).
== Message forwarding and casemapping ==
downstream message handling (joins and parts basically):
When relaying entity names from downstreams to upstreams, soju uses the
upstream casemapping, in order to not get in the way of the user. This
does not brings any issue, as long as soju replies with the ascii
casemapping in mind (solves point 1.).
marshalEntity/marshalUserPrefix:
When relaying entity names from upstreams with non-ascii casemappings,
soju *partially* casemap them: it only change the case of characters
which are not ascii letters. ASCII case is thus kept intact, while
special symbols like []{} are the same every time soju sends them to
downstreams (solves point 2.).
== Casemapping changes ==
Casemapping changes are not fully supported by this patch and will
result in loss of history. This is a limitation of the protocol and
should be solved by the RENAME spec.
2021-03-16 09:00:34 +00:00
|
|
|
ch := uc.network.channels.Value(upstreamName)
|
|
|
|
if ch == nil {
|
2020-11-30 21:16:44 +00:00
|
|
|
return fmt.Errorf("unknown channel %q", name)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := fs.update(ch); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
uc.updateChannelAutoDetach(upstreamName)
|
|
|
|
|
|
|
|
if err := dc.srv.db.StoreChannel(uc.network.ID, ch); err != nil {
|
|
|
|
return fmt.Errorf("failed to update channel: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
sendServicePRIVMSG(dc, fmt.Sprintf("updated channel %q", name))
|
|
|
|
return nil
|
|
|
|
}
|