Compare commits

..

5 commits

36 changed files with 91 additions and 115 deletions

2
.b4-config Normal file
View file

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

View file

@ -4,7 +4,7 @@ packages:
- scdoc
- postgresql
sources:
- https://codeberg.org/emersion/soju.git
- https://git.sr.ht/~emersion/soju
tasks:
- build: |
cd 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 'codeberg.org/emersion/soju/config.DefaultPath=$(config_path)' \
-X 'codeberg.org/emersion/soju/config.DefaultUnixAdminPath=$(admin_socket_path)'
goldflags := -X 'git.sr.ht/~emersion/soju/config.DefaultPath=$(config_path)' \
-X 'git.sr.ht/~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,5 +1,7 @@
# [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
@ -33,8 +35,8 @@ build with PAM authentication support, set `GOFLAGS="-tags=pam"`.
## Contributing
Send patches on [Codeberg] or on [GitHub], report bugs on the [issue tracker].
Discuss in [#soju on Libera Chat][IRC channel].
Send patches on the [mailing list] or on [GitHub], report bugs on the
[issue tracker]. Discuss in [#soju on Libera Chat][IRC channel].
## License
@ -45,7 +47,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
[Codeberg]: https://codeberg.org/emersion/soju
[mailing list]: https://lists.sr.ht/~emersion/soju-dev
[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"
"codeberg.org/emersion/soju/database"
"git.sr.ht/~emersion/soju/database"
)
type Authenticator interface{}

View file

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

View file

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

View file

@ -8,7 +8,7 @@ import (
"github.com/msteinert/pam/v2"
"codeberg.org/emersion/soju/database"
"git.sr.ht/~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"
"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"
"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"
)
// TCP keep-alive interval for downstream TCP connections

View file

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

View file

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

View file

@ -3,6 +3,7 @@ package soju
import (
"context"
"errors"
"fmt"
"io"
"net"
"strings"
@ -157,7 +158,7 @@ func newConn(srv *Server, ic ircConn, options *connOptions) *conn {
break
}
}
if err := c.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
if err := c.conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
c.logger.Printf("failed to close connection: %v", err)
} else {
c.logger.Debugf("connection closed")
@ -184,7 +185,7 @@ func (c *conn) Close() error {
defer c.lock.Unlock()
if c.closed {
return net.ErrClosed
return fmt.Errorf("connection already closed")
}
err := c.conn.Close()
@ -194,10 +195,6 @@ 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,6 +70,12 @@ 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:
@ -114,5 +120,6 @@ 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"
"codeberg.org/emersion/soju/database"
"git.sr.ht/~emersion/soju/database"
)
const usage = `usage: migrate-db <source database> <destination database>

View file

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

View file

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

View file

@ -10,7 +10,7 @@ import (
"strings"
"time"
"codeberg.org/emersion/soju/xirc"
"git.sr.ht/~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"
"codeberg.org/emersion/soju/xirc"
"git.sr.ht/~emersion/soju/xirc"
"github.com/prometheus/client_golang/prometheus"
promcollectors "github.com/prometheus/client_golang/prometheus/collectors"
"gopkg.in/irc.v4"

View file

@ -300,7 +300,7 @@ character.
of an IRC server:
```
openssl s_client -connect irc.example.org:6697 -verify_quiet </dev/null | openssl x509 -fingerprint -sha512 -noout -in /dev/stdin
openssl s_client -connect irc.example.org:6697 </dev/null 2>/dev/null | openssl x509 -fingerprint -sha512 -noout -in /dev/stdin
```
*-nick* <nickname>
@ -560,7 +560,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://soju.im>.
<https://sr.ht/~emersion/soju>.
# 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://soju.im>.
<https://sr.ht/~emersion/soju>.
# SEE ALSO

View file

@ -17,10 +17,10 @@ import (
"github.com/emersion/go-sasl"
"gopkg.in/irc.v4"
"codeberg.org/emersion/soju/auth"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/msgstore"
"codeberg.org/emersion/soju/xirc"
"git.sr.ht/~emersion/soju/auth"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/msgstore"
"git.sr.ht/~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,6 +2142,7 @@ 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 {
@ -2153,10 +2154,8 @@ 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 += "*"
}
@ -2231,23 +2230,6 @@ 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"
"codeberg.org/emersion/soju/auth"
"codeberg.org/emersion/soju/database"
"git.sr.ht/~emersion/soju/auth"
"git.sr.ht/~emersion/soju/database"
)
const maxSize = 50 * 1024 * 1024 // 50 MiB

2
go.mod
View file

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

15
irc.go
View file

@ -9,7 +9,7 @@ import (
"gopkg.in/irc.v4"
"codeberg.org/emersion/soju/xirc"
"git.sr.ht/~emersion/soju/xirc"
)
// TODO: generalize and move helpers to the xirc package
@ -194,19 +194,6 @@ 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"
"codeberg.org/emersion/soju/database"
"git.sr.ht/~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"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/msgstore/znclog"
"codeberg.org/emersion/soju/xirc"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~emersion/soju/msgstore/znclog"
"git.sr.ht/~emersion/soju/xirc"
)
const (

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import (
"gopkg.in/irc.v4"
"codeberg.org/emersion/soju/xirc"
"git.sr.ht/~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"
"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"
"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"
)
var (

View file

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

View file

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

View file

@ -21,8 +21,8 @@ import (
"github.com/emersion/go-sasl"
"gopkg.in/irc.v4"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/xirc"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~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: stripMemberPrefixes(flags, uc),
Flags: flags,
Realname: realname,
})
}
@ -1564,14 +1564,13 @@ 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: stripMemberPrefixes(info.Flags, uc),
Flags: info.Flags,
Account: info.Account,
Realname: info.Realname,
})

View file

@ -14,13 +14,13 @@ import (
"sync/atomic"
"time"
"codeberg.org/emersion/soju/xirc"
"git.sr.ht/~emersion/soju/xirc"
"github.com/SherClockHolmes/webpush-go"
"gopkg.in/irc.v4"
"codeberg.org/emersion/soju/database"
"codeberg.org/emersion/soju/msgstore"
"git.sr.ht/~emersion/soju/database"
"git.sr.ht/~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(encoded); i += MaxSASLLength {
for i := 0; i <= len(resp); i += MaxSASLLength {
j := i + MaxSASLLength
if j > len(encoded) {
j = len(encoded)
if j > len(resp) {
j = len(resp)
}
chunk := encoded[i:j]
if chunk == "" {
chunk = "+"
chunk := resp[i:j]
var respStr = "+"
if len(chunk) != 0 {
respStr = base64.StdEncoding.EncodeToString(chunk)
}
msgs = append(msgs, &irc.Message{
Command: "AUTHENTICATE",
Params: []string{chunk},
Params: []string{respStr},
})
}
return msgs