2020-03-18 11:23:08 +00:00
|
|
|
package soju
|
|
|
|
|
|
|
|
import (
|
2021-10-18 17:15:15 +00:00
|
|
|
"context"
|
2020-05-29 11:10:54 +00:00
|
|
|
"crypto/sha1"
|
|
|
|
"crypto/sha256"
|
2021-07-06 14:31:34 +00:00
|
|
|
"crypto/sha512"
|
2020-05-29 11:10:54 +00:00
|
|
|
"encoding/hex"
|
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-06-24 10:08:35 +00:00
|
|
|
"sort"
|
2021-05-26 08:49:52 +00:00
|
|
|
"strconv"
|
2020-03-18 11:23:08 +00:00
|
|
|
"strings"
|
2020-05-29 11:10:54 +00:00
|
|
|
"time"
|
2021-06-20 20:30:25 +00:00
|
|
|
"unicode"
|
2020-03-18 11:23:08 +00:00
|
|
|
|
2022-11-14 11:06:58 +00:00
|
|
|
"gopkg.in/irc.v4"
|
2022-05-09 10:34:43 +00:00
|
|
|
|
|
|
|
"git.sr.ht/~emersion/soju/database"
|
2020-03-18 11:23:08 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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
|
|
|
|
2021-10-05 17:21:43 +00:00
|
|
|
// maxRSABits is the maximum number of RSA key bits used when generating a new
|
|
|
|
// private key.
|
|
|
|
const maxRSABits = 8192
|
|
|
|
|
2020-04-04 02:57:20 +00:00
|
|
|
var servicePrefix = &irc.Prefix{
|
|
|
|
Name: serviceNick,
|
|
|
|
User: serviceNick,
|
|
|
|
Host: serviceNick,
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
type serviceContext struct {
|
|
|
|
context.Context
|
|
|
|
nick string // optional
|
|
|
|
network *network // optional
|
2023-01-19 17:33:22 +00:00
|
|
|
user *user // optional
|
|
|
|
srv *Server
|
2023-01-19 17:13:59 +00:00
|
|
|
admin bool
|
2023-01-17 12:26:05 +00:00
|
|
|
print func(string)
|
|
|
|
}
|
|
|
|
|
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
|
2023-01-17 12:26:05 +00:00
|
|
|
handle func(ctx *serviceContext, params []string) error
|
2020-03-25 19:58:07 +00:00
|
|
|
children serviceCommandSet
|
2020-06-06 23:27:07 +00:00
|
|
|
admin bool
|
2023-01-19 17:40:40 +00:00
|
|
|
global bool
|
2020-03-18 11:23:08 +00:00
|
|
|
}
|
|
|
|
|
2020-04-04 02:48:25 +00:00
|
|
|
func sendServiceNOTICE(dc *downstreamConn, text string) {
|
2023-04-06 11:23:20 +00:00
|
|
|
dc.SendMessage(context.TODO(), &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) {
|
2023-04-06 11:23:20 +00:00
|
|
|
dc.SendMessage(context.TODO(), &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},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-06-20 20:30:25 +00:00
|
|
|
func splitWords(s string) ([]string, error) {
|
|
|
|
var words []string
|
|
|
|
var lastWord strings.Builder
|
|
|
|
escape := false
|
|
|
|
prev := ' '
|
|
|
|
wordDelim := ' '
|
|
|
|
|
|
|
|
for _, r := range s {
|
|
|
|
if escape {
|
|
|
|
// last char was a backslash, write the byte as-is.
|
|
|
|
lastWord.WriteRune(r)
|
|
|
|
escape = false
|
|
|
|
} else if r == '\\' {
|
|
|
|
escape = true
|
|
|
|
} else if wordDelim == ' ' && unicode.IsSpace(r) {
|
|
|
|
// end of last word
|
|
|
|
if !unicode.IsSpace(prev) {
|
|
|
|
words = append(words, lastWord.String())
|
|
|
|
lastWord.Reset()
|
|
|
|
}
|
|
|
|
} else if r == wordDelim {
|
|
|
|
// wordDelim is either " or ', switch back to
|
|
|
|
// space-delimited words.
|
|
|
|
wordDelim = ' '
|
|
|
|
} else if r == '"' || r == '\'' {
|
|
|
|
if wordDelim == ' ' {
|
|
|
|
// start of (double-)quoted word
|
|
|
|
wordDelim = r
|
|
|
|
} else {
|
|
|
|
// either wordDelim is " and r is ' or vice-versa
|
|
|
|
lastWord.WriteRune(r)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
lastWord.WriteRune(r)
|
|
|
|
}
|
|
|
|
|
|
|
|
prev = r
|
|
|
|
}
|
|
|
|
|
|
|
|
if !unicode.IsSpace(prev) {
|
|
|
|
words = append(words, lastWord.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
if wordDelim != ' ' {
|
|
|
|
return nil, fmt.Errorf("unterminated quoted string")
|
|
|
|
}
|
|
|
|
if escape {
|
|
|
|
return nil, fmt.Errorf("unterminated backslash sequence")
|
|
|
|
}
|
|
|
|
|
|
|
|
return words, nil
|
|
|
|
}
|
|
|
|
|
2023-01-20 14:46:42 +00:00
|
|
|
func handleServicePRIVMSG(ctx *serviceContext, text string) error {
|
2021-06-20 20:30:25 +00:00
|
|
|
words, err := splitWords(text)
|
2020-03-18 11:23:08 +00:00
|
|
|
if err != nil {
|
2023-01-20 14:46:42 +00:00
|
|
|
return fmt.Errorf(`failed to parse command: %v`, err)
|
2020-03-18 11:23:08 +00:00
|
|
|
}
|
2023-01-20 14:46:42 +00:00
|
|
|
return handleServiceCommand(ctx, words)
|
2023-01-17 13:03:13 +00:00
|
|
|
}
|
2020-03-18 11:23:08 +00:00
|
|
|
|
2023-01-20 14:46:42 +00:00
|
|
|
func handleServiceCommand(ctx *serviceContext, words []string) error {
|
2020-03-25 19:58:07 +00:00
|
|
|
cmd, params, err := serviceCommands.Get(words)
|
|
|
|
if err != nil {
|
2023-01-20 14:46:42 +00:00
|
|
|
return fmt.Errorf(`%v (type "help" for a list of commands)`, err)
|
2020-03-18 11:23:08 +00:00
|
|
|
}
|
2023-01-19 17:13:59 +00:00
|
|
|
if cmd.admin && !ctx.admin {
|
2023-01-20 14:46:42 +00:00
|
|
|
return fmt.Errorf("you must be an admin to use this command")
|
2020-06-06 23:27:07 +00:00
|
|
|
}
|
2023-01-19 17:40:40 +00:00
|
|
|
if !cmd.global && ctx.user == nil {
|
2023-01-20 14:46:42 +00:00
|
|
|
return fmt.Errorf("this command must be run as a user (try running with user run)")
|
2023-01-19 17:33:22 +00:00
|
|
|
}
|
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
|
2023-01-19 17:33:22 +00:00
|
|
|
appendServiceCommandSetHelp(cmd.children, words, ctx.admin, ctx.user == nil, &l)
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("available commands: " + strings.Join(l, ", "))
|
2023-01-20 14:46:42 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// 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.
|
|
|
|
var logger Logger
|
|
|
|
if ctx.user != nil {
|
|
|
|
logger = ctx.user.logger
|
2020-06-09 11:39:28 +00:00
|
|
|
} else {
|
2023-01-20 14:46:42 +00:00
|
|
|
logger = ctx.srv.Logger
|
2020-06-09 11:39:28 +00:00
|
|
|
}
|
2023-01-20 14:46:42 +00:00
|
|
|
logger.Printf("command without handler and subcommands invoked:", words[0])
|
|
|
|
return fmt.Errorf("command %q not found", words[0])
|
2020-06-09 11:39:28 +00:00
|
|
|
}
|
|
|
|
|
2023-01-20 14:46:42 +00:00
|
|
|
return cmd.handle(ctx, params)
|
2020-03-18 11:23:08 +00:00
|
|
|
}
|
|
|
|
|
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,
|
2023-01-19 17:40:40 +00:00
|
|
|
global: true,
|
2020-03-18 11:23:08 +00:00
|
|
|
},
|
2020-03-25 19:58:07 +00:00
|
|
|
"network": {
|
|
|
|
children: serviceCommandSet{
|
|
|
|
"create": {
|
2022-12-10 08:12:46 +00:00
|
|
|
usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-certfp fingerprint] [-nick nick] [-auto-away auto-away] [-enabled enabled] [-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": {
|
2022-12-10 08:12:46 +00:00
|
|
|
usage: "[name] [-addr addr] [-name name] [-username username] [-pass pass] [-realname realname] [-certfp fingerprint] [-nick nick] [-auto-away auto-away] [-enabled enabled] [-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": {
|
2022-02-07 20:33:16 +00:00
|
|
|
usage: "[name]",
|
2020-04-01 13:40:20 +00:00
|
|
|
desc: "delete a network",
|
|
|
|
handle: handleServiceNetworkDelete,
|
|
|
|
},
|
2021-07-06 22:44:15 +00:00
|
|
|
"quote": {
|
2022-02-07 20:33:16 +00:00
|
|
|
usage: "[name] <command>",
|
2021-07-06 22:44:15 +00:00
|
|
|
desc: "send a raw line to a network",
|
|
|
|
handle: handleServiceNetworkQuote,
|
|
|
|
},
|
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": {
|
2022-02-04 15:47:34 +00:00
|
|
|
usage: "[-key-type rsa|ecdsa|ed25519] [-bits N] [-network name]",
|
2020-05-29 11:10:54 +00:00
|
|
|
desc: "generate a new self-signed certificate, defaults to using RSA-3072 key",
|
2021-10-08 07:47:25 +00:00
|
|
|
handle: handleServiceCertFPGenerate,
|
2020-05-29 11:10:54 +00:00
|
|
|
},
|
|
|
|
"fingerprint": {
|
2022-02-04 15:47:34 +00:00
|
|
|
usage: "[-network name]",
|
|
|
|
desc: "show fingerprints of certificate",
|
2021-10-08 07:47:25 +00:00
|
|
|
handle: handleServiceCertFPFingerprints,
|
2020-05-29 11:10:54 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2020-07-22 10:16:13 +00:00
|
|
|
"sasl": {
|
|
|
|
children: serviceCommandSet{
|
2021-12-01 10:03:27 +00:00
|
|
|
"status": {
|
2022-02-04 15:47:34 +00:00
|
|
|
usage: "[-network name]",
|
2021-12-01 10:03:27 +00:00
|
|
|
desc: "show SASL status",
|
2022-02-04 14:41:40 +00:00
|
|
|
handle: handleServiceSASLStatus,
|
2021-12-01 10:03:27 +00:00
|
|
|
},
|
2020-07-22 10:16:13 +00:00
|
|
|
"set-plain": {
|
2022-02-04 15:47:34 +00:00
|
|
|
usage: "[-network name] <username> <password>",
|
2020-07-22 10:16:13 +00:00
|
|
|
desc: "set SASL PLAIN credentials",
|
|
|
|
handle: handleServiceSASLSetPlain,
|
|
|
|
},
|
2020-07-22 10:20:52 +00:00
|
|
|
"reset": {
|
2022-02-04 15:47:34 +00:00
|
|
|
usage: "[-network name]",
|
2020-07-22 10:20:52 +00:00
|
|
|
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{
|
2023-01-17 13:25:37 +00:00
|
|
|
"status": {
|
|
|
|
desc: "show a list of users and their current status",
|
|
|
|
handle: handleUserStatus,
|
|
|
|
admin: true,
|
2023-01-19 17:40:40 +00:00
|
|
|
global: true,
|
2023-01-17 13:25:37 +00:00
|
|
|
},
|
2020-06-06 23:30:27 +00:00
|
|
|
"create": {
|
2023-01-30 19:12:04 +00:00
|
|
|
usage: "-username <username> -password <password> [-disable-password] [-admin true|false] [-nick <nick>] [-realname <realname>] [-enabled true|false]",
|
2020-06-06 23:30:27 +00:00
|
|
|
desc: "create a new soju user",
|
|
|
|
handle: handleUserCreate,
|
|
|
|
admin: true,
|
2023-01-19 17:40:40 +00:00
|
|
|
global: true,
|
2020-06-06 23:30:27 +00:00
|
|
|
},
|
2021-06-25 18:33:13 +00:00
|
|
|
"update": {
|
2023-01-30 19:12:04 +00:00
|
|
|
usage: "[username] [-password <password>] [-disable-password] [-admin true|false] [-nick <nick>] [-realname <realname>] [-enabled true|false]",
|
|
|
|
desc: "update a user",
|
2021-06-25 18:33:13 +00:00
|
|
|
handle: handleUserUpdate,
|
2023-01-19 17:40:40 +00:00
|
|
|
global: true,
|
2021-06-25 18:33:13 +00:00
|
|
|
},
|
2020-08-10 13:03:07 +00:00
|
|
|
"delete": {
|
2022-06-24 21:22:01 +00:00
|
|
|
usage: "<username> [confirmation token]",
|
2020-08-10 13:03:07 +00:00
|
|
|
desc: "delete a user",
|
|
|
|
handle: handleUserDelete,
|
2023-01-19 17:40:40 +00:00
|
|
|
global: true,
|
2020-08-10 13:03:07 +00:00
|
|
|
},
|
2023-01-17 13:03:13 +00:00
|
|
|
"run": {
|
|
|
|
usage: "<username> <command>",
|
|
|
|
desc: "run a command as another user",
|
|
|
|
handle: handleUserRun,
|
|
|
|
admin: true,
|
2023-01-19 17:40:40 +00:00
|
|
|
global: true,
|
2023-01-17 13:03:13 +00:00
|
|
|
},
|
2020-06-06 23:30:27 +00:00
|
|
|
},
|
2023-01-19 17:40:40 +00:00
|
|
|
global: true,
|
2020-06-06 23:30:27 +00:00
|
|
|
},
|
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": {
|
2022-06-24 18:41:13 +00:00
|
|
|
usage: "<name> [-detached <true|false>] [-relay-detached <default|none|highlight|message>] [-reattach-on <default|none|highlight|message>] [-detach-after <duration>] [-detach-on <default|none|highlight|message>]",
|
2020-11-30 21:16:44 +00:00
|
|
|
desc: "update a channel",
|
|
|
|
handle: handleServiceChannelUpdate,
|
|
|
|
},
|
2022-12-08 15:00:00 +00:00
|
|
|
"delete": {
|
|
|
|
usage: "<name>",
|
|
|
|
desc: "delete a channel",
|
|
|
|
handle: handleServiceChannelDelete,
|
|
|
|
},
|
2020-11-30 21:16:44 +00:00
|
|
|
},
|
|
|
|
},
|
2021-10-05 17:12:25 +00:00
|
|
|
"server": {
|
|
|
|
children: serviceCommandSet{
|
|
|
|
"status": {
|
|
|
|
desc: "show server statistics",
|
|
|
|
handle: handleServiceServerStatus,
|
|
|
|
admin: true,
|
2023-01-19 17:40:40 +00:00
|
|
|
global: true,
|
2021-10-05 17:12:25 +00:00
|
|
|
},
|
2021-10-08 08:52:03 +00:00
|
|
|
"notice": {
|
2023-01-15 12:36:30 +00:00
|
|
|
usage: "<notice>",
|
2021-10-08 08:52:03 +00:00
|
|
|
desc: "broadcast a notice to all connected bouncer users",
|
|
|
|
handle: handleServiceServerNotice,
|
|
|
|
admin: true,
|
2023-01-19 17:40:40 +00:00
|
|
|
global: true,
|
2021-10-08 08:52:03 +00:00
|
|
|
},
|
2021-10-05 17:12:25 +00:00
|
|
|
},
|
|
|
|
admin: true,
|
|
|
|
},
|
2020-03-18 11:23:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, admin bool, global 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
|
|
|
|
}
|
2023-01-19 17:40:40 +00:00
|
|
|
if !cmd.global && global {
|
2023-01-19 17:33:22 +00:00
|
|
|
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 {
|
2023-01-19 17:33:22 +00:00
|
|
|
appendServiceCommandSetHelp(cmd.children, words, admin, global, l)
|
2020-03-25 19:58:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceHelp(ctx *serviceContext, params []string) error {
|
2020-03-18 11:23:08 +00:00
|
|
|
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
|
2023-01-19 17:33:22 +00:00
|
|
|
appendServiceCommandSetHelp(cmd.children, words, ctx.admin, ctx.user == nil, &l)
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("available commands: " + strings.Join(l, ", "))
|
2020-03-25 19:58:07 +00:00
|
|
|
} else {
|
|
|
|
text := strings.Join(words, " ")
|
|
|
|
if cmd.usage != "" {
|
|
|
|
text += " " + cmd.usage
|
|
|
|
}
|
|
|
|
text += ": " + cmd.desc
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(text)
|
2020-03-18 11:23:08 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
var l []string
|
2023-01-19 17:33:22 +00:00
|
|
|
appendServiceCommandSetHelp(serviceCommands, nil, ctx.admin, ctx.user == nil, &l)
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("available commands: " + strings.Join(l, ", "))
|
2020-03-18 11:23:08 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-05-26 08:49:52 +00:00
|
|
|
type boolPtrFlag struct {
|
|
|
|
ptr **bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f boolPtrFlag) String() string {
|
|
|
|
if f.ptr == nil || *f.ptr == nil {
|
|
|
|
return "<nil>"
|
|
|
|
}
|
|
|
|
return strconv.FormatBool(**f.ptr)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f boolPtrFlag) Set(s string) error {
|
|
|
|
v, err := strconv.ParseBool(s)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
*f.ptr = &v
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func getNetworkFromArg(ctx *serviceContext, params []string) (*network, []string, error) {
|
2022-02-07 20:33:16 +00:00
|
|
|
name, params := popArg(params)
|
|
|
|
if name == "" {
|
2023-01-17 12:26:05 +00:00
|
|
|
if ctx.network == nil {
|
2022-02-07 20:33:16 +00:00
|
|
|
return nil, params, fmt.Errorf("no network selected, a name argument is required")
|
|
|
|
}
|
2023-01-17 12:26:05 +00:00
|
|
|
return ctx.network, params, nil
|
2022-02-07 20:33:16 +00:00
|
|
|
} else {
|
2023-01-17 12:26:05 +00:00
|
|
|
net := ctx.user.getNetwork(name)
|
2022-02-07 20:33:16 +00:00
|
|
|
if net == nil {
|
|
|
|
return nil, params, fmt.Errorf("unknown network %q", name)
|
|
|
|
}
|
|
|
|
return net, params, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-02 09:39:53 +00:00
|
|
|
type networkFlagSet struct {
|
|
|
|
*flag.FlagSet
|
2022-12-10 08:12:46 +00:00
|
|
|
Addr, Name, Nick, Username, Pass, Realname, CertFP *string
|
|
|
|
AutoAway, Enabled *bool
|
|
|
|
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", "")
|
2022-12-18 16:55:31 +00:00
|
|
|
fs.Var(stringPtrFlag{&fs.CertFP}, "certfp", "")
|
2022-09-26 17:49:26 +00:00
|
|
|
fs.Var(boolPtrFlag{&fs.AutoAway}, "auto-away", "")
|
2021-05-26 08:49:52 +00:00
|
|
|
fs.Var(boolPtrFlag{&fs.Enabled}, "enabled", "")
|
2020-06-02 09:39:53 +00:00
|
|
|
fs.Var((*stringSliceFlag)(&fs.ConnectCommands), "connect-command", "")
|
|
|
|
return fs
|
|
|
|
}
|
2020-03-18 23:57:14 +00:00
|
|
|
|
2022-05-09 10:34:43 +00:00
|
|
|
func (fs *networkFlagSet) update(network *database.Network) error {
|
2020-06-02 09:39:53 +00:00
|
|
|
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 {
|
2022-03-20 13:58:18 +00:00
|
|
|
if *fs.Name == "*" {
|
|
|
|
return fmt.Errorf("the network name %q is reserved for multi-upstream mode", *fs.Name)
|
|
|
|
}
|
2020-06-02 09:39:53 +00:00
|
|
|
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
|
|
|
|
}
|
2022-12-10 08:12:46 +00:00
|
|
|
if fs.CertFP != nil {
|
|
|
|
certFP := strings.ReplaceAll(*fs.CertFP, ":", "")
|
|
|
|
if _, err := hex.DecodeString(certFP); err != nil {
|
|
|
|
return fmt.Errorf("the certificate fingerprint must be hex-encoded")
|
|
|
|
}
|
|
|
|
if len(certFP) == 64 {
|
|
|
|
network.CertFP = "sha-256:" + certFP
|
|
|
|
} else if len(certFP) == 128 {
|
|
|
|
network.CertFP = "sha-512:" + certFP
|
|
|
|
} else {
|
|
|
|
return fmt.Errorf("the certificate fingerprint must be a SHA256 or SHA512 hash")
|
|
|
|
}
|
|
|
|
}
|
2022-09-26 17:49:26 +00:00
|
|
|
if fs.AutoAway != nil {
|
|
|
|
network.AutoAway = *fs.AutoAway
|
|
|
|
}
|
2021-05-26 08:49:52 +00:00
|
|
|
if fs.Enabled != nil {
|
|
|
|
network.Enabled = *fs.Enabled
|
|
|
|
}
|
2020-06-02 09:39:53 +00:00
|
|
|
if fs.ConnectCommands != nil {
|
|
|
|
if len(fs.ConnectCommands) == 1 && fs.ConnectCommands[0] == "" {
|
|
|
|
network.ConnectCommands = nil
|
|
|
|
} else {
|
2022-03-14 18:37:12 +00:00
|
|
|
if len(fs.ConnectCommands) > 20 {
|
|
|
|
return fmt.Errorf("too many -connect-command flags supplied")
|
|
|
|
}
|
2020-06-02 09:39:53 +00:00
|
|
|
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
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceNetworkCreate(ctx *serviceContext, params []string) error {
|
2020-06-02 09:39:53 +00:00
|
|
|
fs := newNetworkFlagSet()
|
|
|
|
if err := fs.Parse(params); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-01-15 12:36:30 +00:00
|
|
|
if fs.NArg() > 0 {
|
|
|
|
return fmt.Errorf("unexpected argument: %v", fs.Arg(0))
|
|
|
|
}
|
2020-06-02 09:39:53 +00:00
|
|
|
if fs.Addr == nil {
|
|
|
|
return fmt.Errorf("flag -addr is required")
|
2020-03-18 23:57:14 +00:00
|
|
|
}
|
|
|
|
|
2022-05-09 10:34:43 +00:00
|
|
|
record := &database.Network{
|
2021-05-26 08:49:52 +00:00
|
|
|
Addr: *fs.Addr,
|
|
|
|
Enabled: true,
|
2020-06-02 09:39:53 +00:00
|
|
|
}
|
|
|
|
if err := fs.update(record); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
network, err := ctx.user.createNetwork(ctx, record)
|
2020-03-18 23:57:14 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not create network: %v", err)
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(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
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceNetworkStatus(ctx *serviceContext, params []string) error {
|
2021-05-26 09:27:59 +00:00
|
|
|
n := 0
|
2023-01-17 12:26:05 +00:00
|
|
|
for _, net := range ctx.user.networks {
|
2020-03-25 21:57:48 +00:00
|
|
|
var statuses []string
|
|
|
|
var details string
|
2020-04-30 08:25:16 +00:00
|
|
|
if uc := net.conn; uc != nil {
|
2023-01-17 12:26:05 +00:00
|
|
|
if ctx.nick != "" && ctx.nick != uc.nick {
|
2020-04-28 14:27:53 +00:00
|
|
|
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())
|
2021-05-26 08:49:52 +00:00
|
|
|
} else if !net.Enabled {
|
|
|
|
statuses = append(statuses, "disabled")
|
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
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
if net == ctx.network {
|
2020-03-25 21:57:48 +00:00
|
|
|
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
|
|
|
|
}
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(s)
|
2021-05-26 09:27:59 +00:00
|
|
|
|
|
|
|
n++
|
2022-02-04 13:01:27 +00:00
|
|
|
}
|
2021-05-26 09:27:59 +00:00
|
|
|
|
|
|
|
if n == 0 {
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(`No network configured, add one with "network create".`)
|
2021-05-26 09:27:59 +00:00
|
|
|
}
|
|
|
|
|
2020-03-25 21:57:48 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-04-01 13:40:20 +00:00
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceNetworkUpdate(ctx *serviceContext, params []string) error {
|
|
|
|
net, params, err := getNetworkFromArg(ctx, params)
|
2022-02-07 20:33:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2020-06-02 09:39:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fs := newNetworkFlagSet()
|
2022-02-07 20:33:16 +00:00
|
|
|
if err := fs.Parse(params); err != nil {
|
2020-06-02 09:39:53 +00:00
|
|
|
return err
|
|
|
|
}
|
2023-01-15 12:36:30 +00:00
|
|
|
if fs.NArg() > 0 {
|
|
|
|
return fmt.Errorf("unexpected argument: %v", fs.Arg(0))
|
|
|
|
}
|
2020-06-02 09:39:53 +00:00
|
|
|
|
|
|
|
record := net.Network // copy network record because we'll mutate it
|
|
|
|
if err := fs.update(&record); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
network, err := ctx.user.updateNetwork(ctx, &record)
|
2020-06-02 09:39:53 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not update network: %v", err)
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("updated network %q", network.GetName()))
|
2020-06-02 09:39:53 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceNetworkDelete(ctx *serviceContext, params []string) error {
|
2023-01-15 12:36:30 +00:00
|
|
|
if len(params) != 1 {
|
|
|
|
return fmt.Errorf("expected exactly one argument")
|
|
|
|
}
|
2023-01-17 12:26:05 +00:00
|
|
|
net, params, err := getNetworkFromArg(ctx, params)
|
2022-02-07 20:33:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2020-04-01 13:40:20 +00:00
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
if err := ctx.user.deleteNetwork(ctx, net.ID); err != nil {
|
2020-04-01 13:40:20 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("deleted network %q", net.GetName()))
|
2020-04-01 13:40:20 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-04-08 12:20:00 +00:00
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceNetworkQuote(ctx *serviceContext, params []string) error {
|
2022-02-07 20:33:16 +00:00
|
|
|
if len(params) != 1 && len(params) != 2 {
|
|
|
|
return fmt.Errorf("expected one or two arguments")
|
2021-07-06 22:44:15 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 20:33:16 +00:00
|
|
|
raw := params[len(params)-1]
|
|
|
|
params = params[:len(params)-1]
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
net, params, err := getNetworkFromArg(ctx, params)
|
2022-02-07 20:33:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-07-06 22:44:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
uc := net.conn
|
|
|
|
if uc == nil {
|
2022-02-07 20:33:16 +00:00
|
|
|
return fmt.Errorf("network %q is not currently connected", net.GetName())
|
2021-07-06 22:44:15 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 20:33:16 +00:00
|
|
|
m, err := irc.ParseMessage(raw)
|
2021-07-06 22:44:15 +00:00
|
|
|
if err != nil {
|
2022-02-07 20:33:16 +00:00
|
|
|
return fmt.Errorf("failed to parse command %q: %v", raw, err)
|
2021-07-06 22:44:15 +00:00
|
|
|
}
|
2021-12-08 17:03:40 +00:00
|
|
|
uc.SendMessage(ctx, m)
|
2021-07-06 22:44:15 +00:00
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("sent command to %q", net.GetName()))
|
2021-07-06 22:44:15 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func sendCertfpFingerprints(ctx *serviceContext, cert []byte) {
|
2021-10-08 07:47:25 +00:00
|
|
|
sha1Sum := sha1.Sum(cert)
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("SHA-1 fingerprint: " + hex.EncodeToString(sha1Sum[:]))
|
2021-10-08 07:47:25 +00:00
|
|
|
sha256Sum := sha256.Sum256(cert)
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("SHA-256 fingerprint: " + hex.EncodeToString(sha256Sum[:]))
|
2021-10-08 07:47:25 +00:00
|
|
|
sha512Sum := sha512.Sum512(cert)
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("SHA-512 fingerprint: " + hex.EncodeToString(sha512Sum[:]))
|
2021-10-08 07:47:25 +00:00
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func getNetworkFromFlag(ctx *serviceContext, name string) (*network, error) {
|
2022-02-04 15:47:34 +00:00
|
|
|
if name == "" {
|
2023-01-17 12:26:05 +00:00
|
|
|
if ctx.network == nil {
|
2022-02-04 15:47:34 +00:00
|
|
|
return nil, fmt.Errorf("no network selected, -network is required")
|
|
|
|
}
|
2023-01-17 12:26:05 +00:00
|
|
|
return ctx.network, nil
|
2022-02-04 15:47:34 +00:00
|
|
|
} else {
|
2023-01-17 12:26:05 +00:00
|
|
|
net := ctx.user.getNetwork(name)
|
2022-02-04 15:47:34 +00:00
|
|
|
if net == nil {
|
|
|
|
return nil, fmt.Errorf("unknown network %q", name)
|
|
|
|
}
|
|
|
|
return net, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceCertFPGenerate(ctx *serviceContext, params []string) error {
|
2020-06-06 23:19:25 +00:00
|
|
|
fs := newFlagSet()
|
2022-02-04 15:47:34 +00:00
|
|
|
netName := fs.String("network", "", "select a network")
|
2020-06-06 23:19:25 +00:00
|
|
|
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
|
|
|
|
}
|
2023-01-15 12:36:30 +00:00
|
|
|
if fs.NArg() > 0 {
|
|
|
|
return fmt.Errorf("unexpected argument: %v", fs.Arg(0))
|
|
|
|
}
|
2020-06-06 23:19:25 +00:00
|
|
|
|
2021-10-08 07:47:25 +00:00
|
|
|
if *bits <= 0 || *bits > maxRSABits {
|
|
|
|
return fmt.Errorf("invalid value for -bits")
|
2020-06-06 23:19:25 +00:00
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
net, err := getNetworkFromFlag(ctx, *netName)
|
2022-02-04 15:47:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-10-08 07:47:25 +00:00
|
|
|
privKey, cert, err := generateCertFP(*keyType, *bits)
|
2020-06-06 23:19:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-10-08 07:47:25 +00:00
|
|
|
net.SASL.External.CertBlob = cert
|
|
|
|
net.SASL.External.PrivKeyBlob = privKey
|
2020-06-06 23:19:25 +00:00
|
|
|
net.SASL.Mechanism = "EXTERNAL"
|
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
if err := ctx.srv.db.StoreNetwork(ctx, ctx.user.ID, &net.Network); err != nil {
|
2020-06-06 23:19:25 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("certificate generated")
|
|
|
|
sendCertfpFingerprints(ctx, cert)
|
2020-06-06 23:19:25 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceCertFPFingerprints(ctx *serviceContext, params []string) error {
|
2022-02-04 15:47:34 +00:00
|
|
|
fs := newFlagSet()
|
|
|
|
netName := fs.String("network", "", "select a network")
|
|
|
|
|
|
|
|
if err := fs.Parse(params); err != nil {
|
|
|
|
return err
|
2020-06-06 23:19:25 +00:00
|
|
|
}
|
2023-01-15 12:36:30 +00:00
|
|
|
if fs.NArg() > 0 {
|
|
|
|
return fmt.Errorf("unexpected argument: %v", fs.Arg(0))
|
|
|
|
}
|
2020-06-06 23:19:25 +00:00
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
net, err := getNetworkFromFlag(ctx, *netName)
|
2022-02-04 15:47:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2020-06-06 23:19:25 +00:00
|
|
|
}
|
|
|
|
|
2021-10-08 07:47:25 +00:00
|
|
|
if net.SASL.Mechanism != "EXTERNAL" {
|
|
|
|
return fmt.Errorf("CertFP not set up")
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
sendCertfpFingerprints(ctx, net.SASL.External.CertBlob)
|
2020-06-06 23:19:25 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceSASLStatus(ctx *serviceContext, params []string) error {
|
2022-02-04 15:47:34 +00:00
|
|
|
fs := newFlagSet()
|
|
|
|
netName := fs.String("network", "", "select a network")
|
|
|
|
|
|
|
|
if err := fs.Parse(params); err != nil {
|
|
|
|
return err
|
2021-12-01 10:03:27 +00:00
|
|
|
}
|
2023-01-15 12:36:30 +00:00
|
|
|
if fs.NArg() > 0 {
|
|
|
|
return fmt.Errorf("unexpected argument: %v", fs.Arg(0))
|
|
|
|
}
|
2021-12-01 10:03:27 +00:00
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
net, err := getNetworkFromFlag(ctx, *netName)
|
2022-02-04 15:47:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-12-01 10:03:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
switch net.SASL.Mechanism {
|
|
|
|
case "PLAIN":
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("SASL PLAIN enabled with username %q", net.SASL.Plain.Username))
|
2021-12-01 10:03:27 +00:00
|
|
|
case "EXTERNAL":
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("SASL EXTERNAL (CertFP) enabled")
|
2021-12-01 10:03:27 +00:00
|
|
|
case "":
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("SASL is disabled")
|
2021-12-01 10:03:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if uc := net.conn; uc != nil {
|
|
|
|
if uc.account != "" {
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("Authenticated on upstream network with account %q", uc.account))
|
2021-12-01 10:03:27 +00:00
|
|
|
} else {
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("Unauthenticated on upstream network")
|
2021-12-01 10:03:27 +00:00
|
|
|
}
|
|
|
|
} else {
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("Disconnected from upstream network")
|
2021-12-01 10:03:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceSASLSetPlain(ctx *serviceContext, params []string) error {
|
2022-02-04 15:47:34 +00:00
|
|
|
fs := newFlagSet()
|
|
|
|
netName := fs.String("network", "", "select a network")
|
|
|
|
|
|
|
|
if err := fs.Parse(params); err != nil {
|
|
|
|
return err
|
2020-06-06 23:19:25 +00:00
|
|
|
}
|
|
|
|
|
2023-01-15 12:36:30 +00:00
|
|
|
if fs.NArg() != 2 {
|
2022-02-04 15:47:34 +00:00
|
|
|
return fmt.Errorf("expected exactly 2 arguments")
|
2020-06-06 23:19:25 +00:00
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
net, err := getNetworkFromFlag(ctx, *netName)
|
2022-02-04 15:47:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
net.SASL.Plain.Username = fs.Arg(0)
|
|
|
|
net.SASL.Plain.Password = fs.Arg(1)
|
2020-07-22 10:20:52 +00:00
|
|
|
net.SASL.Mechanism = "PLAIN"
|
2020-06-06 23:19:25 +00:00
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
if err := ctx.srv.db.StoreNetwork(ctx, ctx.user.ID, &net.Network); err != nil {
|
2020-06-06 23:19:25 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("credentials saved")
|
2020-06-06 23:19:25 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceSASLReset(ctx *serviceContext, params []string) error {
|
2022-02-04 15:47:34 +00:00
|
|
|
fs := newFlagSet()
|
|
|
|
netName := fs.String("network", "", "select a network")
|
|
|
|
|
|
|
|
if err := fs.Parse(params); err != nil {
|
|
|
|
return err
|
2020-07-22 10:16:13 +00:00
|
|
|
}
|
2023-01-15 12:36:30 +00:00
|
|
|
if fs.NArg() > 0 {
|
|
|
|
return fmt.Errorf("unexpected argument: %v", fs.Arg(0))
|
|
|
|
}
|
2020-07-22 10:16:13 +00:00
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
net, err := getNetworkFromFlag(ctx, *netName)
|
2022-02-04 15:47:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2020-07-22 10:16:13 +00:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
if err := ctx.srv.db.StoreNetwork(ctx, ctx.user.ID, &net.Network); err != nil {
|
2020-07-22 10:16:13 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("credentials reset")
|
2020-07-22 10:16:13 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-17 13:25:37 +00:00
|
|
|
func handleUserStatus(ctx *serviceContext, params []string) error {
|
|
|
|
// Limit to a small amount of users to avoid sending
|
|
|
|
// thousands of messages on large instances.
|
|
|
|
users := make([]database.User, 0, 50)
|
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
ctx.srv.lock.Lock()
|
|
|
|
n := len(ctx.srv.users)
|
|
|
|
for _, user := range ctx.srv.users {
|
2023-01-17 13:25:37 +00:00
|
|
|
if len(users) == cap(users) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
users = append(users, user.User)
|
|
|
|
}
|
2023-01-19 17:33:22 +00:00
|
|
|
ctx.srv.lock.Unlock()
|
2023-01-17 13:25:37 +00:00
|
|
|
|
|
|
|
for _, user := range users {
|
2023-01-26 18:41:38 +00:00
|
|
|
var attrs []string
|
2023-01-17 13:25:37 +00:00
|
|
|
if user.Admin {
|
2023-01-26 18:41:38 +00:00
|
|
|
attrs = append(attrs, "admin")
|
|
|
|
}
|
|
|
|
if !user.Enabled {
|
|
|
|
attrs = append(attrs, "disabled")
|
|
|
|
}
|
|
|
|
|
|
|
|
line := user.Username
|
|
|
|
if len(attrs) > 0 {
|
|
|
|
line += " (" + strings.Join(attrs, ", ") + ")"
|
2023-01-17 13:25:37 +00:00
|
|
|
}
|
2023-01-19 17:33:22 +00:00
|
|
|
networks, err := ctx.srv.db.ListNetworks(ctx, user.ID)
|
2023-01-17 13:25:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not get networks of user %q: %v", user.Username, err)
|
|
|
|
}
|
|
|
|
line += fmt.Sprintf(": %d networks", len(networks))
|
|
|
|
ctx.print(line)
|
|
|
|
}
|
|
|
|
if n > len(users) {
|
|
|
|
ctx.print(fmt.Sprintf("(%d more users omitted)", n-len(users)))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleUserCreate(ctx *serviceContext, params []string) error {
|
2020-06-06 23:30:27 +00:00
|
|
|
fs := newFlagSet()
|
|
|
|
username := fs.String("username", "", "")
|
|
|
|
password := fs.String("password", "", "")
|
2023-01-26 19:03:37 +00:00
|
|
|
disablePassword := fs.Bool("disable-password", false, "")
|
2022-07-08 16:01:05 +00:00
|
|
|
nick := fs.String("nick", "", "")
|
2021-06-25 18:33:13 +00:00
|
|
|
realname := fs.String("realname", "", "")
|
2020-06-06 23:30:27 +00:00
|
|
|
admin := fs.Bool("admin", false, "")
|
2023-01-26 17:33:55 +00:00
|
|
|
enabled := fs.Bool("enabled", true, "")
|
2020-06-06 23:30:27 +00:00
|
|
|
|
|
|
|
if err := fs.Parse(params); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-01-15 12:36:30 +00:00
|
|
|
if fs.NArg() > 0 {
|
|
|
|
return fmt.Errorf("unexpected argument: %v", fs.Arg(0))
|
|
|
|
}
|
2020-06-06 23:30:27 +00:00
|
|
|
if *username == "" {
|
|
|
|
return fmt.Errorf("flag -username is required")
|
|
|
|
}
|
2023-01-26 19:03:37 +00:00
|
|
|
if *password != "" && *disablePassword {
|
|
|
|
return fmt.Errorf("flags -password and -disable-password are mutually exclusive")
|
|
|
|
}
|
|
|
|
if *password == "" && !*disablePassword {
|
2020-06-06 23:30:27 +00:00
|
|
|
return fmt.Errorf("flag -password is required")
|
|
|
|
}
|
|
|
|
|
2022-05-09 10:34:43 +00:00
|
|
|
user := &database.User{
|
2020-06-06 23:30:27 +00:00
|
|
|
Username: *username,
|
2022-07-08 16:01:05 +00:00
|
|
|
Nick: *nick,
|
2021-06-25 18:33:13 +00:00
|
|
|
Realname: *realname,
|
2020-06-06 23:30:27 +00:00
|
|
|
Admin: *admin,
|
2023-01-26 17:33:55 +00:00
|
|
|
Enabled: *enabled,
|
2020-06-06 23:30:27 +00:00
|
|
|
}
|
2023-01-26 19:03:37 +00:00
|
|
|
if !*disablePassword {
|
|
|
|
if err := user.SetPassword(*password); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-06-08 11:27:33 +00:00
|
|
|
}
|
2023-01-19 17:33:22 +00:00
|
|
|
if _, err := ctx.srv.createUser(ctx, user); err != nil {
|
2020-06-06 23:30:27 +00:00
|
|
|
return fmt.Errorf("could not create user: %v", err)
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("created user %q", *username))
|
2020-06-06 23:30:27 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-08-10 13:03:07 +00:00
|
|
|
|
2021-10-12 07:11:14 +00:00
|
|
|
func popArg(params []string) (string, []string) {
|
|
|
|
if len(params) > 0 && !strings.HasPrefix(params[0], "-") {
|
|
|
|
return params[0], params[1:]
|
|
|
|
}
|
|
|
|
return "", params
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleUserUpdate(ctx *serviceContext, params []string) error {
|
2022-07-08 16:01:05 +00:00
|
|
|
var password, nick, realname *string
|
2023-01-26 17:33:55 +00:00
|
|
|
var admin, enabled *bool
|
2023-01-26 19:03:37 +00:00
|
|
|
var disablePassword bool
|
2021-06-25 18:33:13 +00:00
|
|
|
fs := newFlagSet()
|
2021-06-28 14:49:16 +00:00
|
|
|
fs.Var(stringPtrFlag{&password}, "password", "")
|
2023-01-26 19:03:37 +00:00
|
|
|
fs.BoolVar(&disablePassword, "disable-password", false, "")
|
2022-07-08 16:01:05 +00:00
|
|
|
fs.Var(stringPtrFlag{&nick}, "nick", "")
|
2021-06-28 14:40:49 +00:00
|
|
|
fs.Var(stringPtrFlag{&realname}, "realname", "")
|
2021-10-12 07:11:14 +00:00
|
|
|
fs.Var(boolPtrFlag{&admin}, "admin", "")
|
2023-01-26 17:33:55 +00:00
|
|
|
fs.Var(boolPtrFlag{&enabled}, "enabled", "")
|
2021-06-25 18:33:13 +00:00
|
|
|
|
2021-10-12 07:11:14 +00:00
|
|
|
username, params := popArg(params)
|
2021-06-25 18:33:13 +00:00
|
|
|
if err := fs.Parse(params); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-01-15 12:36:30 +00:00
|
|
|
if fs.NArg() > 0 {
|
|
|
|
return fmt.Errorf("unexpected argument: %v", fs.Arg(0))
|
2021-10-12 07:11:14 +00:00
|
|
|
}
|
2023-01-19 17:33:22 +00:00
|
|
|
if username == "" && ctx.user == nil {
|
|
|
|
return fmt.Errorf("cannot determine the user to update")
|
|
|
|
}
|
2021-06-25 18:33:13 +00:00
|
|
|
|
2023-01-26 19:03:37 +00:00
|
|
|
if password != nil && disablePassword {
|
|
|
|
return fmt.Errorf("flags -password and -disable-password are mutually exclusive")
|
|
|
|
}
|
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
if username != "" && (ctx.user == nil || username != ctx.user.Username) {
|
2023-01-19 17:13:59 +00:00
|
|
|
if !ctx.admin {
|
2021-10-12 07:11:14 +00:00
|
|
|
return fmt.Errorf("you must be an admin to update other users")
|
|
|
|
}
|
2022-07-08 16:01:05 +00:00
|
|
|
if nick != nil {
|
|
|
|
return fmt.Errorf("cannot update -nick of other user")
|
|
|
|
}
|
2021-10-12 07:11:14 +00:00
|
|
|
if realname != nil {
|
|
|
|
return fmt.Errorf("cannot update -realname of other user")
|
|
|
|
}
|
|
|
|
|
2022-06-08 11:27:33 +00:00
|
|
|
var hashed *string
|
|
|
|
if password != nil {
|
2023-03-01 13:14:09 +00:00
|
|
|
var passwordRecord database.User
|
|
|
|
if err := passwordRecord.SetPassword(*password); err != nil {
|
|
|
|
return err
|
2022-06-08 11:27:33 +00:00
|
|
|
}
|
2023-03-01 13:14:09 +00:00
|
|
|
hashed = &passwordRecord.Password
|
2022-06-08 11:27:33 +00:00
|
|
|
}
|
2023-01-26 19:03:37 +00:00
|
|
|
if disablePassword {
|
|
|
|
hashedStr := ""
|
|
|
|
hashed = &hashedStr
|
|
|
|
}
|
2022-06-08 11:27:33 +00:00
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
u := ctx.srv.getUser(username)
|
2021-10-12 07:11:14 +00:00
|
|
|
if u == nil {
|
|
|
|
return fmt.Errorf("unknown username %q", username)
|
|
|
|
}
|
|
|
|
|
|
|
|
done := make(chan error, 1)
|
2021-11-08 18:48:32 +00:00
|
|
|
event := eventUserUpdate{
|
2021-10-12 07:11:14 +00:00
|
|
|
password: hashed,
|
|
|
|
admin: admin,
|
2023-01-26 17:33:55 +00:00
|
|
|
enabled: enabled,
|
2021-10-12 07:11:14 +00:00
|
|
|
done: done,
|
|
|
|
}
|
2021-11-08 18:48:32 +00:00
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
|
|
|
case u.events <- event:
|
|
|
|
}
|
|
|
|
// TODO: send context to the other side
|
2021-10-12 07:11:14 +00:00
|
|
|
if err := <-done; err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("updated user %q", username))
|
2021-10-12 07:11:14 +00:00
|
|
|
} else {
|
|
|
|
if admin != nil {
|
|
|
|
return fmt.Errorf("cannot update -admin of own user")
|
|
|
|
}
|
2023-01-26 17:33:55 +00:00
|
|
|
if enabled != nil {
|
|
|
|
return fmt.Errorf("cannot update -enabled of own user")
|
|
|
|
}
|
2021-10-12 07:11:14 +00:00
|
|
|
|
2023-03-01 13:16:33 +00:00
|
|
|
err := ctx.user.updateUser(ctx, func(record *database.User) error {
|
|
|
|
if password != nil {
|
|
|
|
if err := record.SetPassword(*password); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if disablePassword {
|
|
|
|
record.Password = ""
|
|
|
|
}
|
|
|
|
if nick != nil {
|
|
|
|
record.Nick = *nick
|
|
|
|
}
|
|
|
|
if realname != nil {
|
|
|
|
record.Realname = *realname
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
2021-10-12 07:11:14 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("updated user %q", ctx.user.Username))
|
2021-06-25 18:33:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleUserDelete(ctx *serviceContext, params []string) error {
|
2022-06-24 21:22:01 +00:00
|
|
|
if len(params) != 1 && len(params) != 2 {
|
|
|
|
return fmt.Errorf("expected one or two arguments")
|
2020-08-10 13:03:07 +00:00
|
|
|
}
|
2022-06-24 21:22:01 +00:00
|
|
|
|
2020-08-10 13:03:07 +00:00
|
|
|
username := params[0]
|
2022-06-24 21:22:01 +00:00
|
|
|
hashBytes := sha1.Sum([]byte(username))
|
|
|
|
hash := fmt.Sprintf("%x", hashBytes[0:3])
|
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
self := ctx.user != nil && ctx.user.Username == username
|
2022-06-24 21:22:01 +00:00
|
|
|
|
2023-01-19 17:13:59 +00:00
|
|
|
if !ctx.admin && !self {
|
2022-06-24 21:22:01 +00:00
|
|
|
return fmt.Errorf("only admins may delete other users")
|
|
|
|
}
|
2020-08-10 13:03:07 +00:00
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
u := ctx.srv.getUser(username)
|
2020-08-10 13:03:07 +00:00
|
|
|
if u == nil {
|
|
|
|
return fmt.Errorf("unknown username %q", username)
|
|
|
|
}
|
|
|
|
|
2022-06-24 21:22:01 +00:00
|
|
|
if len(params) < 2 {
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf(`To confirm user deletion, send "user delete %s %s"`, username, hash))
|
2022-06-24 21:22:01 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if token := params[1]; token != hash {
|
|
|
|
return fmt.Errorf("provided confirmation token doesn't match user")
|
|
|
|
}
|
|
|
|
|
2023-01-26 15:32:21 +00:00
|
|
|
var deleteCtx context.Context = ctx
|
2022-06-24 21:22:01 +00:00
|
|
|
if self {
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("Goodbye %s, deleting your account. There will be no further confirmation.", username))
|
2023-01-26 15:32:21 +00:00
|
|
|
// We can't use ctx here, because it'll be cancelled once we close the
|
|
|
|
// downstream connection
|
|
|
|
deleteCtx = context.TODO()
|
2022-06-24 21:22:01 +00:00
|
|
|
}
|
|
|
|
|
2023-01-26 15:32:21 +00:00
|
|
|
if err := u.stop(deleteCtx); err != nil {
|
|
|
|
return fmt.Errorf("failed to stop user: %v", err)
|
|
|
|
}
|
2020-08-10 13:03:07 +00:00
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
if err := ctx.srv.db.DeleteUser(deleteCtx, u.ID); err != nil {
|
2020-08-10 13:03:07 +00:00
|
|
|
return fmt.Errorf("failed to delete user: %v", err)
|
|
|
|
}
|
|
|
|
|
2022-06-24 21:22:01 +00:00
|
|
|
if !self {
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("deleted user %q", username))
|
2022-06-24 21:22:01 +00:00
|
|
|
}
|
|
|
|
|
2020-08-10 13:03:07 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-11-30 21:16:44 +00:00
|
|
|
|
2023-01-17 13:03:13 +00:00
|
|
|
func handleUserRun(ctx *serviceContext, params []string) error {
|
|
|
|
if len(params) < 2 {
|
|
|
|
return fmt.Errorf("expected at least two arguments")
|
|
|
|
}
|
|
|
|
|
|
|
|
username := params[0]
|
|
|
|
params = params[1:]
|
2023-01-19 17:33:22 +00:00
|
|
|
if ctx.user != nil && username == ctx.user.Username {
|
2023-01-20 14:46:42 +00:00
|
|
|
return handleServiceCommand(ctx, params)
|
2023-01-17 13:03:13 +00:00
|
|
|
}
|
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
u := ctx.srv.getUser(username)
|
2023-01-17 13:03:13 +00:00
|
|
|
if u == nil {
|
|
|
|
return fmt.Errorf("unknown username %q", username)
|
|
|
|
}
|
|
|
|
|
|
|
|
printCh := make(chan string, 1)
|
2023-01-20 14:46:42 +00:00
|
|
|
retCh := make(chan error, 1)
|
2023-01-17 13:03:13 +00:00
|
|
|
ev := eventUserRun{
|
|
|
|
params: params,
|
|
|
|
print: printCh,
|
2023-01-20 14:46:42 +00:00
|
|
|
ret: retCh,
|
2023-01-17 13:03:13 +00:00
|
|
|
}
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
|
|
|
case u.events <- ev:
|
|
|
|
}
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
// This handles a possible race condition:
|
|
|
|
// - we send ev to u.events
|
|
|
|
// - the user goroutine for u stops (because of a crash or user deletion)
|
|
|
|
// - we would block on printCh
|
|
|
|
// Quitting on ctx.Done() prevents us from blocking indefinitely
|
|
|
|
// in case the event is never processed.
|
|
|
|
// TODO: Properly fix this condition by flushing the u.events queue
|
|
|
|
// and running close(ev.print) in a defer
|
2023-01-20 14:46:42 +00:00
|
|
|
return fmt.Errorf("timeout executing command")
|
2023-01-17 13:03:13 +00:00
|
|
|
case text, ok := <-printCh:
|
2023-01-20 14:46:42 +00:00
|
|
|
if ok {
|
|
|
|
ctx.print(text)
|
2023-01-17 13:03:13 +00:00
|
|
|
}
|
2023-01-20 14:46:42 +00:00
|
|
|
case ret := <-retCh:
|
|
|
|
return ret
|
2023-01-17 13:03:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceChannelStatus(ctx *serviceContext, params []string) error {
|
2021-05-25 17:22:22 +00:00
|
|
|
var defaultNetworkName string
|
2023-01-17 12:26:05 +00:00
|
|
|
if ctx.network != nil {
|
|
|
|
defaultNetworkName = ctx.network.GetName()
|
2021-05-25 17:22:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fs := newFlagSet()
|
|
|
|
networkName := fs.String("network", defaultNetworkName, "")
|
|
|
|
|
|
|
|
if err := fs.Parse(params); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-01-15 12:36:30 +00:00
|
|
|
if fs.NArg() > 0 {
|
|
|
|
return fmt.Errorf("unexpected argument: %v", fs.Arg(0))
|
|
|
|
}
|
2021-05-25 17:22:22 +00:00
|
|
|
|
2021-05-26 09:27:59 +00:00
|
|
|
n := 0
|
|
|
|
|
2021-05-25 17:22:22 +00:00
|
|
|
sendNetwork := func(net *network) {
|
2022-05-09 10:34:43 +00:00
|
|
|
var channels []*database.Channel
|
2023-03-01 12:15:38 +00:00
|
|
|
net.channels.ForEach(func(_ string, ch *database.Channel) {
|
2022-06-06 07:58:39 +00:00
|
|
|
channels = append(channels, ch)
|
|
|
|
})
|
2021-06-29 15:03:14 +00:00
|
|
|
|
|
|
|
sort.Slice(channels, func(i, j int) bool {
|
|
|
|
return strings.ReplaceAll(channels[i].Name, "#", "") <
|
|
|
|
strings.ReplaceAll(channels[j].Name, "#", "")
|
|
|
|
})
|
2021-05-25 17:22:22 +00:00
|
|
|
|
2021-06-29 15:03:14 +00:00
|
|
|
for _, ch := range channels {
|
2021-05-25 17:22:22 +00:00
|
|
|
var uch *upstreamChannel
|
|
|
|
if net.conn != nil {
|
2022-06-06 07:58:39 +00:00
|
|
|
uch = net.conn.channels.Get(ch.Name)
|
2021-05-25 17:22:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(s)
|
2021-05-26 09:27:59 +00:00
|
|
|
|
|
|
|
n++
|
2021-05-25 17:22:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if *networkName == "" {
|
2023-01-17 12:26:05 +00:00
|
|
|
for _, net := range ctx.user.networks {
|
2022-02-04 13:01:27 +00:00
|
|
|
sendNetwork(net)
|
|
|
|
}
|
2021-05-25 17:22:22 +00:00
|
|
|
} else {
|
2023-01-17 12:26:05 +00:00
|
|
|
net := ctx.user.getNetwork(*networkName)
|
2021-05-25 17:22:22 +00:00
|
|
|
if net == nil {
|
|
|
|
return fmt.Errorf("unknown network %q", *networkName)
|
|
|
|
}
|
|
|
|
sendNetwork(net)
|
|
|
|
}
|
|
|
|
|
2021-05-26 09:27:59 +00:00
|
|
|
if n == 0 {
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print("No channel configured.")
|
2021-05-26 09:27:59 +00:00
|
|
|
}
|
|
|
|
|
2021-05-25 17:22:22 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-05-09 10:34:43 +00:00
|
|
|
func parseFilter(filter string) (database.MessageFilter, error) {
|
|
|
|
switch filter {
|
|
|
|
case "default":
|
|
|
|
return database.FilterDefault, nil
|
|
|
|
case "none":
|
|
|
|
return database.FilterNone, nil
|
|
|
|
case "highlight":
|
|
|
|
return database.FilterHighlight, nil
|
|
|
|
case "message":
|
|
|
|
return database.FilterMessage, nil
|
|
|
|
}
|
|
|
|
return 0, fmt.Errorf("unknown filter: %q", filter)
|
|
|
|
}
|
|
|
|
|
2020-11-30 21:16:44 +00:00
|
|
|
type channelFlagSet struct {
|
|
|
|
*flag.FlagSet
|
2022-06-24 18:41:13 +00:00
|
|
|
Detached *bool
|
2020-11-30 21:16:44 +00:00
|
|
|
RelayDetached, ReattachOn, DetachAfter, DetachOn *string
|
|
|
|
}
|
|
|
|
|
|
|
|
func newChannelFlagSet() *channelFlagSet {
|
|
|
|
fs := &channelFlagSet{FlagSet: newFlagSet()}
|
2022-06-24 18:41:13 +00:00
|
|
|
fs.Var(boolPtrFlag{&fs.Detached}, "detached", "")
|
2020-11-30 21:16:44 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-05-09 10:34:43 +00:00
|
|
|
func (fs *channelFlagSet) update(channel *database.Channel) error {
|
2020-11-30 21:16:44 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func stripNetworkSuffix(ctx *serviceContext, name string) (string, *network, error) {
|
|
|
|
if ctx.network != nil {
|
|
|
|
return name, ctx.network, nil
|
2022-12-08 15:00:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
l := strings.SplitN(name, "/", 2)
|
|
|
|
if len(l) != 2 {
|
|
|
|
return "", nil, fmt.Errorf("missing network name")
|
|
|
|
}
|
|
|
|
name = l[0]
|
|
|
|
netName := l[1]
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
for _, network := range ctx.user.networks {
|
2022-12-08 15:00:00 +00:00
|
|
|
if netName == network.GetName() {
|
|
|
|
return name, network, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", nil, fmt.Errorf("unknown network %q", netName)
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceChannelUpdate(ctx *serviceContext, params []string) error {
|
2020-11-30 21:16:44 +00:00
|
|
|
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
|
|
|
|
}
|
2023-01-15 12:36:30 +00:00
|
|
|
if fs.NArg() > 0 {
|
|
|
|
return fmt.Errorf("unexpected argument: %v", fs.Arg(0))
|
|
|
|
}
|
2020-11-30 21:16:44 +00:00
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
name, network, err := stripNetworkSuffix(ctx, name)
|
2022-12-08 15:00:00 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2020-11-30 21:16:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-08 09:30:10 +00:00
|
|
|
ch := network.channels.Get(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
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-06-24 18:41:13 +00:00
|
|
|
if fs.Detached != nil && *fs.Detached != ch.Detached {
|
|
|
|
if *fs.Detached {
|
2022-08-08 09:30:10 +00:00
|
|
|
network.detach(ch)
|
2022-06-24 18:41:13 +00:00
|
|
|
} else {
|
2022-08-08 09:30:10 +00:00
|
|
|
network.attach(ctx, ch)
|
2022-06-24 18:41:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-08 09:30:10 +00:00
|
|
|
if network.conn != nil {
|
|
|
|
network.conn.updateChannelAutoDetach(name)
|
|
|
|
}
|
2020-11-30 21:16:44 +00:00
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
if err := ctx.srv.db.StoreChannel(ctx, network.ID, ch); err != nil {
|
2020-11-30 21:16:44 +00:00
|
|
|
return fmt.Errorf("failed to update channel: %v", err)
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("updated channel %q", name))
|
2020-11-30 21:16:44 +00:00
|
|
|
return nil
|
|
|
|
}
|
2021-10-05 17:12:25 +00:00
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceChannelDelete(ctx *serviceContext, params []string) error {
|
2023-01-15 12:36:30 +00:00
|
|
|
if len(params) != 1 {
|
|
|
|
return fmt.Errorf("expected exactly one argument")
|
2022-12-08 15:00:00 +00:00
|
|
|
}
|
|
|
|
name := params[0]
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
name, network, err := stripNetworkSuffix(ctx, name)
|
2022-12-08 15:00:00 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := network.deleteChannel(ctx, name); err != nil {
|
|
|
|
return fmt.Errorf("failed to delete channel: %v", err)
|
|
|
|
}
|
|
|
|
|
2022-12-08 15:04:01 +00:00
|
|
|
if uc := network.conn; uc != nil {
|
|
|
|
uc.SendMessage(ctx, &irc.Message{
|
|
|
|
Command: "PART",
|
|
|
|
Params: []string{name},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("deleted channel %q", name))
|
2022-12-08 15:00:00 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceServerStatus(ctx *serviceContext, params []string) error {
|
2023-01-19 17:33:22 +00:00
|
|
|
dbStats, err := ctx.srv.db.Stats(ctx)
|
2021-10-05 17:31:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-01-19 17:33:22 +00:00
|
|
|
serverStats := ctx.srv.Stats()
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("%v/%v users, %v downstreams, %v upstreams, %v networks, %v channels", serverStats.Users, dbStats.Users, serverStats.Downstreams, serverStats.Upstreams, dbStats.Networks, dbStats.Channels))
|
2021-10-05 17:12:25 +00:00
|
|
|
return nil
|
|
|
|
}
|
2021-10-08 08:52:03 +00:00
|
|
|
|
2023-01-17 12:26:05 +00:00
|
|
|
func handleServiceServerNotice(ctx *serviceContext, params []string) error {
|
2021-10-08 08:52:03 +00:00
|
|
|
if len(params) != 1 {
|
|
|
|
return fmt.Errorf("expected exactly one argument")
|
|
|
|
}
|
|
|
|
text := params[0]
|
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
var logger Logger
|
|
|
|
if ctx.user != nil {
|
|
|
|
logger = ctx.user.logger
|
|
|
|
} else {
|
|
|
|
logger = ctx.srv.Logger
|
|
|
|
}
|
|
|
|
logger.Printf("broadcasting bouncer-wide NOTICE: %v", text)
|
2021-10-08 08:52:03 +00:00
|
|
|
|
|
|
|
broadcastMsg := &irc.Message{
|
|
|
|
Prefix: servicePrefix,
|
|
|
|
Command: "NOTICE",
|
2023-01-19 17:33:22 +00:00
|
|
|
Params: []string{"$" + ctx.srv.Config().Hostname, text},
|
2021-10-08 08:52:03 +00:00
|
|
|
}
|
2021-11-08 18:42:36 +00:00
|
|
|
var err error
|
2021-12-08 13:55:31 +00:00
|
|
|
sent := 0
|
|
|
|
total := 0
|
2023-01-19 17:33:22 +00:00
|
|
|
ctx.srv.forEachUser(func(u *user) {
|
2021-12-08 13:55:31 +00:00
|
|
|
total++
|
2021-11-08 18:42:36 +00:00
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
err = ctx.Err()
|
|
|
|
case u.events <- eventBroadcast{broadcastMsg}:
|
2021-12-08 13:55:31 +00:00
|
|
|
sent++
|
2021-11-08 18:42:36 +00:00
|
|
|
}
|
2021-10-08 08:52:03 +00:00
|
|
|
})
|
2021-12-08 13:55:31 +00:00
|
|
|
|
2023-01-19 17:33:22 +00:00
|
|
|
logger.Printf("broadcast bouncer-wide NOTICE to %v/%v downstreams", sent, total)
|
2023-01-17 12:26:05 +00:00
|
|
|
ctx.print(fmt.Sprintf("sent to %v/%v downstream connections", sent, total))
|
2021-12-08 13:55:31 +00:00
|
|
|
|
2021-11-08 18:42:36 +00:00
|
|
|
return err
|
2021-10-08 08:52:03 +00:00
|
|
|
}
|