3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-29 07:29:31 +01:00

Merge remote-tracking branch 'origin/master' into websockets_draft.2

This commit is contained in:
Shivaram Lingamneni 2020-04-30 22:16:07 -04:00
commit 25813f6d3a
20 changed files with 268 additions and 77 deletions

View File

@ -4,7 +4,7 @@ Oragono is a modern IRC server written in Go. Its core design principles are:
* Being simple to set up and use * Being simple to set up and use
* Combining the features of an ircd, a services framework, and a bouncer (integrated account management, history storage, and bouncer functionality) * Combining the features of an ircd, a services framework, and a bouncer (integrated account management, history storage, and bouncer functionality)
* Bleeding-edge [IRCv3 support](http://ircv3.net/software/servers.html), suitable for use as an IRCv3 reference implementation * Bleeding-edge [IRCv3 support](https://ircv3.net/software/servers.html), suitable for use as an IRCv3 reference implementation
* Highly customizable via a rehashable (i.e., reloadable at runtime) YAML config * Highly customizable via a rehashable (i.e., reloadable at runtime) YAML config
Oragono is a fork of the [Ergonomadic](https://github.com/jlatt/ergonomadic) IRC daemon <3 Oragono is a fork of the [Ergonomadic](https://github.com/jlatt/ergonomadic) IRC daemon <3
@ -39,7 +39,7 @@ If you want to take a look at a running Oragono instance or test some client cod
* automated client connection limits * automated client connection limits
* passwords stored with [bcrypt](https://godoc.org/golang.org/x/crypto) * passwords stored with [bcrypt](https://godoc.org/golang.org/x/crypto)
* banning ips/nets and masks with `KLINE` and `DLINE` * banning ips/nets and masks with `KLINE` and `DLINE`
* [IRCv3 support](http://ircv3.net/software/servers.html) * [IRCv3 support](https://ircv3.net/software/servers.html)
* a heavy focus on developing with [specifications](https://oragono.io/specs.html) * a heavy focus on developing with [specifications](https://oragono.io/specs.html)
## Installation ## Installation
@ -112,7 +112,7 @@ oragono run
### How to register a channel ### How to register a channel
1. Register your account with `/NS REGISTER <username> <email> <password>` 1. Register your account with `/NS REGISTER <password>`
2. Join the channel with `/join #channel` 2. Join the channel with `/join #channel`
3. Register the channel with `/CS REGISTER #channel` 3. Register the channel with `/CS REGISTER #channel`

View File

@ -434,6 +434,12 @@ accounts:
offer-list: offer-list:
#- "oragono.test" #- "oragono.test"
# modes that are set by default when a user connects
# if unset, no user modes will be set by default
# +i is invisible (a user's channels are hidden from whois replies)
# see /QUOTE HELP umodes for more user modes
# default-user-modes: +i
# support for deferring password checking to an external LDAP server # support for deferring password checking to an external LDAP server
# you should probably ignore this section! consult the grafana docs for details: # you should probably ignore this section! consult the grafana docs for details:
# https://grafana.com/docs/grafana/latest/auth/ldap/ # https://grafana.com/docs/grafana/latest/auth/ldap/
@ -495,6 +501,10 @@ channels:
# how many channels can each account register? # how many channels can each account register?
max-channels-per-account: 15 max-channels-per-account: 15
# as a crude countermeasure against spambots, anonymous connections younger
# than this value will get an empty response to /LIST (a time period of 0 disables)
list-delay: 0s
# operator classes # operator classes
oper-classes: oper-classes:
# local operator # local operator

View File

@ -0,0 +1,19 @@
[Unit]
Description=oragono
After=network.target
# If you are using MySQL for history storage, comment out the above line
# and uncomment these two instead:
# Requires=mysql.service
# After=network.target mysql.service
[Service]
Type=simple
User=oragono
WorkingDirectory=/home/oragono
ExecStart=/home/oragono/oragono run --conf /home/oragono/ircd.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target

View File

@ -22,6 +22,7 @@ _Copyright © Daniel Oaks <daniel@danieloaks.net>, Shivaram Lingamneni <slingamn
- Installing - Installing
- Windows - Windows
- macOS / Linux / Raspberry Pi - macOS / Linux / Raspberry Pi
- Productionizing
- Upgrading - Upgrading
- Features - Features
- User Accounts - User Accounts
@ -120,16 +121,47 @@ If you're using Arch Linux, you can also install the [`oragono` package](https:/
1. Create a volume for persistent data: `docker volume create oragono-data` 1. Create a volume for persistent data: `docker volume create oragono-data`
1. Run the container, exposing the default ports: `docker run -d --name oragono -v oragono-data:/ircd-data -p 6667:6667 -p 6697:6697 oragono/oragono:latest` 1. Run the container, exposing the default ports: `docker run -d --name oragono -v oragono-data:/ircd-data -p 6667:6667 -p 6697:6697 oragono/oragono:latest`
For further information and a sample docker-compose file see the separate [Docker documentation](https://github.com/oragono/oragono/blog/master/distrib/docker/README.md). For further information and a sample docker-compose file see the separate [Docker documentation](https://github.com/oragono/oragono/blob/master/distrib/docker/README.md).
## Running oragono as a service on Linux ## Productionizing
The recommended way to operate oragono as a service on Linux is via systemd. This provides a standard interface for starting, stopping, and rehashing (via `systemctl reload`) the service. It also captures oragono's loglines (sent to stderr in the default configuration) and writes them to the system journal. The recommended way to operate oragono as a service on Linux is via systemd. This provides a standard interface for starting, stopping, and rehashing (via `systemctl reload`) the service. It also captures oragono's loglines (sent to stderr in the default configuration) and writes them to the system journal.
If you're using Arch, the abovementioned AUR package bundles a systemd file for starting and stopping the server. If you're rolling your own deployment, here's an [example](https://github.com/darwin-network/slash/blob/master/etc/systemd/system/ircd.service) of a systemd unit file that can be used to run Oragono as an unprivileged role user. The only major distribution that currently packages Oragono is Arch Linux; the aforementioned AUR package includes a systemd unit file. However, it should be fairly straightforward to set up a productionized Oragono on any Linux distribution. Here's a quickstart guide for Debian/Ubuntu:
On a non-systemd system, oragono can be configured to log to a file and used [logrotate(8)](https://linux.die.net/man/8/logrotate), since it will reopen its log files (as well as rehashing the config file) upon receiving a SIGHUP. 1. Create a dedicated, unprivileged role user who will own the oragono process and all its associated files: `adduser --system --group oragono`. This user now has a home directory at `/home/oragono`.
1. Copy the executable binary `oragono`, the config file `ircd.yaml`, the database `ircd.db`, and the self-signed TLS certificate (`tls.crt` and `tls.key`) to `/home/oragono`. Ensure that they are all owned by the new oragono role user: `sudo chown oragono:oragono /home/oragono/*`. Ensure that the configuration file logs to stderr.
1. Install our example [oragono.service](https://github.com/oragono/oragono/blob/master/distrib/systemd/oragono.service) file to `/etc/systemd/system/oragono.service`.
1. Enable and start the new service with the following commands:
1. `systemctl daemon-reload`
1. `systemctl enable oragono.service`
1. `systemctl start oragono.service`
1. Confirm that the service started correctly with `systemctl status oragono.service`
The other major hurdle for productionizing (but one well worth the effort) is obtaining valid TLS certificates for your domain, if you haven't already done so:
1. The simplest way to get valid TLS certificates is from [Let's Encrypt](https://letsencrypt.org/) with [Certbot](https://certbot.eff.org/). The correct procedure will depend on whether you are already running a web server on port 80. If you are, follow the guides on the Certbot website; if you aren't, you can use `certbot certonly --standalone --preferred-challenges http -d example.com` (replace `example.com` with your domain).
1. At this point, you should have certificates available at `/etc/letsencrypt/live/example.com` (replacing `example.com` with your domain). You should serve `fullchain.pem` as the certificate and `privkey.pem` as its private key. However, these files are owned by root and the private key is not readable by the oragono role user, so you won't be able to use them directly in their current locations. You can write a post-renewal hook for certbot to make copies of these certificates accessible to the oragono role user. For example, install the following script as `/etc/letsencrypt/renewal-hooks/post/install-oragono-certificates`, again replacing `example.com` with your domain name, and chmod it 0755:
````bash
#!/bin/bash
set -eu
umask 077
cp /etc/letsencrypt/live/example.com/fullchain.pem /home/oragono/tls.crt
cp /etc/letsencrypt/live/example.com/privkey.pem /home/oragono/tls.key
chown oragono:oragono /home/oragono/tls.*
# rehash oragono, which will reload the certificates:
systemctl reload oragono.service
````
Executing this script manually will install the certificates for the first time and perform a rehash, enabling them.
If you are using Certbot 0.29.0 or higher, you can also change the ownership of the files under `/etc/letsencrypt` so that the oragono user can read them, as described in the [UnrealIRCd documentation](https://www.unrealircd.org/docs/Setting_up_certbot_for_use_with_UnrealIRCd#Tweaking_permissions_on_the_key_file).
On a non-systemd system, oragono can be configured to log to a file and used [logrotate(8)](https://linux.die.net/man/8/logrotate), since it will reopen its log files (as well as rehashing the config file) upon receiving a SIGHUP. To rehash manually outside the context of log rotation, you can use `killall -HUP oragono` or `pkill -HUP oragono`.
## Upgrading to a new version of Oragono ## Upgrading to a new version of Oragono
@ -366,30 +398,9 @@ Similarly, for a public channel (one without `+i`), users can ban nick/account n
# IRC over TLS # IRC over TLS
IRC has traditionally been available over both plaintext (on port 6667) and SSL/TLS (on port 6697). We recommend that you make your server available exclusively via TLS, since exposing plaintext access allows for unauthorized interception or modification of user data or passwords. While the default config file exposes a plaintext public port, it also contains instructions on how to disable it or replace it with a 'dummy' plaintext listener that simply directs users to reconnect using TLS. IRC has traditionally been available over both plaintext (on port 6667) and SSL/TLS (on port 6697). We recommend that you make your server available exclusively via TLS, since exposing plaintext access allows for unauthorized interception or modification of user data or passwords. The default config file no longer exposes a plaintext port, so if you haven't modified your `listeners` section, you're good to go.
## How do I use Let's Encrypt certificates?
[Let's Encrypt](https://letsencrypt.org) is a widely recognized certificate authority that provides free certificates. Here's a quick-start guide for using those certificates with Oragono:
1. Follow this [guidance](https://letsencrypt.org/getting-started/) from Let's Encrypt to create your certificates.
2. You should now have a set of `pem` files, Mainly, we're interested in your `live/` Let's Encrypt directory (e.g. `/etc/letsencrypt/live/<site>/`).
3. Here are how the config file keys map to LE files:
- `cert: tls.crt` is `live/<site>/fullchain.pem`
- ` key: tls.key` is `live/<site>/privkey.pem`
4. You may need to copy the `pem` files to another directory so Oragono can read them, or similarly use a script like [this one](https://github.com/darwin-network/slash/blob/master/etc/bin/install-lecerts) to automagically do something similar.
5. By default, `certbot` will automatically renew your certificates. Oragono will only reread certificates when it is restarted, or during a rehash (e.g., on receiving the `/rehash` command or the `SIGHUP` signal). You can add an executable script to `/etc/letsencrypt/renewal-hooks/post` that can perform the rehash. Here's one example of such a script:
```bash
#!/bin/bash
pkill -HUP oragono
```
The main issues you'll run into are going to be permissions issues. This is because by default, certbot will generate certificates that non-root users can't (and probably shouldn't) read. If you run into trouble, look over the script in step **4** and/or make sure you're copying the files to somewhere else, as well as giving them correct permissions with `chown`, `chgrp` and `chmod`.
On other platforms or with alternative ACME tools, you may need to use other steps or the specific files may be named differently.
For a quickstart guide to obtaining valid TLS certificates from Let's Encrypt, see the "productionizing" section of the manual above.
## How can I "redirect" users from plaintext to TLS? ## How can I "redirect" users from plaintext to TLS?

View File

@ -1174,7 +1174,12 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
var channelsStr string var channelsStr string
keepProtections := false keepProtections := false
am.server.store.Update(func(tx *buntdb.Tx) error { am.server.store.Update(func(tx *buntdb.Tx) error {
// get the unfolded account name; for an active account, this is
// stored under accountNameKey, for an unregistered account under unregisteredKey
accountName, _ = tx.Get(accountNameKey) accountName, _ = tx.Get(accountNameKey)
if accountName == "" {
accountName, _ = tx.Get(unregisteredKey)
}
if erase { if erase {
tx.Delete(unregisteredKey) tx.Delete(unregisteredKey)
} else { } else {

View File

@ -541,10 +541,16 @@ func (channel *Channel) ClientPrefixes(client *Client, isMultiPrefix bool) strin
func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool { func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
channel.stateMutex.RLock() channel.stateMutex.RLock()
founder := channel.registeredFounder
clientModes := channel.members[client] clientModes := channel.members[client]
targetModes := channel.members[target] targetModes := channel.members[target]
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
if founder != "" && founder == client.Account() {
// #950: founder can kick or whatever without actually having the +q mode
return true
}
return channelUserModeHasPrivsOver(clientModes.HighestChannelUserMode(), targetModes.HighestChannelUserMode()) return channelUserModeHasPrivsOver(clientModes.HighestChannelUserMode(), targetModes.HighestChannelUserMode())
} }
@ -1064,6 +1070,25 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0]) message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message) rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
} }
case history.Topic:
if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "TOPIC", chname, item.Message.Message)
} else {
message := fmt.Sprintf(client.t("%[1]s set the channel topic to: %[2]s"), nick, item.Message.Message)
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
}
case history.Mode:
params := make([]string, len(item.Message.Split)+1)
params[0] = chname
for i, pair := range item.Message.Split {
params[i+1] = pair.Message
}
if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "MODE", params...)
} else {
message := fmt.Sprintf(client.t("%[1]s set channel modes: %[2]s"), nick, strings.Join(params[1:], " "))
rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message)
}
} }
} }
} }
@ -1113,22 +1138,30 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe
} }
channel.stateMutex.Lock() channel.stateMutex.Lock()
chname := channel.name
channel.topic = topic channel.topic = topic
channel.topicSetBy = client.nickMaskString channel.topicSetBy = client.nickMaskString
channel.topicSetTime = time.Now().UTC() channel.topicSetTime = time.Now().UTC()
channel.stateMutex.Unlock() channel.stateMutex.Unlock()
prefix := client.NickMaskString() details := client.Details()
message := utils.MakeMessage(topic)
rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "TOPIC", chname, topic)
for _, member := range channel.Members() { for _, member := range channel.Members() {
for _, session := range member.Sessions() { for _, session := range member.Sessions() {
if session == rb.session { if session != rb.session {
rb.Add(nil, prefix, "TOPIC", channel.name, topic) session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "TOPIC", chname, topic)
} else {
session.Send(nil, prefix, "TOPIC", channel.name, topic)
} }
} }
} }
channel.AddHistoryItem(history.Item{
Type: history.Topic,
Nick: details.nickMask,
AccountName: details.accountName,
Message: message,
})
channel.MarkDirty(IncludeTopic) channel.MarkDirty(IncludeTopic)
} }
@ -1251,6 +1284,8 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
} }
} }
// #959: don't save STATUSMSG
if minPrefixMode == modes.Mode(0) {
channel.AddHistoryItem(history.Item{ channel.AddHistoryItem(history.Item{
Type: histType, Type: histType,
Message: message, Message: message,
@ -1259,6 +1294,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
Tags: clientOnlyTags, Tags: clientOnlyTags,
}) })
} }
}
func (channel *Channel) applyModeToMember(client *Client, change modes.ModeChange, rb *ResponseBuffer) (applied bool, result modes.ModeChange) { func (channel *Channel) applyModeToMember(client *Client, change modes.ModeChange, rb *ResponseBuffer) (applied bool, result modes.ModeChange) {
target := channel.server.clients.Get(change.Arg) target := channel.server.clients.Get(change.Arg)

View File

@ -244,7 +244,7 @@ func csAmodeHandler(server *Server, client *Client, command string, params []str
if member.Account() == change.Arg { if member.Account() == change.Arg {
applied, change := channel.applyModeToMember(client, change, rb) applied, change := channel.applyModeToMember(client, change, rb)
if applied { if applied {
announceCmodeChanges(channel, modes.ModeChanges{change}, chanservMask, rb) announceCmodeChanges(channel, modes.ModeChanges{change}, chanservMask, "*", rb)
} }
} }
} }
@ -291,7 +291,7 @@ func csOpHandler(server *Server, client *Client, command string, params []string
}, },
rb) rb)
if applied { if applied {
announceCmodeChanges(channelInfo, modes.ModeChanges{change}, chanservMask, rb) announceCmodeChanges(channelInfo, modes.ModeChanges{change}, chanservMask, "*", rb)
} }
csNotice(rb, fmt.Sprintf(client.t("Successfully op'd in channel %s"), channelName)) csNotice(rb, fmt.Sprintf(client.t("Successfully op'd in channel %s"), channelName))
@ -343,7 +343,7 @@ func csRegisterHandler(server *Server, client *Client, command string, params []
}, },
rb) rb)
if applied { if applied {
announceCmodeChanges(channelInfo, modes.ModeChanges{change}, chanservMask, rb) announceCmodeChanges(channelInfo, modes.ModeChanges{change}, chanservMask, "*", rb)
} }
} }

