Compare commits
10 Commits
06fd7a460a
...
c5d07658ab
Author | SHA1 | Date | |
---|---|---|---|
|
c5d07658ab | ||
|
e184c30cef | ||
|
d423a1ca24 | ||
|
e9678cee2f | ||
|
6729297159 | ||
|
3e1efea6e5 | ||
|
2216dd91a0 | ||
|
ec3f0bfd96 | ||
|
a52cd5aa43 | ||
|
5ac4978456 |
2
.b4-config
Normal file
2
.b4-config
Normal file
@ -0,0 +1,2 @@
|
||||
[b4]
|
||||
send-series-to = ~emersion/soju-dev@lists.sr.ht
|
25
conn.go
25
conn.go
@ -143,6 +143,10 @@ func newConn(srv *Server, ic ircConn, options *connOptions) *conn {
|
||||
|
||||
rl := rate.NewLimiter(rate.Every(options.RateLimitDelay), options.RateLimitBurst)
|
||||
for msg := range outgoing {
|
||||
if msg == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if err := rl.Wait(ctx); err != nil {
|
||||
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 {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
35
contrib/certbot.md
Normal file
35
contrib/certbot.md
Normal 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
22
contrib/tlstunnel.md
Normal 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
|
@ -3,6 +3,7 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
@ -26,119 +27,8 @@ CREATE TABLE IF NOT EXISTS "Config" (
|
||||
);
|
||||
`
|
||||
|
||||
const postgresSchema = `
|
||||
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);
|
||||
`
|
||||
//go:embed postgres_schema.sql
|
||||
var postgresSchema string
|
||||
|
||||
var postgresMigrations = []string{
|
||||
"", // migration #0 is reserved for schema initialization
|
||||
|
111
database/postgres_schema.sql
Normal file
111
database/postgres_schema.sql
Normal 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);
|
@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
sqldriver "database/sql/driver"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
@ -57,134 +58,8 @@ func (t sqliteTime) Value() (sqldriver.Value, error) {
|
||||
return t.UTC().Format(sqliteTimeLayout), nil
|
||||
}
|
||||
|
||||
const sqliteSchema = `
|
||||
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;
|
||||
`
|
||||
//go:embed sqlite_schema.sql
|
||||
var sqliteSchema string
|
||||
|
||||
var sqliteMigrations = []string{
|
||||
"", // migration #0 is reserved for schema initialization
|
||||
|
126
database/sqlite_schema.sql
Normal file
126
database/sqlite_schema.sql
Normal 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;
|
@ -622,7 +622,8 @@ func (dc *downstreamConn) handleMessage(ctx context.Context, msg *irc.Message) e
|
||||
|
||||
switch msg.Command {
|
||||
case "QUIT":
|
||||
return dc.Close()
|
||||
dc.conn.Shutdown(ctx)
|
||||
return nil // TODO: stop handling commands
|
||||
default:
|
||||
if dc.registered {
|
||||
return dc.handleMessageRegistered(ctx, msg)
|
||||
@ -1690,12 +1691,15 @@ func (dc *downstreamConn) runUntilRegistered() error {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if err := ctx.Err(); err == context.DeadlineExceeded {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dc.SendMessage(ctx, &irc.Message{
|
||||
Prefix: dc.srv.prefix(),
|
||||
Command: "ERROR",
|
||||
Params: []string{"Connection registration timed out"},
|
||||
})
|
||||
dc.Close()
|
||||
dc.Shutdown(ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
|
2
go.mod
2
go.mod
@ -9,7 +9,7 @@ require (
|
||||
github.com/SherClockHolmes/webpush-go v1.3.0
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
||||
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/pires/go-proxyproto v0.7.0
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
|
4
go.sum
4
go.sum
@ -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/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-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
|
||||
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/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||
github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE=
|
||||
|
12
server.go
12
server.go
@ -339,10 +339,12 @@ func (s *Server) sendWebPush(ctx context.Context, sub *webpush.Subscription, vap
|
||||
},
|
||||
VAPIDPublicKey: s.webPush.VAPIDKeys.Public,
|
||||
VAPIDPrivateKey: s.webPush.VAPIDKeys.Private,
|
||||
Subscriber: "https://soju.im",
|
||||
TTL: 7 * 24 * 60 * 60, // seconds
|
||||
Urgency: urgency,
|
||||
RecordSize: 2048,
|
||||
// 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
|
||||
Urgency: urgency,
|
||||
RecordSize: 2048,
|
||||
}
|
||||
|
||||
if vapidPubKey != options.VAPIDPublicKey {
|
||||
@ -467,7 +469,7 @@ func (s *Server) Handle(ic ircConn) {
|
||||
|
||||
id := atomic.AddUint64(&lastDownstreamID, 1)
|
||||
dc := newDownstreamConn(s, ic, id)
|
||||
defer dc.Close()
|
||||
defer dc.Shutdown(context.TODO())
|
||||
|
||||
if shutdown {
|
||||
dc.SendMessage(context.TODO(), &irc.Message{
|
||||
|
43
upstream.go
43
upstream.go
@ -242,8 +242,6 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er
|
||||
ctx, cancel := context.WithTimeout(ctx, connectTimeout)
|
||||
defer cancel()
|
||||
|
||||
var dialer net.Dialer
|
||||
|
||||
u, err := network.URL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -259,13 +257,6 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er
|
||||
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"}}
|
||||
if network.SASL.Mechanism == "EXTERNAL" {
|
||||
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 {
|
||||
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
|
||||
@ -332,23 +324,17 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er
|
||||
netConn = tls.Client(netConn, tlsConfig)
|
||||
case "irc+insecure":
|
||||
addr := u.Host
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
host = u.Host
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
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)
|
||||
netConn, err = dialer.DialContext(ctx, "tcp", addr)
|
||||
netConn, err = dialTCP(ctx, network.user, addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial %q: %v", addr, err)
|
||||
return nil, err
|
||||
}
|
||||
case "irc+unix", "unix":
|
||||
var dialer net.Dialer
|
||||
logger.Printf("connecting to Unix socket at path %q", u.Path)
|
||||
netConn, err = dialer.DialContext(ctx, "unix", u.Path)
|
||||
if err != nil {
|
||||
@ -386,6 +372,21 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er
|
||||
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)) {
|
||||
uc.network.forEachDownstream(f)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user