]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Test] Add SSL server functional tests
authorVsevolod Stakhov <vsevolod@rspamd.com>
Mon, 9 Feb 2026 14:35:01 +0000 (14:35 +0000)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Mon, 9 Feb 2026 14:35:01 +0000 (14:35 +0000)
Add functional tests for HTTPS server support in the
merged test suite. Tests cover controller and normal
worker SSL endpoints plus plain HTTP coexistence.

test/functional/cases/001_merged/440_ssl_server.robot [new file with mode: 0644]
test/functional/cases/001_merged/__init__.robot
test/functional/configs/merged-local.conf
test/functional/lib/rspamd.py
test/functional/lib/vars.py

diff --git a/test/functional/cases/001_merged/440_ssl_server.robot b/test/functional/cases/001_merged/440_ssl_server.robot
new file mode 100644 (file)
index 0000000..2decb0f
--- /dev/null
@@ -0,0 +1,39 @@
+*** Settings ***
+Library         ${RSPAMD_TESTDIR}/lib/rspamd.py
+Resource        ${RSPAMD_TESTDIR}/lib/rspamd.robot
+Variables       ${RSPAMD_TESTDIR}/lib/vars.py
+
+*** Variables ***
+${GTUBE}               ${RSPAMD_TESTDIR}/messages/gtube.eml
+${SETTINGS_NOSYMBOLS}  {symbols_enabled = []}
+
+*** Test Cases ***
+Controller SSL - stat
+  [Documentation]  Fetch /stat over HTTPS from the controller SSL port
+  @{result} =  HTTPS  GET  ${RSPAMD_LOCAL_ADDR}  ${RSPAMD_PORT_CONTROLLER_SSL}  /stat
+  Should Be Equal As Integers  ${result}[0]  200
+
+Controller SSL - errors
+  [Documentation]  Fetch /errors over HTTPS from the controller SSL port
+  @{result} =  HTTPS  GET  ${RSPAMD_LOCAL_ADDR}  ${RSPAMD_PORT_CONTROLLER_SSL}  /errors
+  Should Be Equal As Integers  ${result}[0]  200
+
+Controller plain still works alongside SSL
+  [Documentation]  Plain HTTP controller port must still work when SSL port is also configured
+  @{result} =  HTTP  GET  ${RSPAMD_LOCAL_ADDR}  ${RSPAMD_PORT_CONTROLLER}  /stat
+  Should Be Equal As Integers  ${result}[0]  200
+
+Normal worker SSL - checkv2
+  [Documentation]  Scan a message via /checkv2 over HTTPS on the normal worker SSL port
+  Scan File SSL  ${GTUBE}  Settings=${SETTINGS_NOSYMBOLS}
+  Expect Symbol  GTUBE
+
+Normal worker SSL - checkv3
+  [Documentation]  Scan a message via /checkv3 over HTTPS on the normal worker SSL port
+  Scan File V3 SSL  ${GTUBE}  Settings=${SETTINGS_NOSYMBOLS}
+  Expect Symbol  GTUBE
+
+Normal worker plain still works alongside SSL
+  [Documentation]  Plain HTTP normal port must still work when SSL port is also configured
+  Scan File  ${GTUBE}  Settings=${SETTINGS_NOSYMBOLS}
+  Expect Symbol  GTUBE
index 909d0417a9c19b15d63f8a93e98b2b6e32f3f00e..7f5a9d275952f45fa075f42281f000ede805b6b9 100644 (file)
@@ -19,6 +19,7 @@ Multi Setup
   Run Redis
   Run Dummy Http
   Run Dummy Https
+  Generate SSL Test Cert
   Rspamd Setup
 
 Multi Teardown
@@ -26,4 +27,15 @@ Multi Teardown
   Dummy Http Teardown
   Dummy Https Teardown
   Redis Teardown
+  SSL Cert Teardown
   Try Reap Zombies
