From: Vsevolod Stakhov Date: Mon, 9 Feb 2026 14:35:01 +0000 (+0000) Subject: [Test] Add SSL server functional tests X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f2127aaad40b9266d51dba76de3385c8a5ff173f;p=thirdparty%2Frspamd.git [Test] Add SSL server functional tests Add functional tests for HTTPS server support in the merged test suite. Tests cover controller and normal worker SSL endpoints plus plain HTTP coexistence. --- 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 index 0000000000..2decb0ff90 --- /dev/null +++ b/test/functional/cases/001_merged/440_ssl_server.robot @@ -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 diff --git a/test/functional/cases/001_merged/__init__.robot b/test/functional/cases/001_merged/__init__.robot index 909d0417a9..7f5a9d2759 100644 --- a/test/functional/cases/001_merged/__init__.robot +++ b/test/functional/cases/001_merged/__init__.robot @@ -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} diff --git a/test/functional/configs/merged-local.conf b/test/functional/configs/merged-local.conf index 96659d315f..fc96199cec 100644 --- a/test/functional/configs/merged-local.conf +++ b/test/functional/configs/merged-local.conf @@ -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 =}"; diff --git a/test/functional/lib/rspamd.py b/test/functional/lib/rspamd.py index 244900f106..1302e059ab 100644 --- a/test/functional/lib/rspamd.py +++ b/test/functional/lib/rspamd.py @@ -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) diff --git a/test/functional/lib/vars.py b/test/functional/lib/vars.py index bee2e92d40..4ef29ffc19 100644 --- a/test/functional/lib/vars.py +++ b/test/functional/lib/vars.py @@ -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