View File

@ -318,6 +318,10 @@ func (server *Server) RunClient(conn clientConn, proxyLine string) {
session.idletimer.Initialize(session) session.idletimer.Initialize(session)
session.resetFakelag() session.resetFakelag()
for _, defaultMode := range config.Accounts.defaultUserModes {
client.SetMode(defaultMode, true)
}
if conn.Config.TLSConfig != nil { if conn.Config.TLSConfig != nil {
client.SetMode(modes.TLS, true) client.SetMode(modes.TLS, true)
// error is not useful to us here anyways so we can ignore it // error is not useful to us here anyways so we can ignore it
@ -371,6 +375,10 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string,
alwaysOn: true, alwaysOn: true,
} }
for _, defaultMode := range config.Accounts.defaultUserModes {
client.SetMode(defaultMode, true)
}
client.SetMode(modes.TLS, true) client.SetMode(modes.TLS, true)
client.writerSemaphore.Initialize(1) client.writerSemaphore.Initialize(1)
client.history.Initialize(0, 0) client.history.Initialize(0, 0)

View File

@ -205,9 +205,18 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
// the client may just be changing case // the client may just be changing case
if currentClient != nil && currentClient != client && session != nil { if currentClient != nil && currentClient != client && session != nil {
// these conditions forbid reattaching to an existing session: // these conditions forbid reattaching to an existing session:
if registered || !bouncerAllowed || account == "" || account != currentClient.Account() || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) { if registered || !bouncerAllowed || account == "" || account != currentClient.Account() {
return "", errNicknameInUse return "", errNicknameInUse
} }
// check TLS modes
if client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
if useAccountName {
// #955: this is fatal because they can't fix it by trying a different nick
return "", errInsecureReattach
} else {
return "", errNicknameInUse
}
}
reattachSuccessful, numSessions, lastSeen := currentClient.AddSession(session) reattachSuccessful, numSessions, lastSeen := currentClient.AddSession(session)
if !reattachSuccessful { if !reattachSuccessful {
return "", errNicknameInUse return "", errNicknameInUse

View File

@ -263,6 +263,8 @@ type AccountConfig struct {
Exempted []string Exempted []string
exemptedNets []net.IPNet exemptedNets []net.IPNet
} `yaml:"require-sasl"` } `yaml:"require-sasl"`
DefaultUserModes *string `yaml:"default-user-modes"`
defaultUserModes modes.Modes
LDAP ldap.ServerConfig LDAP ldap.ServerConfig
LoginThrottling ThrottleConfig `yaml:"login-throttling"` LoginThrottling ThrottleConfig `yaml:"login-throttling"`
SkipServerPassword bool `yaml:"skip-server-password"` SkipServerPassword bool `yaml:"skip-server-password"`
@ -552,6 +554,7 @@ type Config struct {
OperatorOnly bool `yaml:"operator-only"` OperatorOnly bool `yaml:"operator-only"`
MaxChannelsPerAccount int `yaml:"max-channels-per-account"` MaxChannelsPerAccount int `yaml:"max-channels-per-account"`
} }
ListDelay time.Duration `yaml:"list-delay"`
} }
OperClasses map[string]*OperClassConfig `yaml:"oper-classes"` OperClasses map[string]*OperClassConfig `yaml:"oper-classes"`
@ -984,6 +987,8 @@ func LoadConfig(filename string) (config *Config, err error) {
} }
} }
config.Accounts.defaultUserModes = ParseDefaultUserModes(config.Accounts.DefaultUserModes)
config.Accounts.RequireSasl.exemptedNets, err = utils.ParseNetList(config.Accounts.RequireSasl.Exempted) config.Accounts.RequireSasl.exemptedNets, err = utils.ParseNetList(config.Accounts.RequireSasl.Exempted)
if err != nil { if err != nil {
return nil, fmt.Errorf("Could not parse require-sasl exempted nets: %v", err.Error()) return nil, fmt.Errorf("Could not parse require-sasl exempted nets: %v", err.Error())

View File

@ -42,6 +42,7 @@ var (
errNickMissing = errors.New("nick missing") errNickMissing = errors.New("nick missing")
errNicknameInvalid = errors.New("invalid nickname") errNicknameInvalid = errors.New("invalid nickname")
errNicknameInUse = errors.New("nickname in use") errNicknameInUse = errors.New("nickname in use")
errInsecureReattach = errors.New("insecure reattach")
errNicknameReserved = errors.New("nickname is reserved") errNicknameReserved = errors.New("nickname is reserved")
errNickAccountMismatch = errors.New(`Your nickname must match your account name; try logging out and logging back in with SASL`) errNickAccountMismatch = errors.New(`Your nickname must match your account name; try logging out and logging back in with SASL`)
errNoExistingBan = errors.New("Ban does not exist") errNoExistingBan = errors.New("Ban does not exist")

View File

@ -1408,6 +1408,14 @@ func languageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *
// LIST [<channel>{,<channel>}] [<elistcond>{,<elistcond>}] // LIST [<channel>{,<channel>}] [<elistcond>{,<elistcond>}]
func listHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { func listHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
config := server.Config()
if time.Since(client.ctime) < config.Channels.ListDelay && client.Account() == "" && !client.HasMode(modes.Operator) {
remaining := time.Until(client.ctime.Add(config.Channels.ListDelay))
csNotice(rb, fmt.Sprintf(client.t("This server requires that you wait %v after connecting before you can use /LIST. You have %v left."), config.Channels.ListDelay, remaining))
rb.Add(nil, server.name, RPL_LISTEND, client.Nick(), client.t("End of LIST"))
return false
}
// get channels // get channels
var channels []string var channels []string
for _, param := range msg.Params { for _, param := range msg.Params {
@ -1520,24 +1528,35 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
} }
// process mode changes, include list operations (an empty set of changes does a list) // process mode changes, include list operations (an empty set of changes does a list)
applied := channel.ApplyChannelModeChanges(client, msg.Command == "SAMODE", changes, rb) applied := channel.ApplyChannelModeChanges(client, msg.Command == "SAMODE", changes, rb)
announceCmodeChanges(channel, applied, client.NickMaskString(), rb) details := client.Details()
announceCmodeChanges(channel, applied, details.nickMask, details.accountName, rb)
return false return false
} }
func announceCmodeChanges(channel *Channel, applied modes.ModeChanges, source string, rb *ResponseBuffer) { func announceCmodeChanges(channel *Channel, applied modes.ModeChanges, source, accountName string, rb *ResponseBuffer) {
// send out changes // send out changes
if len(applied) > 0 { if len(applied) > 0 {
//TODO(dan): we should change the name of String and make it return a slice here message := utils.MakeMessage("")
args := append([]string{channel.name}, applied.Strings()...) changeStrings := applied.Strings()
rb.Add(nil, source, "MODE", args...) for _, changeString := range changeStrings {
message.Split = append(message.Split, utils.MessagePair{Message: changeString})
}
args := append([]string{channel.name}, changeStrings...)
rb.AddFromClient(message.Time, message.Msgid, source, accountName, nil, "MODE", args...)
for _, member := range channel.Members() { for _, member := range channel.Members() {
for _, session := range member.Sessions() { for _, session := range member.Sessions() {
if session != rb.session { if session != rb.session {
session.Send(nil, source, "MODE", args...) session.sendFromClientInternal(false, message.Time, message.Msgid, source, accountName, nil, "MODE", args...)
} }
} }
} }
channel.AddHistoryItem(history.Item{
Type: history.Mode,
Nick: source,
AccountName: accountName,
Message: message,
})
} }
} }
@ -2054,7 +2073,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
} }
// must pass at least one check, and all enabled checks // must pass at least one check, and all enabled checks
var checkPassed, checkFailed bool var checkPassed, checkFailed, passwordFailed bool
oper := server.GetOperator(msg.Params[0]) oper := server.GetOperator(msg.Params[0])
if oper != nil { if oper != nil {
if oper.Fingerprint != "" { if oper.Fingerprint != "" {
@ -2065,8 +2084,11 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
} }
} }
if !checkFailed && oper.Pass != nil { if !checkFailed && oper.Pass != nil {
if len(msg.Params) == 1 || bcrypt.CompareHashAndPassword(oper.Pass, []byte(msg.Params[1])) != nil { if len(msg.Params) == 1 {
checkFailed = true checkFailed = true
} else if bcrypt.CompareHashAndPassword(oper.Pass, []byte(msg.Params[1])) != nil {
checkFailed = true
passwordFailed = true
} else { } else {
checkPassed = true checkPassed = true
} }
@ -2075,11 +2097,18 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
if !checkPassed || checkFailed { if !checkPassed || checkFailed {
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect")) rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect"))
// #951: only disconnect them if we actually tried to check a password for them
if passwordFailed {
client.Quit(client.t("Password incorrect"), rb.session) client.Quit(client.t("Password incorrect"), rb.session)
return true return true
} else {
return false
}
} }
if oper != nil {
applyOper(client, oper, rb) applyOper(client, oper, rb)
}
return false return false
} }

