Compare commits

..

10 Commits

Author SHA1 Message Date
Henrique Dias
c5d07658ab Upgrade to go-sqlite3 to fix musl build
go-sqlite3 does not build with musl 1.2.4+, which is packaged in some
distros (e.g. Alpine 3.19+). This update fixes it. More info:
https://github.com/mattn/go-sqlite3/pull/1177
2023-12-21 15:36:41 +01:00
Simon Ser
e184c30cef upstream: consoldate TCP dial into function 2023-12-21 13:57:28 +01:00
Simon Ser
d423a1ca24 Add conn.Shutdown
References: https://todo.sr.ht/~emersion/soju/156
2023-12-11 11:50:16 +01:00
Simon Ser
e9678cee2f downstream: use fresh context to send timeout errors
Using an expired context will never actually send the error message
here.
2023-12-11 11:39:57 +01:00
Simon Ser
6729297159 server: fix malformed Web Push subscriber URI
The library prepends "mailto:".
2023-12-09 01:30:48 +01:00
Simon Ser
3e1efea6e5 contrib/certbot: set -eu in renewal hook
That way the script fails if the reload fails.
2023-12-08 17:04:17 +01:00
Simon Ser
2216dd91a0 database: move schema into separate file 2023-12-06 11:39:46 +01:00
Simon Ser
ec3f0bfd96 contrib/tlstunnel: new document 2023-12-01 11:10:38 +01:00
Simon Ser
a52cd5aa43 contrib/certbot: new document 2023-12-01 10:35:01 +01:00
Simon Ser
5ac4978456 Add .b4-config 2023-11-28 14:53:07 +01:00
13 changed files with 365 additions and 272 deletions

2
.b4-config Normal file
View File

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

25
conn.go
View File

@ -143,6 +143,10 @@ func newConn(srv *Server, ic ircConn, options *connOptions) *conn {
rl := rate.NewLimiter(rate.Every(options.RateLimitDelay), options.RateLimitBurst) rl := rate.NewLimiter(rate.Every(options.RateLimitDelay), options.RateLimitBurst)
for msg := range outgoing { for msg := range outgoing {
if msg == nil {
break
}
if err := rl.Wait(ctx); err != nil { if err := rl.Wait(ctx); err != nil {
break break
} }
@ -224,6 +228,27 @@ func (c *conn) SendMessage(ctx context.Context, msg *irc.Message) {
} }
} }
// Shutdown gracefully closes the connection, flushing any pending message.
func (c *conn) Shutdown(ctx context.Context) {
c.lock.Lock()
defer c.lock.Unlock()
if c.closed {
return
}
select {
case c.outgoing <- nil:
// Success
case <-ctx.Done():
c.logger.Printf("failed to shutdown connection: %v", ctx.Err())
// Forcibly close the connection
if err := c.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
c.logger.Printf("failed to close connection: %v", err)
}
}
}
func (c *conn) RemoteAddr() net.Addr { func (c *conn) RemoteAddr() net.Addr {
return c.conn.RemoteAddr() return c.conn.RemoteAddr()
} }

35
contrib/certbot.md Normal file
View File

@ -0,0 +1,35 @@
# Setting up Certbot for soju
If you are using [Certbot] to obtain HTTPS certificates, you can set up soju
like so:
- Obtain the certificate:
certbot certonly -d <domain>
- Allow all local users to access certificates (private keys are still
protected):
chmod 0755 /etc/letsencrypt/{live,archive}
- Allow the soju user to read the private key:
chmod 0640 /etc/letsencrypt/live/<domain>/privkey.pem
chgrp soju /etc/letsencrypt/live/<domain>/privkey.pem
- Set the `tls` directive in the soju configuration file:
tls /etc/letsencrypt/live/<domain>/fullchain.pem /etc/letsencrypt/live/<domain>/privkey.pem
- Configure Certbot to reload soju. Edit
`/etc/letsencrypt/renewal-hooks/post/soju.sh` and add a command to reload
soju, for instance:
#!/bin/sh -eu
systemctl reload soju
Then mark the script as executable:
chmod 755 /etc/letsencrypt/renewal-hooks/post/soju.sh
[Certbot]: https://certbot.eff.org/

22
contrib/tlstunnel.md Normal file
View File