+
+Generate SSL Test Cert
+  ${ssl_dir} =  Make Temporary Directory
+  ${cert}  ${key} =  Generate SSL Cert  ${ssl_dir}
+  Set Suite Variable  ${RSPAMD_SSL_CERT}  ${cert}
+  Set Suite Variable  ${RSPAMD_SSL_KEY}  ${key}
+  Set Suite Variable  ${SSL_TMPDIR}  ${ssl_dir}
+
+SSL Cert Teardown
+  Cleanup Temporary Directory  ${SSL_TMPDIR}
index 96659d315f6cd508c49a58a091d7ab6af705e929..fc96199ceced6e6519bb4ca6077f436d45485acd 100644 (file)
@@ -1072,6 +1072,9 @@ composites {
 
 worker "controller" {
     bind_socket = "{= env.LOCAL_ADDR =}:{= env.PORT_CONTROLLER =}";
+    bind_socket = "{= env.LOCAL_ADDR =}:{= env.PORT_CONTROLLER_SSL =} ssl";
+    ssl_cert = "{= env.SSL_CERT =}";
+    ssl_key = "{= env.SSL_KEY =}";
     keypair {
         pubkey = "{= env.KEY_PUB1 =}";
         privkey = "{= env.KEY_PVT1 =}";
@@ -1081,6 +1084,9 @@ worker "controller" {
 worker "normal" {
     count = 1;
     bind_socket = "{= env.LOCAL_ADDR =}:{= env.PORT_NORMAL =}";
+    bind_socket = "{= env.LOCAL_ADDR =}:{= env.PORT_NORMAL_SSL =} ssl";
+    ssl_cert = "{= env.SSL_CERT =}";
+    ssl_key = "{= env.SSL_KEY =}";
     keypair {
         pubkey = "{= env.KEY_PUB1 =}";
         privkey = "{= env.KEY_PVT1 =}";
index 244900f106cd7a50c83c333270597c9ddc399b3d..1302e059ab65d0b64018877f90986f5acb2bab9f 100644 (file)
@@ -35,7 +35,9 @@ import pwd
 import shutil
 import signal
 import socket
+import ssl
 import stat
+import subprocess
 import random
 import re
 import sys
@@ -164,6 +166,55 @@ def HTTP_With_Headers(method, host, port, path, data=None, headers={}):
     return [s, t, h]
 
 
+def HTTPS(method, host, port, path, data=None, headers={}):
+    ctx = ssl.create_default_context()
+    ctx.check_hostname = False
+    ctx.verify_mode = ssl.CERT_NONE
+    c = http.client.HTTPSConnection("%s:%s" % (host, port), context=ctx)
+    c.request(method, path, data, headers)
+    r = c.getresponse()
+    t = r.read()
+    s = r.status
+    c.close()
+    return [s, t]
+
+
+def HTTPS_With_Headers(method, host, port, path, data=None, headers={}):
+    """HTTPS request that returns response headers.
+    Returns [status, body, headers_dict]
+    """
+    ctx = ssl.create_default_context()
+    ctx.check_hostname = False
+    ctx.verify_mode = ssl.CERT_NONE
+    c = http.client.HTTPSConnection("%s:%s" % (host, port), context=ctx)
+    c.request(method, path, data, headers)
+    r = c.getresponse()
+    t = r.read()
+    s = r.status
+    h = dict(r.getheaders())
+    c.close()
+    return [s, t, h]
+
+
+def generate_ssl_cert(tmpdir):
+    """Generate a self-signed EC certificate and key in tmpdir.
+    Returns (cert_path, key_path).
+    """
+    cert_path = os.path.join(tmpdir, "test-cert.pem")
+    key_path = os.path.join(tmpdir, "test-key.pem")
+    subprocess.check_call([
+        "openssl", "req", "-x509", "-newkey", "ec",
+        "-pkeyopt", "ec_paramgen_curve:prime256v1",
+        "-keyout", key_path, "-out", cert_path,
+        "-days", "1", "-nodes",
+        "-subj", "/CN=rspamd-test",
+    ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+    # Make readable by rspamd worker (runs as nobody)
+    os.chmod(cert_path, 0o644)
+    os.chmod(key_path, 0o644)
+    return cert_path, key_path
+
+
 def hard_link(src, dst):
     os.link(src, dst)
 
@@ -385,6 +436,60 @@ def Scan_File_V3_Single_Part(part_name, part_data, content_type_part=None, **hea
     return status
 
 
+def Scan_File_SSL(filename, port=None, **headers):
+    """Like Scan_File but over HTTPS (TLS) to the normal worker SSL port."""
+    addr = BuiltIn().get_variable_value("${RSPAMD_LOCAL_ADDR}")
+    if port is None:
+        port = BuiltIn().get_variable_value("${RSPAMD_PORT_NORMAL_SSL}")
+    headers["Queue-Id"] = BuiltIn().get_variable_value("${TEST_NAME}")
+    ctx = ssl.create_default_context()
+    ctx.check_hostname = False
+    ctx.verify_mode = ssl.CERT_NONE
+    c = http.client.HTTPSConnection("%s:%s" % (addr, port), context=ctx)
+    c.request("POST", "/checkv2", open(filename, "rb"), headers)
+    r = c.getresponse()
+    assert r.status == 200, "Expected HTTP 200 but got %d" % r.status
+    d = json.JSONDecoder(strict=True).decode(r.read().decode('utf-8'))
+    c.close()
+    BuiltIn().set_test_variable("${SCAN_RESULT}", d)
+    return
+
+
+def Scan_File_V3_SSL(filename, port=None, metadata=None, **headers):
+    """Like Scan_File_V3 but over HTTPS (TLS)."""
+    addr = BuiltIn().get_variable_value("${RSPAMD_LOCAL_ADDR}")
+    if port is None:
+        port = BuiltIn().get_variable_value("${RSPAMD_PORT_NORMAL_SSL}")
+
+    meta = metadata if metadata else {}
+    meta_json = json.dumps(meta)
+    message_data = open(filename, "rb").read()
+
+    boundary = "----rspamd-test-%016x" % random.getrandbits(64)
+    body = _build_multipart(boundary, meta_json, message_data)
+
+    headers["Content-Type"] = "multipart/form-data; boundary=" + boundary
+    if "Queue-Id" not in headers:
+        headers["Queue-Id"] = BuiltIn().get_variable_value("${TEST_NAME}")
+
+    ctx = ssl.create_default_context()
+    ctx.check_hostname = False
+    ctx.verify_mode = ssl.CERT_NONE
+    c = http.client.HTTPSConnection("%s:%s" % (addr, port), context=ctx)
+    c.request("POST", "/checkv3", body, headers)
+    r = c.getresponse()
+    assert r.status == 200, "Expected HTTP 200 but got %d" % r.status
+
+    resp_body = r.read()
+    resp_ct = r.getheader("Content-Type", "")
+    result_data = _parse_multipart_response(resp_body, resp_ct)
+
+    d = json.JSONDecoder(strict=True).decode(result_data)
+    c.close()
+    BuiltIn().set_test_variable("${SCAN_RESULT}", d)
+    return
+
+
 def Send_SIGUSR1(pid):
     pid = int(pid)
     os.kill(pid, signal.SIGUSR1)
index bee2e92d409015f2c7aa177bdd90e19b194d7560..4ef29ffc19468c66e47fbf854b62e7465e6a4832 100644 (file)
@@ -32,6 +32,8 @@ RSPAMD_PORT_FUZZY_SLAVE = 56792
 RSPAMD_PORT_NORMAL = 56789
 RSPAMD_PORT_NORMAL_SLAVE = 56794
 RSPAMD_PORT_PROXY = 56795
+RSPAMD_PORT_CONTROLLER_SSL = 56796
+RSPAMD_PORT_NORMAL_SSL = 56797
 RSPAMD_PORT_CLAM = 2100
 RSPAMD_PORT_FPROT = 2101
 RSPAMD_PORT_FPROT2_DUPLICATE = 2102