View File

@ -22,6 +22,7 @@ const (
Mode Mode
Tagmsg Tagmsg
Nick Nick
Topic
) )
const ( const (

View File

@ -20,6 +20,10 @@ var (
DefaultChannelModes = modes.Modes{ DefaultChannelModes = modes.Modes{
modes.NoOutside, modes.OpOnlyTopic, modes.NoOutside, modes.OpOnlyTopic,
} }
// DefaultUserModes are set on all users when they login.
// this can be overridden in the `accounts` config, with the `default-user-modes` key
DefaultUserModes = modes.Modes{}
) )
// ApplyUserModeChanges applies the given changes, and returns the applied changes. // ApplyUserModeChanges applies the given changes, and returns the applied changes.
@ -102,21 +106,35 @@ func ApplyUserModeChanges(client *Client, changes modes.ModeChanges, force bool,
return applied return applied
} }
// parseDefaultModes uses the provided mode change parser to parse the rawModes.
func parseDefaultModes(rawModes string, parser func(params ...string) (modes.ModeChanges, map[rune]bool)) modes.Modes {
modeChangeStrings := strings.Fields(rawModes)
modeChanges, _ := parser(modeChangeStrings...)
defaultModes := make(modes.Modes, 0)
for _, modeChange := range modeChanges {
if modeChange.Op == modes.Add {
defaultModes = append(defaultModes, modeChange.Mode)
}
}
return defaultModes
}
// ParseDefaultChannelModes parses the `default-modes` line of the config // ParseDefaultChannelModes parses the `default-modes` line of the config
func ParseDefaultChannelModes(rawModes *string) modes.Modes { func ParseDefaultChannelModes(rawModes *string) modes.Modes {
if rawModes == nil { if rawModes == nil {
// not present in config, fall back to compile-time default // not present in config, fall back to compile-time default
return DefaultChannelModes return DefaultChannelModes
} }
modeChangeStrings := strings.Fields(*rawModes) return parseDefaultModes(*rawModes, modes.ParseChannelModeChanges)
modeChanges, _ := modes.ParseChannelModeChanges(modeChangeStrings...)
defaultChannelModes := make(modes.Modes, 0)
for _, modeChange := range modeChanges {
if modeChange.Op == modes.Add {
defaultChannelModes = append(defaultChannelModes, modeChange.Mode)
} }
// ParseDefaultUserModes parses the `default-user-modes` line of the config
func ParseDefaultUserModes(rawModes *string) modes.Modes {
if rawModes == nil {
// not present in config, fall back to compile-time default
return DefaultUserModes
} }
return defaultChannelModes return parseDefaultModes(*rawModes, modes.ParseUserModeChanges)
} }
// ApplyChannelModeChanges applies a given set of mode changes. // ApplyChannelModeChanges applies a given set of mode changes.