@ -0,0 +1,22 @@
# Setting up tlstunnel with soju
[tlstunnel] can be used in front of soju to take care of TLS.
## tlstunnel configuration
```
frontend {
listen irc.example.org:6697
backend tcp+proxy://localhost:6667
protocol irc
}
```
## soju configuration
```
listen irc+insecure://localhost
accept-proxy-ip localhost
```
[tlstunnel]: https://git.sr.ht/~emersion/tlstunnel

View File

@ -3,6 +3,7 @@ package database
import ( import (
"context" "context"
"database/sql" "database/sql"
_ "embed"
"errors" "errors"
"fmt" "fmt"
"math" "math"
@ -26,119 +27,8 @@ CREATE TABLE IF NOT EXISTS "Config" (
); );
` `
const postgresSchema = ` //go:embed postgres_schema.sql
CREATE TABLE "User" ( var postgresSchema string
id SERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255),
admin BOOLEAN NOT NULL DEFAULT FALSE,
nick VARCHAR(255),
realname VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
downstream_interacted_at TIMESTAMP WITH TIME ZONE
);
CREATE TYPE sasl_mechanism AS ENUM ('PLAIN', 'EXTERNAL');
CREATE TABLE "Network" (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
"user" INTEGER NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
addr VARCHAR(255) NOT NULL,
nick VARCHAR(255),
username VARCHAR(255),
realname VARCHAR(255),
certfp TEXT,
pass VARCHAR(255),
connect_commands VARCHAR(1023),
sasl_mechanism sasl_mechanism,
sasl_plain_username VARCHAR(255),
sasl_plain_password VARCHAR(255),
sasl_external_cert BYTEA,
sasl_external_key BYTEA,
auto_away BOOLEAN NOT NULL DEFAULT TRUE,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE("user", addr, nick),
UNIQUE("user", name)
);
CREATE TABLE "Channel" (
id SERIAL PRIMARY KEY,
network INTEGER NOT NULL REFERENCES "Network"(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
key VARCHAR(255),
detached BOOLEAN NOT NULL DEFAULT FALSE,
detached_internal_msgid VARCHAR(255),
relay_detached INTEGER NOT NULL DEFAULT 0,
reattach_on INTEGER NOT NULL DEFAULT 0,
detach_after INTEGER NOT NULL DEFAULT 0,
detach_on INTEGER NOT NULL DEFAULT 0,
UNIQUE(network, name)
);
CREATE TABLE "DeliveryReceipt" (
id SERIAL PRIMARY KEY,
network INTEGER NOT NULL REFERENCES "Network"(id) ON DELETE CASCADE,
target VARCHAR(255) NOT NULL,
client VARCHAR(255) NOT NULL DEFAULT '',
internal_msgid VARCHAR(255) NOT NULL,
UNIQUE(network, target, client)
);
CREATE TABLE "ReadReceipt" (
id SERIAL PRIMARY KEY,
network INTEGER NOT NULL REFERENCES "Network"(id) ON DELETE CASCADE,
target VARCHAR(255) NOT NULL,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
UNIQUE(network, target)
);
CREATE TABLE "WebPushConfig" (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
vapid_key_public TEXT NOT NULL,
vapid_key_private TEXT NOT NULL,
UNIQUE(vapid_key_public)
);
CREATE TABLE "WebPushSubscription" (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
"user" INTEGER NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
network INTEGER REFERENCES "Network"(id) ON DELETE CASCADE,
endpoint TEXT NOT NULL,
key_vapid TEXT,
key_auth TEXT,
key_p256dh TEXT,
UNIQUE(network, endpoint)
);
CREATE TABLE "MessageTarget" (
id SERIAL PRIMARY KEY,
network INTEGER NOT NULL REFERENCES "Network"(id) ON DELETE CASCADE,
target TEXT NOT NULL,
UNIQUE(network, target)
);
CREATE TEXT SEARCH DICTIONARY search_simple_dictionary (
TEMPLATE = pg_catalog.simple
);
CREATE TEXT SEARCH CONFIGURATION @SCHEMA_PREFIX@search_simple ( COPY = pg_catalog.simple );
ALTER TEXT SEARCH CONFIGURATION @SCHEMA_PREFIX@search_simple ALTER MAPPING FOR asciiword, asciihword, hword_asciipart, hword, hword_part, word WITH @SCHEMA_PREFIX@search_simple_dictionary;
CREATE TABLE "Message" (
id SERIAL PRIMARY KEY,
target INTEGER NOT NULL REFERENCES "MessageTarget"(id) ON DELETE CASCADE,
raw TEXT NOT NULL,
time TIMESTAMP WITH TIME ZONE NOT NULL,
sender TEXT NOT NULL,
text TEXT,
text_search tsvector GENERATED ALWAYS AS (to_tsvector('@SCHEMA_PREFIX@search_simple', text)) STORED
);
CREATE INDEX "MessageIndex" ON "Message" (target, time);
CREATE INDEX "MessageSearchIndex" ON "Message" USING GIN (text_search);
`
var postgresMigrations = []string{ var postgresMigrations = []string{
"", // migration #0 is reserved for schema initialization "", // migration #0 is reserved for schema initialization

View File

@ -0,0 +1,111 @@
CREATE TABLE "User" (
id SERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255),
admin BOOLEAN NOT NULL DEFAULT FALSE,
nick VARCHAR(255),
realname VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
downstream_interacted_at TIMESTAMP WITH TIME ZONE
);
CREATE TYPE sasl_mechanism AS ENUM ('PLAIN', 'EXTERNAL');
CREATE TABLE "Network" (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
"user" INTEGER NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
addr VARCHAR(255) NOT NULL,
nick VARCHAR(255),
username VARCHAR(255),
realname VARCHAR(255),
certfp TEXT,
pass VARCHAR(255),
connect_commands VARCHAR(1023),
sasl_mechanism sasl_mechanism,
sasl_plain_username VARCHAR(255),
sasl_plain_password VARCHAR(255),
sasl_external_cert BYTEA,
sasl_external_key BYTEA,
auto_away BOOLEAN NOT NULL DEFAULT TRUE,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE("user", addr, nick),
UNIQUE("user", name)
);
CREATE TABLE "Channel" (
id SERIAL PRIMARY KEY,
network INTEGER NOT NULL REFERENCES "Network"(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
key VARCHAR(255),
detached BOOLEAN NOT NULL DEFAULT FALSE,
detached_internal_msgid VARCHAR(255),
relay_detached INTEGER NOT NULL DEFAULT 0,
reattach_on INTEGER NOT NULL DEFAULT 0,
detach_after INTEGER NOT NULL DEFAULT 0,
detach_on INTEGER NOT NULL DEFAULT 0,
UNIQUE(network, name)
);
CREATE TABLE "DeliveryReceipt" (
id SERIAL PRIMARY KEY,
network INTEGER NOT NULL REFERENCES "Network"(id) ON DELETE CASCADE,
target VARCHAR(255) NOT NULL,
client VARCHAR(255) NOT NULL DEFAULT '',
internal_msgid VARCHAR(255) NOT NULL,
UNIQUE(network, target, client)
);
CREATE TABLE "ReadReceipt" (
id SERIAL PRIMARY KEY,
network INTEGER NOT NULL REFERENCES "Network"(id) ON DELETE CASCADE,
target VARCHAR(255) NOT NULL,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
UNIQUE(network, target)
);
CREATE TABLE "WebPushConfig" (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
vapid_key_public TEXT NOT NULL,
vapid_key_private TEXT NOT NULL,
UNIQUE(vapid_key_public)
);
CREATE TABLE "WebPushSubscription" (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
"user" INTEGER NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
network INTEGER REFERENCES "Network"(id) ON DELETE CASCADE,
endpoint TEXT NOT NULL,
key_vapid TEXT,
key_auth TEXT,
key_p256dh TEXT,
UNIQUE(network, endpoint)
);
CREATE TABLE "MessageTarget" (
id SERIAL PRIMARY KEY,
network INTEGER NOT NULL REFERENCES "Network"(id) ON DELETE CASCADE,
target TEXT NOT NULL,
UNIQUE(network, target)
);
CREATE TEXT SEARCH DICTIONARY search_simple_dictionary (
TEMPLATE = pg_catalog.simple
);
CREATE TEXT SEARCH CONFIGURATION @SCHEMA_PREFIX@search_simple ( COPY = pg_catalog.simple );
ALTER TEXT SEARCH CONFIGURATION @SCHEMA_PREFIX@search_simple ALTER MAPPING FOR asciiword, asciihword, hword_asciipart, hword, hword_part, word WITH @SCHEMA_PREFIX@search_simple_dictionary;
CREATE TABLE "Message" (
id SERIAL PRIMARY KEY,
target INTEGER NOT NULL REFERENCES "MessageTarget"(id) ON DELETE CASCADE,
raw TEXT NOT NULL,
time TIMESTAMP WITH TIME ZONE NOT NULL,
sender TEXT NOT NULL,
text TEXT,
text_search tsvector GENERATED ALWAYS AS (to_tsvector('@SCHEMA_PREFIX@search_simple', text)) STORED
);
CREATE INDEX "MessageIndex" ON "Message" (target, time);
CREATE INDEX "MessageSearchIndex" ON "Message" USING GIN (text_search);

View File

@ -6,6 +6,7 @@ import (
"context" "context"
"database/sql" "database/sql"
sqldriver "database/sql/driver" sqldriver "database/sql/driver"
_ "embed"
"fmt" "fmt"
"math" "math"
"strings" "strings"
@ -57,134 +58,8 @@ func (t sqliteTime) Value() (sqldriver.Value, error) {
return t.UTC().Format(sqliteTimeLayout), nil return t.UTC().Format(sqliteTimeLayout), nil
} }
const sqliteSchema = ` //go:embed sqlite_schema.sql
CREATE TABLE User ( var sqliteSchema string
id INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password TEXT,
admin INTEGER NOT NULL DEFAULT 0,
realname TEXT,
nick TEXT,
created_at TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
downstream_interacted_at TEXT
);
CREATE TABLE Network (
id INTEGER PRIMARY KEY,
name TEXT,
user INTEGER NOT NULL,
addr TEXT NOT NULL,
nick TEXT,
username TEXT,
realname TEXT,
certfp TEXT,
pass TEXT,
connect_commands TEXT,
sasl_mechanism TEXT,
sasl_plain_username TEXT,
sasl_plain_password TEXT,
sasl_external_cert BLOB,
sasl_external_key BLOB,
auto_away INTEGER NOT NULL DEFAULT 1,
enabled INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY(user) REFERENCES User(id),
UNIQUE(user, addr, nick),
UNIQUE(user, name)
);
CREATE TABLE Channel (
id INTEGER PRIMARY KEY,
network INTEGER NOT NULL,
name TEXT NOT NULL,
key TEXT,
detached INTEGER NOT NULL DEFAULT 0,
detached_internal_msgid TEXT,
relay_detached INTEGER NOT NULL DEFAULT 0,
reattach_on INTEGER NOT NULL DEFAULT 0,
detach_after INTEGER NOT NULL DEFAULT 0,
detach_on INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(network) REFERENCES Network(id),
UNIQUE(network, name)
);
CREATE TABLE DeliveryReceipt (
id INTEGER PRIMARY KEY,
network INTEGER NOT NULL,
target TEXT NOT NULL,
client TEXT,
internal_msgid TEXT NOT NULL,
FOREIGN KEY(network) REFERENCES Network(id),
UNIQUE(network, target, client)
);
CREATE TABLE ReadReceipt (
id INTEGER PRIMARY KEY,
network INTEGER NOT NULL,
target TEXT NOT NULL,
timestamp TEXT NOT NULL,
FOREIGN KEY(network) REFERENCES Network(id),
UNIQUE(network, target)
);
CREATE TABLE WebPushConfig (
id INTEGER PRIMARY KEY,
created_at TEXT NOT NULL,
vapid_key_public TEXT NOT NULL,
vapid_key_private TEXT NOT NULL,
UNIQUE(vapid_key_public)
);
CREATE TABLE WebPushSubscription (
id INTEGER PRIMARY KEY,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
user INTEGER NOT NULL,
network INTEGER,
endpoint TEXT NOT NULL,
key_vapid TEXT,
key_auth TEXT,
key_p256dh TEXT,
FOREIGN KEY(user) REFERENCES User(id),
FOREIGN KEY(network) REFERENCES Network(id),
UNIQUE(network, endpoint)
);
CREATE TABLE Message (
id INTEGER PRIMARY KEY,
target INTEGER NOT NULL,
raw TEXT NOT NULL,
time TEXT NOT NULL,
sender TEXT NOT NULL,
text TEXT,
FOREIGN KEY(target) REFERENCES MessageTarget(id)
);
CREATE INDEX MessageIndex ON Message(target, time);
CREATE TABLE MessageTarget (
id INTEGER PRIMARY KEY,
network INTEGER NOT NULL,
target TEXT NOT NULL,
FOREIGN KEY(network) REFERENCES Network(id),
UNIQUE(network, target)
);
CREATE VIRTUAL TABLE MessageFTS USING fts5 (
text,
content=Message,
content_rowid=id
);
CREATE TRIGGER MessageFTSInsert AFTER INSERT ON Message BEGIN
INSERT INTO MessageFTS(rowid, text) VALUES (new.id, new.text);
END;
CREATE TRIGGER MessageFTSDelete AFTER DELETE ON Message BEGIN
INSERT INTO MessageFTS(MessageFTS, rowid, text) VALUES ('delete', old.id, old.text);
END;
CREATE TRIGGER MessageFTSUpdate AFTER UPDATE ON Message BEGIN
INSERT INTO MessageFTS(MessageFTS, rowid, text) VALUES ('delete', old.id, old.text);
INSERT INTO MessageFTS(rowid, text) VALUES (new.id, new.text);
END;
`
var sqliteMigrations = []string{ var sqliteMigrations = []string{
"", // migration #0 is reserved for schema initialization "", // migration #0 is reserved for schema initialization

126
database/sqlite_schema.sql Normal file
View File

@ -0,0 +1,126 @@
CREATE TABLE User (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password TEXT,
admin INTEGER NOT NULL DEFAULT 0,
realname TEXT,
nick TEXT,
created_at TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
downstream_interacted_at TEXT
);
CREATE TABLE Network (
id INTEGER PRIMARY KEY,
name TEXT,
user INTEGER NOT NULL,
addr TEXT NOT NULL,
nick TEXT,
username TEXT,
realname TEXT,
certfp TEXT,
pass TEXT,
connect_commands TEXT,
sasl_mechanism TEXT,
sasl_plain_username TEXT,
sasl_plain_password TEXT,
sasl_external_cert BLOB,
sasl_external_key BLOB,
auto_away INTEGER NOT NULL DEFAULT 1,
enabled INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY(user) REFERENCES User(id),
UNIQUE(user, addr, nick),
UNIQUE(user, name)
);
CREATE TABLE Channel (
id INTEGER PRIMARY KEY,
network INTEGER NOT NULL,
name TEXT NOT NULL,
key TEXT,
detached INTEGER NOT NULL DEFAULT 0,
detached_internal_msgid TEXT,
relay_detached INTEGER NOT NULL DEFAULT 0,
reattach_on INTEGER NOT NULL DEFAULT 0,
detach_after INTEGER NOT NULL DEFAULT 0,
detach_on INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(network) REFERENCES Network(id),
UNIQUE(network, name)
);
CREATE TABLE DeliveryReceipt (
id INTEGER PRIMARY KEY,
network INTEGER NOT NULL,
target TEXT NOT NULL,
client TEXT,
internal_msgid TEXT NOT NULL,
FOREIGN KEY(network) REFERENCES Network(id),
UNIQUE(network, target, client)
);
CREATE TABLE ReadReceipt (
id INTEGER PRIMARY KEY,
network INTEGER NOT NULL,
target TEXT NOT NULL,
timestamp TEXT NOT NULL,
FOREIGN KEY(network) REFERENCES Network(id),
UNIQUE(network, target)
);
CREATE TABLE WebPushConfig (
id INTEGER PRIMARY KEY,
created_at TEXT NOT NULL,
vapid_key_public TEXT NOT NULL,
vapid_key_private TEXT NOT NULL,
UNIQUE(vapid_key_public)
);
CREATE TABLE WebPushSubscription (
id INTEGER PRIMARY KEY,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
user INTEGER NOT NULL,
network INTEGER,
endpoint TEXT NOT NULL,
key_vapid TEXT,
key_auth TEXT,
key_p256dh TEXT,
FOREIGN KEY(user) REFERENCES User(id),
FOREIGN KEY(network) REFERENCES Network(id),
UNIQUE(network, endpoint)
);
CREATE TABLE Message (
id INTEGER PRIMARY KEY,
target INTEGER NOT NULL,
raw TEXT NOT NULL,
time TEXT NOT NULL,
sender TEXT NOT NULL,
text TEXT,
FOREIGN KEY(target) REFERENCES MessageTarget(id)
);
CREATE INDEX MessageIndex ON Message(target, time);
CREATE TABLE MessageTarget (
id INTEGER PRIMARY KEY,
network INTEGER NOT NULL,
target TEXT NOT NULL,
FOREIGN KEY(network) REFERENCES Network(id),
UNIQUE(network, target)
);
CREATE VIRTUAL TABLE MessageFTS USING fts5 (
text,
content=Message,
content_rowid=id
);
CREATE TRIGGER MessageFTSInsert AFTER INSERT ON Message BEGIN
INSERT INTO MessageFTS(rowid, text) VALUES (new.id, new.text);
END;
CREATE TRIGGER MessageFTSDelete AFTER DELETE ON Message BEGIN
INSERT INTO MessageFTS(MessageFTS, rowid, text) VALUES ('delete', old.id, old.text);
END;
CREATE TRIGGER MessageFTSUpdate AFTER UPDATE ON Message BEGIN
INSERT INTO MessageFTS(MessageFTS, rowid, text) VALUES ('delete', old.id, old.text);
INSERT INTO MessageFTS(rowid, text) VALUES (new.id, new.text);
END;

View File

@ -622,7 +622,8 @@ func (dc *downstreamConn) handleMessage(ctx context.Context, msg *irc.Message) e
switch msg.Command { switch msg.Command {
case "QUIT": case "QUIT":
return dc.Close() dc.conn.Shutdown(ctx)
return nil // TODO: stop handling commands
default: default:
if dc.registered { if dc.registered {
return dc.handleMessageRegistered(ctx, msg) return dc.handleMessageRegistered(ctx, msg)
@ -1690,12 +1691,15 @@ func (dc *downstreamConn) runUntilRegistered() error {
go func() { go func() {
<-ctx.Done() <-ctx.Done()
if err := ctx.Err(); err == context.DeadlineExceeded { if err := ctx.Err(); err == context.DeadlineExceeded {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
dc.SendMessage(ctx, &irc.Message{ dc.SendMessage(ctx, &irc.Message{
Prefix: dc.srv.prefix(), Prefix: dc.srv.prefix(),
Command: "ERROR", Command: "ERROR",
Params: []string{"Connection registration timed out"}, Params: []string{"Connection registration timed out"},
}) })
dc.Close() dc.Shutdown(ctx)
} }
}() }()

2
go.mod
View File

@ -9,7 +9,7 @@ require (
github.com/SherClockHolmes/webpush-go v1.3.0 github.com/SherClockHolmes/webpush-go v1.3.0
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.17 github.com/mattn/go-sqlite3 v1.14.19
github.com/msteinert/pam v1.2.0 github.com/msteinert/pam v1.2.0
github.com/pires/go-proxyproto v0.7.0 github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.17.0 github.com/prometheus/client_golang v1.17.0

4
go.sum
View File

@ -32,8 +32,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE= github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE=

View File

@ -339,7 +339,9 @@ func (s *Server) sendWebPush(ctx context.Context, sub *webpush.Subscription, vap
}, },
VAPIDPublicKey: s.webPush.VAPIDKeys.Public, VAPIDPublicKey: s.webPush.VAPIDKeys.Public,
VAPIDPrivateKey: s.webPush.VAPIDKeys.Private, VAPIDPrivateKey: s.webPush.VAPIDKeys.Private,
Subscriber: "https://soju.im", // TODO: switch back to an HTTP URL once this is merged:
// https://github.com/SherClockHolmes/webpush-go/pull/57
Subscriber: "webpush@soju.im",
TTL: 7 * 24 * 60 * 60, // seconds TTL: 7 * 24 * 60 * 60, // seconds
Urgency: urgency, Urgency: urgency,
RecordSize: 2048, RecordSize: 2048,
@ -467,7 +469,7 @@ func (s *Server) Handle(ic ircConn) {
id := atomic.AddUint64(&lastDownstreamID, 1) id := atomic.AddUint64(&lastDownstreamID, 1)
dc := newDownstreamConn(s, ic, id) dc := newDownstreamConn(s, ic, id)
defer dc.Close() defer dc.Shutdown(context.TODO())
if shutdown { if shutdown {
dc.SendMessage(context.TODO(), &irc.Message{ dc.SendMessage(context.TODO(), &irc.Message{

View File

@ -242,8 +242,6 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er
ctx, cancel := context.WithTimeout(ctx, connectTimeout) ctx, cancel := context.WithTimeout(ctx, connectTimeout)
defer cancel() defer cancel()
var dialer net.Dialer
u, err := network.URL() u, err := network.URL()
if err != nil { if err != nil {
return nil, err return nil, err
@ -259,13 +257,6 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er
addr = u.Host + ":6697" addr = u.Host + ":6697"
} }
dialer.LocalAddr, err = network.user.localTCPAddrForHost(ctx, host)
if err != nil {
return nil, fmt.Errorf("failed to pick local IP for remote host %q: %v", host, err)
}
logger.Printf("connecting to TLS server at address %q", addr)
tlsConfig := &tls.Config{ServerName: host, NextProtos: []string{"irc"}} tlsConfig := &tls.Config{ServerName: host, NextProtos: []string{"irc"}}
if network.SASL.Mechanism == "EXTERNAL" { if network.SASL.Mechanism == "EXTERNAL" {
if network.SASL.External.CertBlob == nil { if network.SASL.External.CertBlob == nil {
@ -321,9 +312,10 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er
} }
} }
netConn, err = dialer.DialContext(ctx, "tcp", addr) logger.Printf("connecting to TLS server at address %q", addr)
netConn, err = dialTCP(ctx, network.user, addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to dial %q: %v", addr, err) return nil, err
} }
// Don't do the TLS handshake immediately, because we need to register // Don't do the TLS handshake immediately, because we need to register
@ -332,23 +324,17 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er
netConn = tls.Client(netConn, tlsConfig) netConn = tls.Client(netConn, tlsConfig)
case "irc+insecure": case "irc+insecure":
addr := u.Host addr := u.Host
host, _, err := net.SplitHostPort(addr) if _, _, err := net.SplitHostPort(addr); err != nil {
if err != nil {
host = u.Host
addr = u.Host + ":6667" addr = u.Host + ":6667"
} }
dialer.LocalAddr, err = network.user.localTCPAddrForHost(ctx, host)
if err != nil {
return nil, fmt.Errorf("failed to pick local IP for remote host %q: %v", host, err)
}
logger.Printf("connecting to plain-text server at address %q", addr) logger.Printf("connecting to plain-text server at address %q", addr)
netConn, err = dialer.DialContext(ctx, "tcp", addr) netConn, err = dialTCP(ctx, network.user, addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to dial %q: %v", addr, err) return nil, err
} }
case "irc+unix", "unix": case "irc+unix", "unix":
var dialer net.Dialer
logger.Printf("connecting to Unix socket at path %q", u.Path) logger.Printf("connecting to Unix socket at path %q", u.Path)
netConn, err = dialer.DialContext(ctx, "unix", u.Path) netConn, err = dialer.DialContext(ctx, "unix", u.Path)
if err != nil { if err != nil {
@ -386,6 +372,21 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er
return uc, nil return uc, nil
} }
func dialTCP(ctx context.Context, user *user, addr string) (net.Conn, error) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
localAddr, err := user.localTCPAddrForHost(ctx, host)
if err != nil {
return nil, fmt.Errorf("failed to pick local IP for remote host %q: %v", host, err)
}
dialer := net.Dialer{LocalAddr: localAddr}
return dialer.DialContext(ctx, "tcp", addr)
}
func (uc *upstreamConn) forEachDownstream(f func(*downstreamConn)) { func (uc *upstreamConn) forEachDownstream(f func(*downstreamConn)) {
uc.network.forEachDownstream(f) uc.network.forEachDownstream(f)
} }