Compare commits

...

13 commits

Author SHA1 Message Date
Gabriel Simmer faeb46d188
Add Containerfile 2024-07-12 20:28:32 +01:00
Simon Ser bb234c8348 conn: document conn.ReadMessage 2024-07-08 23:16:14 +02:00
Conrad Hoffmann 07502541e4 conn: fix goroutine leak
As is, soju leaks goroutines on client disconnects, because the closure
started as goroutine in newConn never finishes. It gets stuck in the
for loop annotated as "draining the outgoing channel", because the
outgoing channel is in fact never closed.

This commit fixes the issue by calling conn.Close rather than
conn.conn.Close, which closes not only the underlying net.Conn (in
c.conn), but also the channel.
2024-07-08 23:04:52 +02:00
Simon Ser 3667102e72 conn: return net.ErrClosed in conn.Close 2024-07-08 20:37:00 +02:00
Simon Ser d65c1654b8 Stop dereferencing *conn
The *conn pointer returned by newConn is dereferenced in
newDownstreamConn and connectToUpstream (a premature optimization).
As a result, the whole struct is copied and the internal newConn
goroutine works with a different chunk of memory than
downstreamConn and upstreamConn.
2024-07-08 20:35:20 +02:00
Simon Ser a3716dc2d2 downstream: reflect AWAY status in self-WHO reply without upstream 2024-07-08 08:28:50 +02:00
Simon Ser 4682bbef66 downstream: drop TODO about WHOX mask2
This is not part of the IRCv3 spec.
2024-07-08 08:24:34 +02:00
Simon Ser 9a36e6730d contrib/clients: remove note about chat.sr.ht
This document is not a good place for SourceHut-specific
recommendations.
2024-07-04 21:34:23 +02:00
Simon Ser 29bdc1aa45 Migrate to Codeberg 2024-07-04 21:28:11 +02:00
Simon Ser 0ced56a155 doc: use openssl -verify_quiet instead of discarding stderr
stderr prints useful information when openssl fails to connect to
the server. Use -verify_quiet to reduce chatter a bit and still
retain error messages.
2024-07-01 15:13:08 +02:00
Simon Ser d5dd194b01 downstream: fix WHO membership prefix order without server-specific flags
Gregory noticed that my last-minute edit was wrong [1]. Indeed,
when i == -1, that means that Flags only contains 'H'/'G'/'*' and
nothing else. We need to append the membership prefix in that case.

[1]: https://lists.sr.ht/~emersion/soju-dev/%3C20240630213249.13061-2-greg@gpanders.com%3E#%3CD2DP18U4PP40.DBYWGA8WM2KN@gpanders.com%3E

Fixes: ae203388e1 ("Fix channel membership prefixes in cached WHO replies")
Reported-by: Gregory Anders <greg@gpanders.com>
2024-07-01 00:14:06 +02:00
Gregory Anders ae203388e1 Fix channel membership prefixes in cached WHO replies
Channel membership prefixes in WHO replies (RPL_WHOREPLY and
RPL_WHOSPCRPL) were cached in the user's flags, which meant those same
prefixes were returned on future cache hits, even though the flags are
channel specific.

Strip the channel membership prefixes from the user's flags before
adding a user to the cache and add the prefixes back when reading from
the cache (using the membership info from the NAMES reply).
2024-06-30 23:50:50 +02:00
Simon Ser 965ce9cdb9 xirc: fix chunking in GenerateSASL
The SASL response needs to be encoded into base64, then split into
400 byte chunks. We were doing it in reverse order.
2024-06-30 23:42:31 +02:00
37 changed files with 134 additions and 91 deletions

View file

@ -1,2 +0,0 @@
[b4]
send-series-to = ~emersion/soju-dev@lists.sr.ht

View file

@ -4,7 +4,7 @@ packages:
- scdoc
- postgresql
sources:
- https://git.sr.ht/~emersion/soju
- https://codeberg.org/emersion/soju.git
tasks:
- build: |
cd soju

19
Containerfile Normal file
View file

@ -0,0 +1,19 @@
FROM golang:1.21.6-bookworm as build
ENV CGO_ENABLED=0 \
GOOS=linux \
GOFLAGS="-tags=moderncsqlite"
WORKDIR /build
COPY . /build
RUN make soju
FROM scratch
COPY --from=build /build/soju /app/soju
COPY --from=build /build/sojuctl /app/sojuctl
COPY --from=build /build/sojudb /app/sojudb
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
CMD ["/app/soju"]

View file

