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)
|
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
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 (
|
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
|
||||||
|
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"
|
"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
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 {
|
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
2
go.mod
@ -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
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/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=
|
||||||
|
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,
|
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:
|
||||||
TTL: 7 * 24 * 60 * 60, // seconds
|
// https://github.com/SherClockHolmes/webpush-go/pull/57
|
||||||
Urgency: urgency,
|
Subscriber: "webpush@soju.im",
|
||||||
RecordSize: 2048,
|
TTL: 7 * 24 * 60 * 60, // seconds
|
||||||
|
Urgency: urgency,
|
||||||
|
RecordSize: 2048,
|
||||||
}
|
}
|
||||||
|
|
||||||
if vapidPubKey != options.VAPIDPublicKey {
|
if vapidPubKey != options.VAPIDPublicKey {
|
||||||
@ -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{
|
||||||
|
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)
|
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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user