View File

@ -35,6 +35,31 @@ func TestParseDefaultChannelModes(t *testing.T) {
} }
} }
func TestParseDefaultUserModes(t *testing.T) {
iR := "+iR"
i := "+i"
empty := ""
rminusi := "+R -i"
var parseTests = []struct {
raw *string
expected modes.Modes
}{
{&iR, modes.Modes{modes.Invisible, modes.RegisteredOnly}},
{&i, modes.Modes{modes.Invisible}},
{&empty, modes.Modes{}},
{&rminusi, modes.Modes{modes.RegisteredOnly}},
{nil, modes.Modes{}},
}
for _, testcase := range parseTests {
result := ParseDefaultUserModes(testcase.raw)
if !reflect.DeepEqual(result, testcase.expected) {
t.Errorf("expected modes %s, got %s", testcase.expected, result)
}
}
}
func TestUmodeGreaterThan(t *testing.T) { func TestUmodeGreaterThan(t *testing.T) {
if !umodeGreaterThan(modes.Halfop, modes.Voice) { if !umodeGreaterThan(modes.Halfop, modes.Voice) {
t.Errorf("expected Halfop > Voice") t.Errorf("expected Halfop > Voice")

View File

@ -25,12 +25,11 @@ var (
restrictedSkeletons = make(map[string]bool) restrictedSkeletons = make(map[string]bool)
) )
// returns whether the change succeeded or failed func performNickChange(server *Server, client *Client, target *Client, session *Session, nickname string, rb *ResponseBuffer) error {
func performNickChange(server *Server, client *Client, target *Client, session *Session, nickname string, rb *ResponseBuffer) bool {
currentNick := client.Nick() currentNick := client.Nick()
details := target.Details() details := target.Details()
if details.nick == nickname { if details.nick == nickname {
return true return nil
} }
hadNick := details.nick != "*" hadNick := details.nick != "*"
origNickMask := details.nickMask origNickMask := details.nickMask
@ -52,7 +51,7 @@ func performNickChange(server *Server, client *Client, target *Client, session *
rb.Add(nil, server.name, ERR_UNKNOWNERROR, currentNick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error())) rb.Add(nil, server.name, ERR_UNKNOWNERROR, currentNick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error()))
} }
if err != nil { if err != nil {
return false return err
} }
message := utils.MakeMessage("") message := utils.MakeMessage("")
@ -88,7 +87,7 @@ func performNickChange(server *Server, client *Client, target *Client, session *
client.server.monitorManager.AlertAbout(target, true) client.server.monitorManager.AlertAbout(target, true)
target.nickTimer.Touch(rb) target.nickTimer.Touch(rb)
} // else: these will be deferred to the end of registration (see #572) } // else: these will be deferred to the end of registration (see #572)
return true return nil
} }
func (server *Server) RandomlyRename(client *Client) { func (server *Server) RandomlyRename(client *Client) {
@ -124,7 +123,7 @@ func fixupNickEqualsAccount(client *Client, rb *ResponseBuffer, config *Config)
if !client.registered { if !client.registered {
return true return true
} }
if !performNickChange(client.server, client, client, rb.session, client.AccountName(), rb) { if performNickChange(client.server, client, client, rb.session, client.AccountName(), rb) != nil {
client.server.accounts.Logout(client) client.server.accounts.Logout(client)
nsNotice(rb, client.t("A client is already using that account; try logging out and logging back in with SASL")) nsNotice(rb, client.t("A client is already using that account; try logging out and logging back in with SASL"))
return false return false

View File

@ -841,6 +841,8 @@ func nsUnregisterHandler(server *Server, client *Client, command string, params
if erase { if erase {
// account may not be in a loadable state, e.g., if it was unregistered // account may not be in a loadable state, e.g., if it was unregistered
accountName = username accountName = username
// make the confirmation code nondeterministic for ERASE
registeredAt = server.ctime
} else { } else {
account, err := server.accounts.LoadAccount(username) account, err := server.accounts.LoadAccount(username)
if err == errAccountDoesNotExist { if err == errAccountDoesNotExist {

View File

@ -438,11 +438,14 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
} }
rb := NewResponseBuffer(session) rb := NewResponseBuffer(session)
nickAssigned := performNickChange(server, c, c, session, c.preregNick, rb) nickError := performNickChange(server, c, c, session, c.preregNick, rb)
rb.Send(true) rb.Send(true)
if !nickAssigned { if nickError == errInsecureReattach {
c.Quit(c.t("You can't mix secure and insecure connections to this account"), nil)
return true
} else if nickError != nil {
c.preregNick = "" c.preregNick = ""
return return false
} }
if session.client != c { if session.client != c {
@ -450,7 +453,7 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
// we'll play the reg burst later, on the new goroutine associated with // we'll play the reg burst later, on the new goroutine associated with
// (thisSession, otherClient). This is to avoid having to transfer state // (thisSession, otherClient). This is to avoid having to transfer state
// like nickname, hostname, etc. to show the correct values in the reg burst. // like nickname, hostname, etc. to show the correct values in the reg burst.
return return false
} }
// check KLINEs // check KLINEs

View File

@ -113,7 +113,7 @@
"Former Core Developers:": "Foști dezvoltatori:", "Former Core Developers:": "Foști dezvoltatori:",
"Founder: %s": "Fondator: %s", "Founder: %s": "Fondator: %s",
"GHOSTed by %s": "%s a folosit GHOST", "GHOSTed by %s": "%s a folosit GHOST",
"Given current server settings, the channel history setting is: %s": "", "Given current server settings, the channel history setting is: %s": "Conform setărilor actuale ale serverului, setarea pentru istoricul de canal este: %s",
"Given current server settings, your client is always-on": "Conform setărilor actuale ale serverului, clientul tău are activă opțiunea de conectare permanentă", "Given current server settings, your client is always-on": "Conform setărilor actuale ale serverului, clientul tău are activă opțiunea de conectare permanentă",
"Given current server settings, your client is not always-on": "Conform setărilor actuale ale serverului, clientul tău nu are activă opțiunea de conectare permanentă", "Given current server settings, your client is not always-on": "Conform setărilor actuale ale serverului, clientul tău nu are activă opțiunea de conectare permanentă",
"Given current server settings, your direct message history setting is: %s": "Conform setărilor actuale ale serverului, setarea aferentă istoricului de mesaje este: %s", "Given current server settings, your direct message history setting is: %s": "Conform setărilor actuale ale serverului, setarea aferentă istoricului de mesaje este: %s",
@ -228,7 +228,7 @@
"Successfully ungrouped nick %s with your account": "Pseudonimul %s a fost degrupat de la contul tău, cu succes", "Successfully ungrouped nick %s with your account": "Pseudonimul %s a fost degrupat de la contul tău, cu succes",
"Successfully unpurged channel %s from the server": "Canalul %s nu mai este purjat din server", "Successfully unpurged channel %s from the server": "Canalul %s nu mai este purjat din server",
"Successfully unregistered account %s": "Contul %s a fost șters cu succes", "Successfully unregistered account %s": "Contul %s a fost șters cu succes",
"That account is set to always-on; try logging out and logging back in with SASL": "", "That account is set to always-on; try logging out and logging back in with SASL": "Acel cont are activă setarea conectare-permanentă; încearcă să te deconectezi și să te reconectezi cu SASL",
"That certificate fingerprint is already associated with another account": "Amprenta de certificat este deja asociată unui alt cont", "That certificate fingerprint is already associated with another account": "Amprenta de certificat este deja asociată unui alt cont",
"That certificate fingerprint was already authorized": "Amprenta certificatului a fost autorizată deja", "That certificate fingerprint was already authorized": "Amprenta certificatului a fost autorizată deja",
"That channel is not registered": "Acel canal nu este înregistrat", "That channel is not registered": "Acel canal nu este înregistrat",

View File

@ -455,6 +455,12 @@ accounts:
offer-list: offer-list:
#- "oragono.test" #- "oragono.test"
# modes that are set by default when a user connects
# if unset, no user modes will be set by default
# +i is invisible (a user's channels are hidden from whois replies)
# see /QUOTE HELP umodes for more user modes
default-user-modes: +i
# support for deferring password checking to an external LDAP server # support for deferring password checking to an external LDAP server
# you should probably ignore this section! consult the grafana docs for details: # you should probably ignore this section! consult the grafana docs for details:
# https://grafana.com/docs/grafana/latest/auth/ldap/ # https://grafana.com/docs/grafana/latest/auth/ldap/
@ -516,6 +522,10 @@ channels:
# how many channels can each account register? # how many channels can each account register?
max-channels-per-account: 15 max-channels-per-account: 15
# as a crude countermeasure against spambots, anonymous connections younger
# than this value will get an empty response to /LIST (a time period of 0 disables)
list-delay: 0s
# operator classes # operator classes
oper-classes: oper-classes:
# local operator # local operator