mirror of
https://github.com/chatmail/relay.git
synced 2026-05-11 16:34:39 +00:00
Compare commits
5 Commits
j4n/docker
...
add_max_re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
813de8e8d9 | ||
|
|
8db668c037 | ||
|
|
45fafa10a9 | ||
|
|
ee435a7ef7 | ||
|
|
8fafd4e79f |
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@@ -57,8 +57,9 @@ jobs:
|
||||
|
||||
lxc-test:
|
||||
name: LXC deploy and test
|
||||
uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@v0.10.0
|
||||
uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@v0.13.5
|
||||
with:
|
||||
cmlxc_version: v0.13.5
|
||||
cmlxc_commands: |
|
||||
cmlxc init
|
||||
# single cmdeploy relay test
|
||||
|
||||
@@ -70,6 +70,9 @@ class Metadata:
|
||||
# Some tokens have expired, remove them.
|
||||
with self._modify_tokens(addr) as _tokens:
|
||||
pass
|
||||
elif isinstance(tokens, list):
|
||||
with self._modify_tokens(addr) as tokens:
|
||||
token_list = list(tokens.keys())
|
||||
else:
|
||||
token_list = []
|
||||
return token_list
|
||||
@@ -85,29 +88,27 @@ class MetadataDictProxy(DictProxy):
|
||||
|
||||
def handle_lookup(self, parts):
|
||||
# Lpriv/43f5f508a7ea0366dff30200c15250e3/devicetoken\tlkj123poi@c2.testrun.org
|
||||
keyparts = parts[0].split("/", 2)
|
||||
if keyparts[0] == "priv":
|
||||
keyname = keyparts[2]
|
||||
addr = parts[1]
|
||||
if keyname == self.metadata.DEVICETOKEN_KEY:
|
||||
match parts[0].split("/", 2):
|
||||
case ["priv", _, keyname] if keyname == self.metadata.DEVICETOKEN_KEY:
|
||||
addr = parts[1]
|
||||
res = " ".join(self.metadata.get_tokens_for_addr(addr))
|
||||
return f"O{res}\n"
|
||||
elif keyparts[0] == "shared":
|
||||
keyname = keyparts[2]
|
||||
if (
|
||||
keyname == "vendor/vendor.dovecot/pvt/server/vendor/deltachat/irohrelay"
|
||||
and self.iroh_relay
|
||||
):
|
||||
# Handle `GETMETADATA "" /shared/vendor/deltachat/irohrelay`
|
||||
return f"O{self.iroh_relay}\n"
|
||||
elif keyname == "vendor/vendor.dovecot/pvt/server/vendor/deltachat/turn":
|
||||
try:
|
||||
res = turn_credentials()
|
||||
except Exception:
|
||||
logging.exception("failed to get TURN credentials")
|
||||
return "N\n"
|
||||
port = 3478
|
||||
return f"O{self.turn_hostname}:{port}:{res}\n"
|
||||
case ["shared", _, keyname]:
|
||||
prefix = "vendor/vendor.dovecot/pvt/server/vendor/deltachat/"
|
||||
if keyname.startswith(prefix):
|
||||
match keyname[len(prefix) :]:
|
||||
case "irohrelay" if self.iroh_relay:
|
||||
return f"O{self.iroh_relay}\n"
|
||||
case "turn":
|
||||
try:
|
||||
res = turn_credentials()
|
||||
except Exception:
|
||||
logging.exception("failed to get TURN credentials")
|
||||
return "N\n"
|
||||
return f"O{self.turn_hostname}:3478:{res}\n"
|
||||
case "maxsmtprecipients":
|
||||
# postfix default (see "postconf smtpd_recipient_limit")
|
||||
return "O1000\n"
|
||||
|
||||
logging.warning(f"lookup ignored: {parts!r}")
|
||||
return "N\n"
|
||||
@@ -117,12 +118,13 @@ class MetadataDictProxy(DictProxy):
|
||||
# https://github.com/dovecot/core/blob/main/src/lib-storage/mailbox-attribute.h
|
||||
keyname = parts[1].split("/")
|
||||
value = parts[2] if len(parts) > 2 else ""
|
||||
if keyname[0] == "priv" and keyname[2] == self.metadata.DEVICETOKEN_KEY:
|
||||
self.metadata.add_token_to_addr(addr, value)
|
||||
return True
|
||||
elif keyname[0] == "priv" and keyname[2] == "messagenew":
|
||||
self.notifier.new_message_for_addr(addr, self.metadata)
|
||||
return True
|
||||
match keyname:
|
||||
case ["priv", _, key] if key == self.metadata.DEVICETOKEN_KEY:
|
||||
self.metadata.add_token_to_addr(addr, value)
|
||||
return True
|
||||
case ["priv", _, "messagenew"]:
|
||||
self.notifier.new_message_for_addr(addr, self.metadata)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -360,15 +360,39 @@ def test_turn_credentials_success(notifier, metadata, monkeypatch):
|
||||
|
||||
|
||||
def test_iroh_relay(dictproxy):
|
||||
rfile = io.BytesIO(
|
||||
b"\n".join(
|
||||
[
|
||||
b"H",
|
||||
b"Lshared/0123/vendor/vendor.dovecot/pvt/server/vendor/deltachat/irohrelay\tuser@example.org",
|
||||
]
|
||||
)
|
||||
)
|
||||
wfile = io.BytesIO()
|
||||
key = b"Lshared/0123/vendor/vendor.dovecot/pvt/server/vendor/deltachat/irohrelay\tuser@example.org"
|
||||
rfile, wfile = io.BytesIO(b"H\n" + key), io.BytesIO()
|
||||
dictproxy.iroh_relay = "https://example.org/"
|
||||
dictproxy.loop_forever(rfile, wfile)
|
||||
assert wfile.getvalue() == b"Ohttps://example.org/\n"
|
||||
|
||||
|
||||
def test_legacy_token_migration(metadata, testaddr):
|
||||
with metadata.get_metadata_dict(testaddr).modify() as data:
|
||||
data[metadata.DEVICETOKEN_KEY] = ["oldtoken1", "oldtoken2"]
|
||||
|
||||
assert metadata.get_tokens_for_addr(testaddr) == ["oldtoken1", "oldtoken2"]
|
||||
mdict = metadata.get_metadata_dict(testaddr).read()
|
||||
tokens = mdict[metadata.DEVICETOKEN_KEY]
|
||||
assert isinstance(tokens, dict)
|
||||
assert "oldtoken1" in tokens and "oldtoken2" in tokens
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"suffix, expected",
|
||||
[
|
||||
(b"vendor/deltachat/maxsmtprecipients", b"O1000\n"),
|
||||
(b"wrong/prefix/key", b"N\n"),
|
||||
(b"vendor/deltachat/unknown", b"N\n"),
|
||||
],
|
||||
ids=["maxsmtprecipients", "prefix_mismatch", "unknown_name"],
|
||||
)
|
||||
def test_shared_lookup(dictproxy, suffix, expected):
|
||||
key = (
|
||||
b"Lshared/0123/vendor/vendor.dovecot/pvt/server/"
|
||||
+ suffix
|
||||
+ b"\tuser@example.org"
|
||||
)
|
||||
rfile, wfile = io.BytesIO(b"H\n" + key), io.BytesIO()
|
||||
dictproxy.loop_forever(rfile, wfile)
|
||||
assert wfile.getvalue() == expected
|
||||
|
||||
@@ -42,6 +42,9 @@ stream {
|
||||
}
|
||||
|
||||
http {
|
||||
# access_log setting is inherited by all server sections
|
||||
access_log syslog:server=unix:/dev/log,facility=local7;
|
||||
|
||||
{% if config.tls_cert_mode == "self" %}
|
||||
limit_req_zone $binary_remote_addr zone=newaccount:10m rate=2r/s;
|
||||
{% endif %}
|
||||
@@ -69,9 +72,7 @@ http {
|
||||
|
||||
index index.html index.htm;
|
||||
|
||||
server_name {{ config.mail_domain }} www.{{ config.mail_domain }} mta-sts.{{ config.mail_domain }};
|
||||
|
||||
access_log syslog:server=unix:/dev/log,facility=local7;
|
||||
server_name {{ config.mail_domain }} mta-sts.{{ config.mail_domain }};
|
||||
|
||||
location /mxdeliv {
|
||||
proxy_pass http://127.0.0.1:{{ config.filtermail_http_port_incoming }};
|
||||
@@ -143,7 +144,6 @@ http {
|
||||
listen 127.0.0.1:8443 ssl;
|
||||
server_name www.{{ config.mail_domain }};
|
||||
return 301 $scheme://{{ config.mail_domain }}$request_uri;
|
||||
access_log syslog:server=unix:/dev/log,facility=local7;
|
||||
}
|
||||
|
||||
server {
|
||||
|
||||
@@ -64,21 +64,25 @@ def get_dkim_entry(mail_domain, pre_command, dkim_selector):
|
||||
)
|
||||
|
||||
|
||||
def query_dns(typ, domain):
|
||||
# Get autoritative nameserver from the SOA record.
|
||||
soa_answers = [
|
||||
def get_authoritative_ns(domain):
|
||||
ns_replies = [
|
||||
x.split()
|
||||
for x in shell(
|
||||
f"dig -r -q {domain} -t SOA +noall +authority +answer", print=log_progress
|
||||
f"dig -r -q {domain} -t NS +noall +authority +answer", print=log_progress
|
||||
).split("\n")
|
||||
]
|
||||
soa = [a for a in soa_answers if len(a) >= 3 and a[3] == "SOA"]
|
||||
if not soa:
|
||||
filtered_replies = [a for a in ns_replies if len(a) >= 5 and a[3] == "NS"]
|
||||
if not filtered_replies:
|
||||
return
|
||||
ns = soa[0][4]
|
||||
return filtered_replies[0][4]
|
||||
|
||||
|
||||
def query_dns(typ, domain):
|
||||
ns = get_authoritative_ns(domain)
|
||||
|
||||
# Query authoritative nameserver directly to bypass DNS cache.
|
||||
res = shell(f"dig @{ns} -r -q {domain} -t {typ} +short", print=log_progress)
|
||||
direct_ns = f"@{ns}" if ns else ""
|
||||
res = shell(f"dig {direct_ns} -r -q {domain} -t {typ} +short", print=log_progress)
|
||||
return next((line for line in res.split("\n") if not line.startswith(";")), "")
|
||||
|
||||
|
||||
|
||||
@@ -281,3 +281,13 @@ def test_deployed_state(remote):
|
||||
# assert len(git_status) == len(remote_version) # for some reason, we only get 11 lines from remote.iter_output()
|
||||
for i in range(len(remote_version)):
|
||||
assert git_status[i] == remote_version[i], "You have undeployed changes."
|
||||
|
||||
|
||||
def test_nginx_access_log_only_defined_once(sshdomain):
|
||||
sshexec = get_sshexec(sshdomain)
|
||||
conf = sshexec(
|
||||
call=remote.rshell.shell,
|
||||
kwargs=dict(command="nginx -T 2>/dev/null"),
|
||||
)
|
||||
access_logs = [l for l in conf.splitlines() if l.strip().startswith("access_log")]
|
||||
assert len(access_logs) == 1, f"expected 1 access_log, found {len(access_logs)}: {access_logs}"
|
||||
|
||||
@@ -4,6 +4,7 @@ import pytest
|
||||
|
||||
from cmdeploy import remote
|
||||
from cmdeploy.dns import check_full_zone, check_initial_remote_data, parse_zone_records
|
||||
from cmdeploy.remote.rdns import get_authoritative_ns
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -14,11 +15,15 @@ def mockdns_base(monkeypatch):
|
||||
if command.startswith("dig"):
|
||||
if command == "dig":
|
||||
return "."
|
||||
if "SOA" in command:
|
||||
if "with.public.soa" in command and "NS" in command:
|
||||
return "domain.with.public.soa. 2419 IN NS ns1.first-ns.de."
|
||||
if "with.hidden.soa" in command and "NS" in command:
|
||||
return (
|
||||
"delta.chat. 21600 IN SOA ns1.first-ns.de. dns.hetzner.com."
|
||||
" 2025102800 14400 1800 604800 3600"
|
||||
"domain.with.hidden.soa. 2137 IN NS ns1.desec.io.\n"
|
||||
"domain.with.hidden.soa. 2137 IN NS ns2.desec.org."
|
||||
)
|
||||
if "NS" in command:
|
||||
return "delta.chat. 21600 IN NS ns1.first-ns.de."
|
||||
command_chunks = command.split()
|
||||
domain, typ = command_chunks[4], command_chunks[6]
|
||||
try:
|
||||
@@ -125,6 +130,17 @@ class TestPerformInitialChecks:
|
||||
assert not l
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("domain", "ns"),
|
||||
[
|
||||
("domain.with.public.soa", "ns1.first-ns.de."),
|
||||
("domain.with.hidden.soa", "ns1.desec.io."),
|
||||
],
|
||||
)
|
||||
def test_get_authoritative_ns(domain, ns, mockdns):
|
||||
assert get_authoritative_ns(domain) == ns
|
||||
|
||||
|
||||
def test_parse_zone_records():
|
||||
text = """
|
||||
; This is a comment
|
||||
|
||||
Reference in New Issue
Block a user