@ -11,8 +11,8 @@ RUNDIR ?= /run
sharedstatedir := /var/lib
config_path := $(SYSCONFDIR)/soju/config
admin_socket_path := $(RUNDIR)/soju/admin
goldflags := -X 'git.sr.ht/~emersion/soju/config.DefaultPath=$(config_path)' \
-X 'git.sr.ht/~emersion/soju/config.DefaultUnixAdminPath=$(admin_socket_path)'
goldflags := -X 'codeberg.org/emersion/soju/config.DefaultPath=$(config_path)' \
-X 'codeberg.org/emersion/soju/config.DefaultUnixAdminPath=$(admin_socket_path)'
goflags := $(GOFLAGS) -ldflags="$(goldflags)"
commands := soju sojuctl sojudb
man_pages := doc/soju.1 doc/sojuctl.1

View file

@ -1,7 +1,5 @@
# [soju]
[![builds.sr.ht status](https://builds.sr.ht/~emersion/soju/commits/master.svg)](https://builds.sr.ht/~emersion/soju/commits/master?)
soju is a user-friendly IRC bouncer. soju connects to upstream IRC servers on
behalf of the user to provide extra functionality. soju supports many features
such as multiple users, numerous [IRCv3] extensions, chat history playback and
@ -35,8 +33,8 @@ build with PAM authentication support, set `GOFLAGS="-tags=pam"`.
## Contributing
Send patches on the [mailing list] or on [GitHub], report bugs on the
[issue tracker]. Discuss in [#soju on Libera Chat][IRC channel].
Send patches on [Codeberg] or on [GitHub], report bugs on the [issue tracker].
Discuss in [#soju on Libera Chat][IRC channel].
## License
@ -47,7 +45,7 @@ Copyright (C) 2020 The soju Contributors
[soju]: https://soju.im
[Getting started]: doc/getting-started.md
[Man page]: https://soju.im/doc/soju.1.html
[mailing list]: https://lists.sr.ht/~emersion/soju-dev
[Codeberg]: https://codeberg.org/emersion/soju
[GitHub]: https://github.com/emersion/soju
[issue tracker]: https://todo.sr.ht/~emersion/soju
[IRC channel]: ircs://irc.libera.chat/#soju

View file

@ -4,7 +4,7 @@ import (
"context"
"fmt"
"git.sr.ht/~emersion/soju/database"
"codeberg.org/emersion/soju/database"
)
type Authenticator interface{}

View file

@ -4,7 +4,7 @@ import (
"context"
"fmt"
"git.sr.ht/~emersion/soju/database"
"codeberg.org/emersion/soju/database"
)
type internal struct{}

View file

@ -10,7 +10,7 @@ import (
"strings"
"time"
"git.sr.ht/~emersion/soju/database"
"codeberg.org/emersion/soju/database"
)
type oauth2 struct {

View file

@ -8,7 +8,7 @@ import (
"github.com/msteinert/pam/v2"
"git.sr.ht/~emersion/soju/database"
"codeberg.org/emersion/soju/database"
)
type pamAuth struct{}

View file

@ -22,12 +22,12 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"git.sr.ht/~emersion/soju"
"git.sr.ht/~emersion/soju/auth"
"git.sr.ht/~emersion/soju/config"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/fileupload"
"git.sr.ht/~emersion/soju/identd"
"codeberg.org/emersion/soju"
"codeberg.org/emersion/soju/auth"
"codeberg.org/emersion/soju/config"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/fileupload"
"codeberg.org/emersion/soju/identd"
)
// TCP keep-alive interval for downstream TCP connections

View file

@ -11,7 +11,7 @@ import (
"strconv"
"strings"
"git.sr.ht/~emersion/soju/config"
"codeberg.org/emersion/soju/config"
)
const usage = `usage: sojuctl [-config path] <command>

View file

@ -11,8 +11,8 @@ import (
"golang.org/x/crypto/ssh/terminal"
"git.sr.ht/~emersion/soju/config"
"git.sr.ht/~emersion/soju/database"
"codeberg.org/emersion/soju/config"
"codeberg.org/emersion/soju/database"
)
const usage = `usage: sojudb [-config path] <action> [options...]

View file

@ -3,7 +3,6 @@ package soju
import (
"context"
"errors"
"fmt"
"io"
"net"
"strings"
@ -158,7 +157,7 @@ func newConn(srv *Server, ic ircConn, options *connOptions) *conn {
break
}
}
if err := c.conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
if err := c.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
c.logger.Printf("failed to close connection: %v", err)
} else {
c.logger.Debugf("connection closed")
@ -185,7 +184,7 @@ func (c *conn) Close() error {
defer c.lock.Unlock()
if c.closed {
return fmt.Errorf("connection already closed")
return net.ErrClosed
}
err := c.conn.Close()
@ -195,6 +194,10 @@ func (c *conn) Close() error {
return err
}
// Read reads an incoming message. It must be called from a single goroutine
// at a time.
//
// io.EOF is returned when there are no more messages to read.
func (c *conn) ReadMessage() (*irc.Message, error) {
msg, err := c.conn.ReadMessage()
if errors.Is(err, net.ErrClosed) {

View file

@ -70,12 +70,6 @@ outgoing messages:
/set irc_reconnect_rejoin off
/set net_throttle off
Older Hexchat versions (without the [hexchat password length fix]) do not
support long passwords, which include personal access tokens from sourcehut with
limited scope. To work around this issue for sourcehut, [generate a sourcehut
personal access token] without limiting the grant (by not selecting any
permissions).
# [irssi]
To connect irssi to a network, for example Libera Chat:
@ -120,6 +114,5 @@ See `/help cap` for more information.
[read_marker.py]: https://weechat.org/scripts/source/read_marker.py.html/
[Hexchat]: https://hexchat.github.io/
[hexchat password length fix]: https://github.com/hexchat/hexchat/commit/778047bc65e529804c3342ee0f3a8d5d7550fde5
[generate a sourcehut personal access token]: https://meta.sr.ht/oauth2/personal-token
[Emacs]: https://www.gnu.org/software/emacs/
[irssi]: https://irssi.org/

View file

@ -7,7 +7,7 @@ import (
"log"
"strings"
"git.sr.ht/~emersion/soju/database"
"codeberg.org/emersion/soju/database"
)
const usage = `usage: migrate-db <source database> <destination database>

View file

@ -14,9 +14,9 @@ import (
"gopkg.in/irc.v4"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/msgstore"
"git.sr.ht/~emersion/soju/msgstore/znclog"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/msgstore"
"codeberg.org/emersion/soju/msgstore/znclog"
)
const usage = `usage: migrate-logs <source logs> <destination database>

View file

@ -12,8 +12,8 @@ import (
"strings"
"unicode"
"git.sr.ht/~emersion/soju/config"
"git.sr.ht/~emersion/soju/database"
"codeberg.org/emersion/soju/config"
"codeberg.org/emersion/soju/database"
)
const usage = `usage: znc-import [options...] <znc config path>

View file

@ -10,7 +10,7 @@ import (
"strings"
"time"
"git.sr.ht/~emersion/soju/xirc"
"codeberg.org/emersion/soju/xirc"
_ "github.com/lib/pq"
"github.com/prometheus/client_golang/prometheus"
promcollectors "github.com/prometheus/client_golang/prometheus/collectors"

View file

@ -13,7 +13,7 @@ import (
"time"
"unicode"
"git.sr.ht/~emersion/soju/xirc"
"codeberg.org/emersion/soju/xirc"
"github.com/prometheus/client_golang/prometheus"
promcollectors "github.com/prometheus/client_golang/prometheus/collectors"
"gopkg.in/irc.v4"

View file

@ -296,7 +296,7 @@ character.
of an IRC server:
```
openssl s_client -connect irc.example.org:6697 </dev/null 2>/dev/null | openssl x509 -fingerprint -sha512 -noout -in /dev/stdin
openssl s_client -connect irc.example.org:6697 -verify_quiet </dev/null | openssl x509 -fingerprint -sha512 -noout -in /dev/stdin
```
*-nick* <nickname>
@ -556,7 +556,7 @@ character.
Maintained by Simon Ser <contact@emersion.fr>, who is assisted by other
open-source contributors. For more information about soju development, see
<https://sr.ht/~emersion/soju>.
<https://soju.im>.
# SEE ALSO

View file

@ -29,7 +29,7 @@ file. sojuctl needs to be run with write permissions on the soju admin socket.
Maintained by Simon Ser <contact@emersion.fr>, who is assisted by other
open-source contributors. For more information about soju development, see
<https://sr.ht/~emersion/soju>.
<https://soju.im>.
# SEE ALSO

View file

@ -17,10 +17,10 @@ import (
"github.com/emersion/go-sasl"
"gopkg.in/irc.v4"
"git.sr.ht/~emersion/soju/auth"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/msgstore"
"git.sr.ht/~emersion/soju/xirc"
"codeberg.org/emersion/soju/auth"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/msgstore"
"codeberg.org/emersion/soju/xirc"
)
type ircError struct {
@ -328,7 +328,7 @@ func serverSASLMechanisms(srv *Server) []string {
}
type downstreamConn struct {
conn
*conn
id uint64
@ -363,7 +363,7 @@ func newDownstreamConn(srv *Server, ic ircConn, id uint64) *downstreamConn {
options := connOptions{Logger: logger}
cm := xirc.CaseMappingASCII
dc := &downstreamConn{
conn: *newConn(srv, ic, &options),
conn: newConn(srv, ic, &options),
id: id,
nick: "*",
nickCM: "*",
@ -2142,7 +2142,6 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc.
// Clients will use the first mask to match RPL_ENDOFWHO
endOfWhoToken := msg.Params[0]
// TODO: add support for WHOX mask2
mask := msg.Params[0]
var options string
if len(msg.Params) > 1 {
@ -2154,8 +2153,10 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc.
// TODO: support mixed bouncer/upstream WHO queries
maskCM := dc.casemap(mask)
if dc.network == nil && maskCM == dc.nickCM {
// TODO: support AWAY (H/G) in self WHO reply
flags := "H"
if dc.away != nil {
flags = "G"
}
if dc.user.Admin {
flags += "*"
}
@ -2230,6 +2231,23 @@ func (dc *downstreamConn) handleMessageRegistered(ctx context.Context, msg *irc.
}
if uc.isChannel(mask) {
info.Channel = mask
// Set channel membership prefixes from cached NAMES reply
ch := uc.channels.Get(info.Channel)
memberships := ch.Members.Get(info.Nickname)
prefixes := formatMemberPrefix(*memberships, dc)
// Channel membership prefixes are listed after away status ('G'/'H')
// and optional server operator indicator ('*')
i := strings.IndexFunc(info.Flags, func(f rune) bool {
return f != 'G' && f != 'H' && f != '*'
})
if i == -1 {
info.Flags += prefixes
} else {
info.Flags = info.Flags[:i] + prefixes + info.Flags[i:]
}
}
dc.SendMessage(ctx, xirc.GenerateWHOXReply(fields, &info))
}

View file

@ -12,8 +12,8 @@ import (
"strings"
"time"
"git.sr.ht/~emersion/soju/auth"
"git.sr.ht/~emersion/soju/database"
"codeberg.org/emersion/soju/auth"
"codeberg.org/emersion/soju/database"
)
const maxSize = 50 * 1024 * 1024 // 50 MiB

2
go.mod
View file

@ -1,4 +1,4 @@
module git.sr.ht/~emersion/soju
module codeberg.org/emersion/soju
go 1.19

15
irc.go
View file

@ -9,7 +9,7 @@ import (
"gopkg.in/irc.v4"
"git.sr.ht/~emersion/soju/xirc"
"codeberg.org/emersion/soju/xirc"
)
// TODO: generalize and move helpers to the xirc package
@ -194,6 +194,19 @@ func formatMemberPrefix(ms xirc.MembershipSet, dc *downstreamConn) string {
return string(prefixes)
}
// Remove channel membership prefixes from flags
func stripMemberPrefixes(flags string, uc *upstreamConn) string {
return strings.Map(func(r rune) rune {
for _, v := range uc.availableMemberships {
if byte(r) == v.Prefix {
return -1
}
}
return r
}, flags)
}
func parseMessageParams(msg *irc.Message, out ...*string) error {
if len(msg.Params) < len(out) {
return newNeedMoreParamsError(msg.Command)

View file

@ -4,7 +4,7 @@ import (
"context"
"time"
"git.sr.ht/~emersion/soju/database"
"codeberg.org/emersion/soju/database"
"git.sr.ht/~sircmpwn/go-bare"
"gopkg.in/irc.v4"
)

View file

@ -14,9 +14,9 @@ import (
"git.sr.ht/~sircmpwn/go-bare"
"gopkg.in/irc.v4"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/msgstore/znclog"
"git.sr.ht/~emersion/soju/xirc"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/msgstore/znclog"
"codeberg.org/emersion/soju/xirc"
)
const (

View file

@ -8,7 +8,7 @@ import (
"git.sr.ht/~sircmpwn/go-bare"
"gopkg.in/irc.v4"
"git.sr.ht/~emersion/soju/database"
"codeberg.org/emersion/soju/database"
)
const messageRingBufferCap = 4096

View file

@ -10,7 +10,7 @@ import (
"git.sr.ht/~sircmpwn/go-bare"
"gopkg.in/irc.v4"
"git.sr.ht/~emersion/soju/database"
"codeberg.org/emersion/soju/database"
)
type LoadMessageOptions struct {

View file

@ -7,8 +7,8 @@ import (
"gopkg.in/irc.v4"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/xirc"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/xirc"
)
var timestampPrefixLen = len("[01:02:03] ")

View file

@ -7,7 +7,7 @@ import (
"gopkg.in/irc.v4"
"git.sr.ht/~emersion/soju/xirc"
"codeberg.org/emersion/soju/xirc"
)
func MarshalLine(msg *irc.Message, t time.Time) string {

View file

@ -20,11 +20,11 @@ import (
"gopkg.in/irc.v4"
"nhooyr.io/websocket"
"git.sr.ht/~emersion/soju/auth"
"git.sr.ht/~emersion/soju/config"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/fileupload"
"git.sr.ht/~emersion/soju/identd"
"codeberg.org/emersion/soju/auth"
"codeberg.org/emersion/soju/config"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/fileupload"
"codeberg.org/emersion/soju/identd"
)
var (

View file

@ -10,8 +10,8 @@ import (
"gopkg.in/irc.v4"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/xirc"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/xirc"
)
var testServerPrefix = &irc.Prefix{Name: "soju-test-server"}

View file

@ -17,7 +17,7 @@ import (
"gopkg.in/irc.v4"
"git.sr.ht/~emersion/soju/database"
"codeberg.org/emersion/soju/database"
)
const serviceNick = "BouncerServ"

View file

@ -21,8 +21,8 @@ import (
"github.com/emersion/go-sasl"
"gopkg.in/irc.v4"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/xirc"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/xirc"
)
// permanentUpstreamCaps is the static list of upstream capabilities always
@ -191,7 +191,7 @@ type pendingUpstreamCommand struct {
}
type upstreamConn struct {
conn
*conn
network *network
user *user
@ -353,7 +353,7 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er
cm := stdCaseMapping
uc := &upstreamConn{
conn: *newConn(network.user.srv, newNetIRCConn(netConn), &options),
conn: newConn(network.user.srv, newNetIRCConn(netConn), &options),
network: network,
user: network.user,
channels: xirc.NewCaseMappingMap[*upstreamChannel](cm),
@ -1541,7 +1541,7 @@ func (uc *upstreamConn) handleMessage(ctx context.Context, msg *irc.Message) err
Hostname: host,
Server: server,
Nickname: nick,
Flags: flags,
Flags: stripMemberPrefixes(flags, uc),
Realname: realname,
})
}
@ -1564,13 +1564,14 @@ func (uc *upstreamConn) handleMessage(ctx context.Context, msg *irc.Message) err
if err != nil {
return err
}
if uc.shouldCacheUserInfo(info.Nickname) {
uc.cacheUserInfo(info.Nickname, &upstreamUser{
Nickname: info.Nickname,
Username: info.Username,
Hostname: info.Hostname,
Server: info.Server,
Flags: info.Flags,
Flags: stripMemberPrefixes(info.Flags, uc),
Account: info.Account,
Realname: info.Realname,
})

View file

@ -14,13 +14,13 @@ import (
"sync/atomic"
"time"
"git.sr.ht/~emersion/soju/xirc"
"codeberg.org/emersion/soju/xirc"
"github.com/SherClockHolmes/webpush-go"
"gopkg.in/irc.v4"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/msgstore"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/msgstore"
)
type UserUpdateFunc func(record *database.User) error

View file

@ -206,25 +206,25 @@ func GenerateNamesReply(channel string, status ChannelStatus, members []string)
}
func GenerateSASL(resp []byte) []*irc.Message {
encoded := base64.StdEncoding.EncodeToString(resp)
// <= instead of < because we need to send a final empty response if
// the last chunk is exactly 400 bytes long
var msgs []*irc.Message
for i := 0; i <= len(resp); i += MaxSASLLength {
for i := 0; i <= len(encoded); i += MaxSASLLength {
j := i + MaxSASLLength
if j > len(resp) {
j = len(resp)
if j > len(encoded) {
j = len(encoded)
}
chunk := resp[i:j]
var respStr = "+"
if len(chunk) != 0 {
respStr = base64.StdEncoding.EncodeToString(chunk)
chunk := encoded[i:j]
if chunk == "" {
chunk = "+"
}
msgs = append(msgs, &irc.Message{
Command: "AUTHENTICATE",
Params: []string{respStr},
Params: []string{chunk},
})
}
return msgs