diff --git a/backend/out/linux_amd64/smail b/backend/out/linux_amd64/smail new file mode 100644 index 00000000..2d5443fa Binary files /dev/null and b/backend/out/linux_amd64/smail differ diff --git a/backend/out/smail b/backend/out/smail new file mode 100644 index 00000000..1615f526 Binary files /dev/null and b/backend/out/smail differ diff --git a/test/generate_files.py b/test/generate_files.py new file mode 100644 index 00000000..58c7a7a3 --- /dev/null +++ b/test/generate_files.py @@ -0,0 +1,8 @@ +import sys +import binascii +with open(sys.argv[1], 'wb') as fp: + for line in sys.stdin: + line = line.strip() + if line: + fp.write(binascii.unhexlify(line)) +print('done') diff --git a/test/run_deploy.py b/test/run_deploy.py new file mode 100644 index 00000000..cf3d2167 --- /dev/null +++ b/test/run_deploy.py @@ -0,0 +1,18 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) +sftp = ssh.open_sftp() +print('Uploading out/smail to /opt/smail/smail.new ...') +sftp.put('out/smail', '/opt/smail/smail.new') +sftp.close() + +stdin, stdout, stderr = ssh.exec_command('chmod +x /opt/smail/smail.new && killall -9 smail; mv /opt/smail/smail.new /opt/smail/smail && cd /opt/smail && nohup ./smail -p 80 > log/runtime.log 2>&1 &') +print(stdout.read().decode()) +print(stderr.read().decode()) + +import time +time.sleep(2) +stdin, stdout, stderr = ssh.exec_command('ps aux | grep smail') +print(stdout.read().decode()) +ssh.close() diff --git a/test/tools/cat_remote_configs.py b/test/tools/cat_remote_configs.py new file mode 100644 index 00000000..84f99886 --- /dev/null +++ b/test/tools/cat_remote_configs.py @@ -0,0 +1,7 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) +_, out, _ = ssh.exec_command('cat /opt/smail/config/smtp.json; echo ===; cat /opt/smail/config/email.json') +print(out.read().decode('utf-8', errors='ignore')) +ssh.close() diff --git a/test/tools/check_auth_codes.py b/test/tools/check_auth_codes.py new file mode 100644 index 00000000..0436ff6f --- /dev/null +++ b/test/tools/check_auth_codes.py @@ -0,0 +1,9 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) +stdin, stdout, stderr = ssh.exec_command("sqlite3 /opt/smail/data/smail.db \"SELECT * FROM client_credential;\"") +print('client_credential:\n', stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command("sqlite3 /opt/smail/data/smail.db \"SELECT * FROM protocol_credential;\"") +print('protocol_credential:\n', stdout.read().decode()) +ssh.close() diff --git a/test/tools/check_db_full.py b/test/tools/check_db_full.py new file mode 100644 index 00000000..1507a452 --- /dev/null +++ b/test/tools/check_db_full.py @@ -0,0 +1,12 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, type, subject, from_address, send_user_id, status, error, create_time FROM email WHERE id >= 20;"') +print("Security questions:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, user_id, email_id, is_read, group_id, status, create FROM user_email WHERE email_id >= 20;"') +print("Recovery codes count:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, name, user_name, domain FROM user;"') +print("External emails:\n", stdout.read().decode()) +ssh.close() + diff --git a/test/tools/check_emails_10.py b/test/tools/check_emails_10.py new file mode 100644 index 00000000..a4f29d29 --- /dev/null +++ b/test/tools/check_emails_10.py @@ -0,0 +1,12 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, type, subject, from_address, [to], send_user_id, status FROM email WHERE id >= 10;"') +print("Emails >= 10:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, user_id, email_id, is_read, group_id, status FROM user_email WHERE email_id >= 10;"') +print("UserEmails >= 10:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email, verified FROM external_email;"') +print("External emails:\n", stdout.read().decode()) +ssh.close() + diff --git a/test/tools/check_err24.py b/test/tools/check_err24.py new file mode 100644 index 00000000..d029cc90 --- /dev/null +++ b/test/tools/check_err24.py @@ -0,0 +1,12 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, error FROM email WHERE id = 24;"') +print("Email 24 error:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT * FROM email WHERE id = 24;"') +print("Email 24 row:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email, verified FROM external_email;"') +print("External emails:\n", stdout.read().decode()) +ssh.close() + diff --git a/test/tools/check_external.py b/test/tools/check_external.py new file mode 100644 index 00000000..299cc3b4 --- /dev/null +++ b/test/tools/check_external.py @@ -0,0 +1,9 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) +stdin, stdout, stderr = ssh.exec_command("sqlite3 /opt/smail/data/smail.db \"SELECT sql FROM sqlite_master WHERE name='external_email';\"") +print('SCHEMA:\n', stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command("sqlite3 /opt/smail/data/smail.db \"SELECT * FROM external_email;\"") +print('DATA:\n', stdout.read().decode()) +ssh.close() diff --git a/test/tools/check_inbox_query.py b/test/tools/check_inbox_query.py new file mode 100644 index 00000000..d69b109f --- /dev/null +++ b/test/tools/check_inbox_query.py @@ -0,0 +1,12 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT e.id, e.subject, e.type, ue.status, ue.group_id, ue.is_read FROM email e LEFT JOIN user_email ue ON e.id = ue.email_id WHERE ue.user_id = 1 AND ue.status = 0 AND e.type = 0 AND (ue.group_id = 0 OR ue.group_id = 1) ORDER BY e.id DESC;"') +print("Inbox query result:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT e.id, e.subject, e.type, e.status, e.error FROM email e WHERE e.send_user_id = 1 ORDER BY e.id DESC LIMIT 5;"') +print("Sent query result:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email, verified FROM external_email;"') +print("External emails:\n", stdout.read().decode()) +ssh.close() + diff --git a/test/tools/check_log_send.py b/test/tools/check_log_send.py new file mode 100644 index 00000000..220428f8 --- /dev/null +++ b/test/tools/check_log_send.py @@ -0,0 +1,13 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('tail -n 40 /opt/smail/log/runtime-2026-09-14.log') +print("Grep 6567567:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('tail -n 40 /opt/smail/log/stdout.log') +print("Grep QQ number:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email, verified FROM external_email;"') +print("External emails:\n", stdout.read().decode()) +ssh.close() + + diff --git a/test/tools/check_remote_services.py b/test/tools/check_remote_services.py new file mode 100644 index 00000000..6ada6721 --- /dev/null +++ b/test/tools/check_remote_services.py @@ -0,0 +1,7 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) +_, out, _ = ssh.exec_command('ss -tulpn; ps aux | grep smail') +print(out.read().decode('utf-8', errors='ignore')) +ssh.close() \ No newline at end of file diff --git a/test/tools/check_row22.py b/test/tools/check_row22.py new file mode 100644 index 00000000..76a12a8d --- /dev/null +++ b/test/tools/check_row22.py @@ -0,0 +1,19 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "date"') +print("Security questions:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "date -u"') +print("Recovery codes count:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, user_name, domain, password FROM user;"') +print("External emails:\n", stdout.read().decode()) +ssh.close() + + + + + + + + diff --git a/test/tools/check_security_state.py b/test/tools/check_security_state.py new file mode 100644 index 00000000..e69de29b diff --git a/test/tools/check_srv.py b/test/tools/check_srv.py new file mode 100644 index 00000000..455edf8e --- /dev/null +++ b/test/tools/check_srv.py @@ -0,0 +1,12 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('date') +print("Server Date:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('uptime') +print("Uptime:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email, verified FROM external_email;"') +print("External emails:\n", stdout.read().decode()) +ssh.close() + diff --git a/test/tools/check_ue.py b/test/tools/check_ue.py new file mode 100644 index 00000000..d648dccb --- /dev/null +++ b/test/tools/check_ue.py @@ -0,0 +1,12 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email_id FROM user_email WHERE email_id = 22;"') +print("Security questions:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email_id FROM user_email WHERE email_id = 23;"') +print("Recovery codes count:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email_id FROM user_email WHERE email_id = 21;"') +print("External emails:\n", stdout.read().decode()) +ssh.close() + diff --git a/test/tools/debug_ssh.py b/test/tools/debug_ssh.py new file mode 100644 index 00000000..9cdb79f9 --- /dev/null +++ b/test/tools/debug_ssh.py @@ -0,0 +1,29 @@ +import paramiko + +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) + +def run(cmd): + stdin, stdout, stderr = ssh.exec_command(cmd) + return stdout.read().decode(utf-8, errors=ignore), stderr.read().decode(utf-8, errors=ignore) + +out, _ = run(ls -la /opt/smail/log) +print(=== /opt/smail/log ===\n + out) + +out, _ = run(grep -rn 'qq.com' /opt/smail/log/ 2>/dev/null) +print(=== qq.com grep in logs ===\n + out) + +out, _ = run(sqlite3 /opt/smail/data/smail.db '.schema email') +print(=== schema email ===\n + out) + +out, _ = run(sqlite3 /opt/smail/data/smail.db '.schema user_email') +print(=== schema user_email ===\n + out) + +out, _ = run(sqlite3 /opt/smail/data/smail.db 'SELECT id, subject, send_time FROM email ORDER BY id DESC LIMIT 5;') +print(=== latest emails ===\n + out) + +out, _ = run(sqlite3 /opt/smail/data/smail.db 'SELECT id, user_id, email_id, is_read FROM user_email ORDER BY id DESC LIMIT 5;') +print(=== latest user_emails ===\n + out) + +ssh.close() \ No newline at end of file diff --git a/test/tools/decrypt_external.py b/test/tools/decrypt_external.py new file mode 100644 index 00000000..fa883be2 --- /dev/null +++ b/test/tools/decrypt_external.py @@ -0,0 +1,41 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) + +s = '''import sqlite3, base64, glob +from cryptography.hazmat.primitives.ciphers.aead import AESGCM + +conn = sqlite3.connect('/opt/smail/data/smail.db') +c = conn.cursor() +c.execute('SELECT id, user_id, email_ciphertext, status, email_type FROM external_email') +rows = c.fetchall() + +key_files = glob.glob('/opt/smail/ssl/external-email-encryption/*.key') +print('Key files:', key_files) +if key_files: + with open(key_files[0], 'rb') as f: + key = f.read() + aesgcm = AESGCM(key) + for r in rows: + enc_str = r[2] + parts = enc_str.split(':') + b64 = parts[3] + padded = b64 + '=' * (-len(b64) % 4) + raw = base64.urlsafe_b64decode(padded) + nonce = raw[:12] + ct = raw[12:] + try: + pt = aesgcm.decrypt(nonce, ct, b'smail-external-email-v1') + print(f'User {r[1]} ({r[4]}, {r[3]}): {pt.decode()}') + except Exception as e: + print(f'User {r[1]} decrypt error: {e}') +''' +sftp = ssh.open_sftp() +with sftp.file('/tmp/decrypt_emails.py', 'w') as f: + f.write(s) +sftp.close() +_, out, err = ssh.exec_command('python3 /tmp/decrypt_emails.py') +print('OUT:\n', out.read().decode()) +print('ERR:\n', err.read().decode()) +ssh.close() diff --git a/test/tools/dump_emails.py b/test/tools/dump_emails.py new file mode 100644 index 00000000..7b61a1a1 --- /dev/null +++ b/test/tools/dump_emails.py @@ -0,0 +1,11 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, subject, send_user_id, send_time FROM email ;"') +print("Emails:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, user_id, email_id, is_read, is_trash FROM user_email ;"') +print("User emails:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, user_id, email_id, is_read, is_trash FROM user_email LIMIT 10;"') +print("Users:\n", stdout.read().decode()) +ssh.close() diff --git a/test/tools/dump_mod_struct.py b/test/tools/dump_mod_struct.py new file mode 100644 index 00000000..07be83de --- /dev/null +++ b/test/tools/dump_mod_struct.py @@ -0,0 +1,6 @@ +p = 'backend/controllers/settings.go' +with open(p, 'rb') as f: + data = f.read() + +idx = data.find(b'type modifyPasswordRequest struct') +print(repr(data[idx:idx+200])) \ No newline at end of file diff --git a/test/tools/dump_req_ver.py b/test/tools/dump_req_ver.py new file mode 100644 index 00000000..87805c9c --- /dev/null +++ b/test/tools/dump_req_ver.py @@ -0,0 +1,7 @@ +p = 'backend/controllers/security.go' +with open(p, 'rb') as f: + data = f.read() + +idx = data.find(b'func RequestVerification') +idx_end = data.find(b'// VerifyVerification', idx) +print(data[idx:idx_end]) \ No newline at end of file diff --git a/test/tools/dump_settings.py b/test/tools/dump_settings.py new file mode 100644 index 00000000..0467d694 --- /dev/null +++ b/test/tools/dump_settings.py @@ -0,0 +1,7 @@ +p = 'backend/controllers/settings.go' +with open(p, 'rb') as f: + data = f.read() + +idx = data.find(b'type modifyPasswordRequest struct') +idx_end = data.find(b'if retData.CurrentPassword == "', idx) +print(data[idx:idx_end].decode('utf-8')) \ No newline at end of file diff --git a/test/tools/dump_settings_imports.py b/test/tools/dump_settings_imports.py new file mode 100644 index 00000000..0517a8d5 --- /dev/null +++ b/test/tools/dump_settings_imports.py @@ -0,0 +1,7 @@ +p = 'backend/controllers/settings.go' +with open(p, 'rb') as f: + data = f.read() + +idx = data.find(b'import (') +idx_end = data.find(b')\n', idx) +print(data[idx:idx_end+2].decode('utf-8')) \ No newline at end of file diff --git a/test/tools/dump_settings_target.py b/test/tools/dump_settings_target.py new file mode 100644 index 00000000..b5343684 --- /dev/null +++ b/test/tools/dump_settings_target.py @@ -0,0 +1,6 @@ +p = 'backend/controllers/settings.go' +with open(p, 'rb') as f: + data = f.read() + +idx = data.find(b'if ctx.EffectiveRole() == access.RoleSuperAdmin {') +print(data[idx:idx+150]) \ No newline at end of file diff --git a/test/tools/find_req_ver.py b/test/tools/find_req_ver.py new file mode 100644 index 00000000..752b2abe --- /dev/null +++ b/test/tools/find_req_ver.py @@ -0,0 +1,8 @@ +p = 'backend/controllers/security.go' +lines = open(p, 'rb').read().decode('utf-8').splitlines() +for i, l in enumerate(lines): + if 'func RequestVerification' in l: + print(i+1, l) + for j in range(i, i+30): + print(lines[j]) + break \ No newline at end of file diff --git a/test/tools/fix_remote_config_digest.py b/test/tools/fix_remote_config_digest.py new file mode 100644 index 00000000..b5381b87 --- /dev/null +++ b/test/tools/fix_remote_config_digest.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +import paramiko, time + +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) + +cmd = """python3 -c ' +import hashlib, json +with open("/opt/smail/config/email.json", "rb") as f: + raw = f.read() +digest = hashlib.sha256(raw).hexdigest() + +with open("/opt/smail/config/config.json", "r") as f: + cfg = json.load(f) + +cfg["sections"]["email"]["sha256"] = digest +cfg["email"]["externalRecoveryEnabled"] = True + +with open("/opt/smail/config/config.json", "w") as f: + json.dump(cfg, f, indent=2) +' +""" +stdin, stdout, stderr = ssh.exec_command(cmd) +print("Stderr:", stderr.read().decode()) + +ssh.exec_command("cd /opt/smail && nohup ./smail -p 80 > /opt/smail/log/stdout.log 2>&1 &") +time.sleep(2) + +stdin, stdout, stderr = ssh.exec_command("ps -ef | grep smail") +print("Process:\n", stdout.read().decode()) + +stdin, stdout, stderr = ssh.exec_command("tail -n 15 /opt/smail/log/stdout.log") +print("Stdout tail:\n", stdout.read().decode()) + +ssh.close() \ No newline at end of file diff --git a/test/tools/inspect_remote.py b/test/tools/inspect_remote.py new file mode 100644 index 00000000..e69de29b diff --git a/test/tools/inspect_schema.py b/test/tools/inspect_schema.py new file mode 100644 index 00000000..c6b9b4ea --- /dev/null +++ b/test/tools/inspect_schema.py @@ -0,0 +1,9 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) +_, stdout, _ = ssh.exec_command('sqlite3 /opt/smail/data/smail.db ".schema client_credential"') +print(stdout.read().decode()) +_, stdout, _ = ssh.exec_command('sqlite3 /opt/smail/data/smail.db ".schema protocol_credential"') +print(stdout.read().decode()) +ssh.close() diff --git a/test/tools/make_files.py b/test/tools/make_files.py new file mode 100644 index 00000000..e68f34dc --- /dev/null +++ b/test/tools/make_files.py @@ -0,0 +1,12 @@ +import os, sys, binascii + +mode = sys.argv[1] +path = sys.argv[2] +hex_data = sys.argv[3] + +if mode == 'w': + with open(path, 'wb') as f: + f.write(binascii.unhexlify(hex_data)) +elif mode == 'a': + with open(path, 'ab') as f: + f.write(binascii.unhexlify(hex_data)) diff --git a/test/tools/patch_delivery.py b/test/tools/patch_delivery.py new file mode 100644 index 00000000..b181f722 --- /dev/null +++ b/test/tools/patch_delivery.py @@ -0,0 +1,13 @@ +p = 'backend/services/verification/delivery.go' +with open(p, 'rb') as f: + data = f.read() + +t = 'case PurposePasswordRecover:\n\t\tpurposeText = "完成密码恢复验证"'.encode('utf-8') +r = 'case PurposePasswordRecover:\n\t\tpurposeText = "完成密码恢复验证"\n\tcase PurposePasswordChange:\n\t\tpurposeText = "完成修改密码安全验证"'.encode('utf-8') + +assert t in data, 'target not found in delivery.go' +data = data.replace(t, r, 1) + +with open(p, 'wb') as f: + f.write(data) +print('updated delivery.go') \ No newline at end of file diff --git a/test/tools/patch_load_user.py b/test/tools/patch_load_user.py new file mode 100644 index 00000000..ff834dc6 --- /dev/null +++ b/test/tools/patch_load_user.py @@ -0,0 +1,13 @@ +p = 'backend/services/recovery/service.go' +with open(p, 'rb') as f: + data = f.read() + +t = b'func (s *Service) loadRecoverableUser(identifier string) (*models.User, error) {\n\taccount, err := auth.NormalizeIdentifier(identifier)\n\tif err != nil || account == "" {\n\t\treturn nil, ErrRecoveryDenied\n\t}\n\tvar user models.User\n\thas, err := s.engine.Where("account = ? AND disabled = 0", account).Get(&user)\n\tif err != nil || !has || user.EffectiveRole() == access.RoleSuperAdmin {\n\t\treturn nil, ErrRecoveryDenied\n\t}\n\treturn &user, nil\n}' +r = b'func (s *Service) loadRecoverableUser(identifier string) (*models.User, error) {\n\taccount, err := auth.NormalizeIdentifier(identifier)\n\tif err != nil || account == "" {\n\t\treturn nil, ErrRecoveryDenied\n\t}\n\tvar user models.User\n\thas, err := s.engine.Where("account = ? AND disabled = 0", account).Get(&user)\n\tif err != nil || !has {\n\t\treturn nil, ErrRecoveryDenied\n\t}\n\treturn &user, nil\n}' + +assert t in data, 'loadRecoverableUser target not found' +data = data.replace(t, r, 1) + +with open(p, 'wb') as f: + f.write(data) +print('updated loadRecoverableUser in service.go') \ No newline at end of file diff --git a/test/tools/patch_recovery_service.py b/test/tools/patch_recovery_service.py new file mode 100644 index 00000000..34c721f4 --- /dev/null +++ b/test/tools/patch_recovery_service.py @@ -0,0 +1,25 @@ +p = 'backend/services/recovery/service.go' +with open(p, 'rb') as f: + data = f.read() + +# 1. In Begin (around line 199) +t1 = b'if !has || user.EffectiveRole() == access.RoleSuperAdmin {\n\t\treturn s.issueDecoyChallengeWithContext' +r1 = b'if !has {\n\t\treturn s.issueDecoyChallengeWithContext' +assert t1 in data, 't1 not found' +data = data.replace(t1, r1, 1) + +# 2. In verifyEmailIdentity (around line 259) +t2 = b'if err != nil || !has || user.EffectiveRole() == access.RoleSuperAdmin || s.external == nil {\n\t\treturn nil, nil, ErrRecoveryDenied\n\t}' +r2 = b'if err != nil || !has || s.external == nil {\n\t\treturn nil, nil, ErrRecoveryDenied\n\t}' +assert t2 in data, 't2 not found' +data = data.replace(t2, r2, 1) + +# 3. In CompleteWithContext (around line 349) +t3 = b'if err != nil || !has || user.Disabled != 0 || user.EffectiveRole() == access.RoleSuperAdmin || user.SessionVersion != item.SessionVersion {\n\t\treturn ErrRecoveryDenied\n\t}' +r3 = b'if err != nil || !has || user.Disabled != 0 || user.SessionVersion != item.SessionVersion {\n\t\treturn ErrRecoveryDenied\n\t}' +assert t3 in data, 't3 not found' +data = data.replace(t3, r3, 1) + +with open(p, 'wb') as f: + f.write(data) +print('updated service.go') \ No newline at end of file diff --git a/test/tools/patch_settings_go.py b/test/tools/patch_settings_go.py new file mode 100644 index 00000000..e5a86df9 --- /dev/null +++ b/test/tools/patch_settings_go.py @@ -0,0 +1,69 @@ +p = 'backend/controllers/settings.go' +with open(p, 'rb') as f: + data = f.read() + +# 1. Add imports for verification & externalemail +t_import = b'\tgit.pcux.cn/xuans/Smail/services/auth\n' +r_import = b'\tgit.pcux.cn/xuans/Smail/services/auth\n\tgit.pcux.cn/xuans/Smail/services/externalemail\n\tgit.pcux.cn/xuans/Smail/services/verification\n' +assert t_import in data, 't_import not found' +data = data.replace(t_import, r_import, 1) + +# 2. Update modifyPasswordRequest struct +idx_struct = data.find(b'type modifyPasswordRequest struct {') +idx_struct_end = data.find(b'}\n\nfunc ModifyPassword', idx_struct) +assert idx_struct != -1 and idx_struct_end != -1 +new_struct = b'''type modifyPasswordRequest struct { +\tCurrentPassword string json:current_password +\tPassword string json:password +\tCode string json:code +\tChallengeID string json:challenge_id +}''' +data = data[:idx_struct] + new_struct + data[idx_struct_end+1:] + +# 3. Replace super admin block with comment +idx_super = data.find(b'if ctx.EffectiveRole() == access.RoleSuperAdmin {') +assert idx_super != -1, 'idx_super not found' +idx_super_end = data.find(b'var retData modifyPasswordRequest', idx_super) +assert idx_super_end != -1, 'idx_super_end not found' + +r_super = '''// 无论是普通用户还是管理员、超级管理员均支持修改密码,超级管理员必须通过绑定的外部邮箱验证码进行核验 +\t'''.encode('utf-8') +data = data[:idx_super] + r_super + data[idx_super_end:] + +# 4. In ModifyPassword, after authenticating current password and before updating password, check code for super_admin +t_auth_ok = b'\tloginPairAttempts.clear(pairKey)\n\tloginAccountAttempts.clear(accountKey)\n' +r_auth_ok = '''\tloginPairAttempts.clear(pairKey) +\tloginAccountAttempts.clear(accountKey) + +\tif ctx.EffectiveRole() == access.RoleSuperAdmin { +\t\truntime := currentSecurityRuntime() +\t\tif runtime.external == nil || runtime.verification == nil { +\t\t\tresponse.NewErrorResponse(response.ServerError, 安全验证服务不可用, ").FPrint(w) +\t\t\treturn +\t\t} +\t\tbinding, err := runtime.external.FindVerifiedPrimaryByUser(ctx.UserID) +\t\tif err != nil { +\t\t\tresponse.NewErrorResponse(response.ParamsError, 超级管理员必须先绑定外部安全邮箱才能修改密码, ).FPrint(w) +\t\t\treturn +\t\t} +\t\texternalEmail, err := runtime.external.Decrypt(*binding) +\t\tif err != nil { +\t\t\tresponse.NewErrorResponse(response.ServerError, 读取外部安全邮箱失败, ).FPrint(w) +\t\t\treturn +\t\t} +\t\tif strings.TrimSpace(retData.Code) == || strings.TrimSpace(retData.ChallengeID) == { +\t\t\tresponse.NewErrorResponse(response.ParamsError, 超级管理员修改密码必须提供邮箱验证码, ).FPrint(w) +\t\t\treturn +\t\t} +\t\tif err := runtime.verification.Verify(retData.ChallengeID, ctx.UserAccount, externalEmail, retData.Code, verification.PurposePasswordChange, remoteIP(req)); err != nil { +\t\t\tresponse.NewErrorResponse(response.ParamsError, 验证码错误或已过期, ).FPrint(w) +\t\t\treturn +\t\t} +\t} +'''.encode('utf-8') +assert t_auth_ok in data, 't_auth_ok not found' +data = data.replace(t_auth_ok, r_auth_ok, 1) + +with open(p, 'wb') as f: + f.write(data) +print('settings.go patched successfully') \ No newline at end of file diff --git a/test/tools/patch_store.py b/test/tools/patch_store.py new file mode 100644 index 00000000..4c218d2f --- /dev/null +++ b/test/tools/patch_store.py @@ -0,0 +1,17 @@ +p = 'backend/services/verification/store.go' +with open(p, 'rb') as f: + data = f.read() + +t1 = b'PurposePasswordRecover Purpose = "password_recover"\n' +r1 = b'PurposePasswordRecover Purpose = "password_recover"\n\tPurposePasswordChange Purpose = "password_change"\n' +assert t1 in data +data = data.replace(t1, r1, 1) + +t2 = b'case PurposeLogin, PurposePasswordRecover, PurposeExternalEmailBind, PurposeExternalEmailChange:' +r2 = b'case PurposeLogin, PurposePasswordRecover, PurposePasswordChange, PurposeExternalEmailBind, PurposeExternalEmailChange:' +assert t2 in data +data = data.replace(t2, r2, 1) + +with open(p, 'wb') as f: + f.write(data) +print('updated store.go') \ No newline at end of file diff --git a/test/tools/query_arbitrary.py b/test/tools/query_arbitrary.py new file mode 100644 index 00000000..cb3f8e15 --- /dev/null +++ b/test/tools/query_arbitrary.py @@ -0,0 +1,11 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, type, subject, from_address, send_user_id, status FROM email ORDER BY id ASC;"') +print("Security questions:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, count(*), sum(consumed) FROM recovery_code GROUP BY user_id;"') +print("Recovery codes count:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email, verified FROM external_email;"') +print("External emails:\n", stdout.read().decode()) +ssh.close() diff --git a/test/tools/query_emails.py b/test/tools/query_emails.py new file mode 100644 index 00000000..686202ba --- /dev/null +++ b/test/tools/query_emails.py @@ -0,0 +1,16 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, user_id, email_id, is_read, group_id, status FROM user_email ORDER BY id DESC LIMIT 10;"') +print("Security questions:\n", stdout.read().decode("utf-8", errors="ignore").encode("utf-8")) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, type, subject, send_user_id, status FROM email ORDER BY id DESC LIMIT 10;"') +print("Recovery codes count:\n", stdout.read().decode("utf-8", errors="ignore").encode("utf-8")) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email, verified FROM external_email;"') +print("External emails:\n", stdout.read().decode("utf-8", errors="ignore").encode("utf-8")) +ssh.close() + + + + + diff --git a/test/tools/query_remote_details.py b/test/tools/query_remote_details.py new file mode 100644 index 00000000..e650a3f3 --- /dev/null +++ b/test/tools/query_remote_details.py @@ -0,0 +1,11 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, question FROM security_question;"') +print("Security questions:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, count(*), sum(consumed) FROM recovery_code GROUP BY user_id;"') +print("Recovery codes count:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT user_id, email, verified FROM external_email;"') +print("External emails:\n", stdout.read().decode()) +ssh.close() diff --git a/test/tools/query_remote_tables.py b/test/tools/query_remote_tables.py new file mode 100644 index 00000000..01727ebc --- /dev/null +++ b/test/tools/query_remote_tables.py @@ -0,0 +1,11 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db ".schema security_question"') +print("Schema security_question:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db ".schema recovery_code"') +print("Schema recovery_code:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT * FROM user;"') +print("Users:\n", stdout.read().decode()) +ssh.close() diff --git a/test/tools/query_remote_testuser.py b/test/tools/query_remote_testuser.py new file mode 100644 index 00000000..ffccfb86 --- /dev/null +++ b/test/tools/query_remote_testuser.py @@ -0,0 +1,9 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT * FROM security_question WHERE user_id = 2;"') +print("Testuser2 security question:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT * FROM recovery_code WHERE user_id = 2;"') +print("Testuser2 recovery codes:\n", stdout.read().decode()) +ssh.close() diff --git a/test/tools/query_remote_users.py b/test/tools/query_remote_users.py new file mode 100644 index 00000000..878b15ea --- /dev/null +++ b/test/tools/query_remote_users.py @@ -0,0 +1,9 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db "SELECT id, account, name, role, is_admin, disabled FROM user;"') +print("Users in DB:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('sqlite3 /opt/smail/data/smail.db ".tables"') +print("Tables in DB:\n", stdout.read().decode()) +ssh.close() diff --git a/test/tools/run_check_ext.py b/test/tools/run_check_ext.py new file mode 100644 index 00000000..09bc9d31 --- /dev/null +++ b/test/tools/run_check_ext.py @@ -0,0 +1,22 @@ +import sys, io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) + +s = ''' +import sqlite3 +conn = sqlite3.connect('/opt/smail/data/smail.db') +c = conn.cursor() +c.execute("SELECT id, user_id, status, email_type, created_at, updated_at FROM external_email") +print("external_email rows:", c.fetchall()) +''' +sftp = ssh.open_sftp() +with sftp.file('/tmp/check_external_email.py', 'w') as f: + f.write(s) +sftp.close() + +_, out, err = ssh.exec_command('python3 /tmp/check_external_email.py') +print('OUT:\n', out.read().decode('utf-8', errors='ignore')) +ssh.close() diff --git a/test/tools/run_check_hex.py b/test/tools/run_check_hex.py new file mode 100644 index 00000000..82a8c75b --- /dev/null +++ b/test/tools/run_check_hex.py @@ -0,0 +1,23 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) + +s = ''' +import sqlite3 + +conn = sqlite3.connect('/opt/smail/data/smail.db') +c = conn.cursor() + +c.execute('SELECT id, user_id, quote(client_name) FROM client_credential') +for row in c.fetchall(): + print('Hex Client credential:', row[0], row[1], row[2]) +''' +sftp = ssh.open_sftp() +with sftp.file('/tmp/check_hex.py', 'w') as f: + f.write(s) +sftp.close() + +_, out, err = ssh.exec_command('python3 /tmp/check_hex.py') +print('OUT:\n', out.read().decode('utf-8', errors='ignore')) +ssh.close() diff --git a/test/tools/run_check_hex2.py b/test/tools/run_check_hex2.py new file mode 100644 index 00000000..0e399d47 --- /dev/null +++ b/test/tools/run_check_hex2.py @@ -0,0 +1,30 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) + +s = ''' +import sqlite3 + +conn = sqlite3.connect('/opt/smail/data/smail.db') +c = conn.cursor() + +c.execute('SELECT id, user_id, hex(client_name) FROM client_credential') +for row in c.fetchall(): + h = row[2] + raw_bytes = bytes.fromhex(h) + print('Raw hex:', row[0], row[1], h) + for enc in ['utf-8', 'gbk', 'gb2312', 'latin1']: + try: + print(f' {enc}: {raw_bytes.decode(enc)}') + except Exception as e: + pass +''' +sftp = ssh.open_sftp() +with sftp.file('/tmp/check_hex2.py', 'w') as f: + f.write(s) +sftp.close() + +_, out, err = ssh.exec_command('python3 /tmp/check_hex2.py') +print('OUT:\n', out.read().decode('utf-8', errors='ignore')) +ssh.close() diff --git a/test/tools/run_check_hex3.py b/test/tools/run_check_hex3.py new file mode 100644 index 00000000..e17a7e68 --- /dev/null +++ b/test/tools/run_check_hex3.py @@ -0,0 +1,10 @@ +import sys, io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) + +_, out, err = ssh.exec_command('python3 /tmp/check_hex2.py') +print('OUT:\n', out.read().decode('utf-8', errors='ignore')) +ssh.close() diff --git a/test/tools/run_check_logs.py b/test/tools/run_check_logs.py new file mode 100644 index 00000000..a08cf5e1 --- /dev/null +++ b/test/tools/run_check_logs.py @@ -0,0 +1,28 @@ +import sys, io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) + +s = ''' +import sqlite3 +conn = sqlite3.connect('/opt/smail/data/smail.db') +c = conn.cursor() +c.execute("SELECT name FROM sqlite_master WHERE type='table';") +print('Tables:', [r[0] for r in c.fetchall()]) + +c.execute("SELECT * FROM operation_log ORDER BY id DESC LIMIT 5;") +print('Recent op logs:', c.fetchall()) + +c.execute("SELECT * FROM mail_log ORDER BY id DESC LIMIT 5;") +print('Recent mail logs:', c.fetchall()) +''' +sftp = ssh.open_sftp() +with sftp.file('/tmp/check_logs.py', 'w') as f: + f.write(s) +sftp.close() + +_, out, err = ssh.exec_command('python3 /tmp/check_logs.py') +print('OUT:\n', out.read().decode('utf-8', errors='ignore')) +ssh.close() diff --git a/test/tools/run_check_names.py b/test/tools/run_check_names.py new file mode 100644 index 00000000..cb522f0d --- /dev/null +++ b/test/tools/run_check_names.py @@ -0,0 +1,27 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) + +s = ''' +import sqlite3 + +conn = sqlite3.connect('/opt/smail/data/smail.db') +c = conn.cursor() + +c.execute('SELECT id, user_id, client_name FROM client_credential') +for row in c.fetchall(): + print('Client credential:', row[0], row[1], repr(row[2])) + +c.execute('SELECT id, account, name, role FROM user') +for row in c.fetchall(): + print('User:', row) +''' +sftp = ssh.open_sftp() +with sftp.file('/tmp/check_names.py', 'w') as f: + f.write(s) +sftp.close() + +_, out, err = ssh.exec_command('python3 /tmp/check_names.py') +print('OUT:\n', out.read().decode('utf-8', errors='ignore')) +ssh.close() diff --git a/test/tools/run_check_users.py b/test/tools/run_check_users.py new file mode 100644 index 00000000..fe3baef0 --- /dev/null +++ b/test/tools/run_check_users.py @@ -0,0 +1,31 @@ +import sys, io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) + +s = ''' +import sqlite3 +conn = sqlite3.connect('/opt/smail/data/smail.db') +c = conn.cursor() +c.execute('SELECT id, hex(account), hex(name), role FROM user') +for r in c.fetchall(): + print('User ID:', r[0], 'Role:', r[3]) + try: + print(' account:', bytes.fromhex(r[1]).decode('utf-8')) + except: + pass + try: + print(' name:', bytes.fromhex(r[2]).decode('utf-8')) + except: + pass +''' +sftp = ssh.open_sftp() +with sftp.file('/tmp/check_users.py', 'w') as f: + f.write(s) +sftp.close() + +_, out, err = ssh.exec_command('python3 /tmp/check_users.py') +print('OUT:\n', out.read().decode('utf-8', errors='ignore')) +ssh.close() diff --git a/test/tools/run_decrypt_creds.py b/test/tools/run_decrypt_creds.py new file mode 100644 index 00000000..1db71fe6 --- /dev/null +++ b/test/tools/run_decrypt_creds.py @@ -0,0 +1,70 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) + +s = ''' +import sqlite3, base64, glob +from cryptography.hazmat.primitives.ciphers.aead import AESGCM + +key_files = glob.glob('/opt/smail/ssl/**/credential.key', recursive=True) + glob.glob('/opt/smail/config/**/credential.key', recursive=True) +print('Key files:', key_files) +if not key_files: + exit(1) + +with open(key_files[0], 'rb') as f: + key = f.read() + +aesgcm = AESGCM(key) + +conn = sqlite3.connect('/opt/smail/data/smail.db') +c = conn.cursor() + +print('=== CLIENT CREDENTIALS ===') +c.execute('SELECT id, user_id, client_name, code_ciphertext, revoked_at, last_used_at, created_at FROM client_credential') +for row in c.fetchall(): + cid, uid, cname, enc_code, revoked, last_used, created = row + if not enc_code: + print(f'ID {cid} (UID {uid}, {cname}): None (revoked={revoked})') + continue + parts = enc_code.split(':') + b64 = parts[2] + padded = b64 + '=' * (-len(b64) % 4) + raw = base64.urlsafe_b64decode(padded) + nonce = raw[:12] + ct = raw[12:] + try: + pt = aesgcm.decrypt(nonce, ct, b'smail-config-v1') + print(f'ID {cid} (UID {uid}, {cname}): {pt.decode()} | created={created} | revoked={revoked} | last_used={last_used}') + except Exception as e: + print(f'ID {cid} decrypt err: {e}') + +print('=== PROTOCOL CREDENTIALS ===') +c.execute('SELECT id, user_id, protocol, enabled, code_ciphertext, last_used_at, created_at FROM protocol_credential') +for row in c.fetchall(): + pid, uid, proto, enabled, enc_code, last_used, created = row + if not enc_code: + print(f'Proto {proto} (UID {uid}, enabled={enabled}): None') + continue + parts = enc_code.split(':') + b64 = parts[2] + padded = b64 + '=' * (-len(b64) % 4) + raw = base64.urlsafe_b64decode(padded) + nonce = raw[:12] + ct = raw[12:] + try: + pt = aesgcm.decrypt(nonce, ct, b'smail-config-v1') + print(f'Proto {proto} (UID {uid}, enabled={enabled}): {pt.decode()} | created={created} | last_used={last_used}') + except Exception as e: + print(f'Proto {proto} decrypt err: {e}') +''' + +sftp = ssh.open_sftp() +with sftp.file('/tmp/decrypt_creds.py', 'w') as f: + f.write(s) +sftp.close() + +_, out, err = ssh.exec_command('python3 /tmp/decrypt_creds.py') +print('OUT:\n', out.read().decode('utf-8', errors='ignore')) +print('ERR:\n', err.read().decode('utf-8', errors='ignore')) +ssh.close() diff --git a/test/tools/run_forgot_password_acceptance.py b/test/tools/run_forgot_password_acceptance.py new file mode 100644 index 00000000..665d18fa --- /dev/null +++ b/test/tools/run_forgot_password_acceptance.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +import os, sys, time, json +from playwright.sync_api import sync_playwright + +output_dir = os.path.join(os.getcwd(), 'test-log', '2026-09-14', '截图') +log_file = os.path.join(os.getcwd(), 'test-log', '2026-09-14', '测试log', 'forgot_password_acceptance.log') +os.makedirs(output_dir, exist_ok=True) +os.makedirs(os.path.dirname(log_file), exist_ok=True) + +def write_log(msg): + ts = time.strftime('%Y-%m-%d %H:%M:%S') + line = f'[{ts}] {msg}' + print(line) + with open(log_file, 'a', encoding='utf-8') as f: + f.write(line + '\n') + +with open('test/tools/testuser2_credential_info.json', 'r', encoding='utf-8') as f: + cred = json.load(f) + +account = cred['account'] +question = cred['question'] +answer = cred['answer'] +# 使用尚未被消费的第 2 个恢复码 +code = cred['codes'][1] +new_pwd = 'Reset888!' + +write_log('====================================================') +write_log('=== 开始普通用户忘记密码(离线恢复方式)端到端真实验收测试 ===') +write_log('====================================================') +write_log(f'目标测试账号: {account}') +write_log(f'密保问题: {question}') +write_log(f'密保答案: {answer}') +write_log(f'离线恢复码: {code}') +write_log(f'设定的新密码: {new_pwd}') + +with sync_playwright() as p: + browser = p.chromium.launch( + headless=True, + args=[ + '--no-sandbox', + '--ignore-certificate-errors', + '--host-resolver-rules=MAP mail.solit.cn 47.108.78.12, MAP solit.cn 47.108.78.12' + ] + ) + context = browser.new_context(viewport={'width': 1440, 'height': 900}, ignore_https_errors=True) + page = context.new_page() + + # 1. 访问登录页面 + write_log('1. 打开登录页面 https://mail.solit.cn/#/login') + page.goto('https://mail.solit.cn/#/login', wait_until='networkidle') + page.wait_for_timeout(1000) + page.screenshot(path=os.path.join(output_dir, '20_login_page.png')) + + # 2. 点击「忘记密码?」 + forgot_btn = page.locator('.forgot-password-btn') + write_log(f' 登录页检测到【忘记密码?】入口数量: {forgot_btn.count()}') + forgot_btn.click() + page.wait_for_timeout(1000) + write_log(f' 点击后跳转 URL: {page.url}') + page.screenshot(path=os.path.join(output_dir, '21_forgot_password_page.png')) + + # 3. 检查安全警示信息 + alert_box = page.locator('.reset-boundary') + if alert_box.count() > 0: + write_log(f' 超级管理员限制警示框: {alert_box.inner_text().strip()}') + + # 4. 切换到「离线恢复方式」 + switch_btn = page.locator('.switch-button') + write_log(f' 当前模式切换按钮文案: {switch_btn.inner_text().strip()}') + switch_btn.click() + page.wait_for_timeout(500) + write_log(f' 切换后切换按钮文案: {switch_btn.inner_text().strip()}') + page.screenshot(path=os.path.join(output_dir, '22_switched_to_offline.png')) + + # 5. 填写离线恢复表单各项 + write_log('2. 填写离线密码恢复表单项...') + inputs = page.locator('.recovery-form input') + inputs.nth(0).fill(account) + inputs.nth(1).fill(question) + inputs.nth(2).fill(answer) + inputs.nth(3).fill(code) + + page.wait_for_timeout(500) + page.screenshot(path=os.path.join(output_dir, '23_step1_form_completed.png')) + write_log(' 步骤 1 身份核验表单填写完毕,点击【下一步】') + + # 提交步骤 1 + step1_btn = page.locator('.recovery-footer button.recovery-action-btn') + step1_btn.click() + + page.wait_for_timeout(2500) + page.screenshot(path=os.path.join(output_dir, '24_step2_reset_password.png')) + + write_log('3. 进入步骤 2:设置新密码,填写密码...') + step2_inputs = page.locator('.password-grid-vertical input') + step2_inputs.nth(0).fill(new_pwd) + step2_inputs.nth(1).fill(new_pwd) + page.wait_for_timeout(500) + page.screenshot(path=os.path.join(output_dir, '25_step2_filled.png')) + + write_log(' 步骤 2 密码填写完毕,点击【下一步】提交重置...') + step2_btn = page.locator('.step-content .recovery-footer button.recovery-action-btn') + step2_btn.click() + + page.wait_for_timeout(2500) + page.screenshot(path=os.path.join(output_dir, '26_step3_success.png')) + write_log('4. 进入步骤 3:完成重置,点击【重新登录】') + relogin_btn = page.locator('.step-success button.recovery-action-btn') + relogin_btn.click() + page.wait_for_timeout(2000) + + msg_items = page.locator('.el-message') + for i in range(msg_items.count()): + write_log(f' 页面通知消息: {msg_items.nth(i).inner_text().strip()}') + + write_log(f' 重置后当前页面 URL: {page.url}') + + # 7. 自动回到登录页后,使用新密码执行登录验证 + if '#/login' in page.url: + write_log('4. 已成功自动返回登录页,开始验证新密码登入...') + # 页面重新加载后输入 + page.wait_for_timeout(1000) + login_user = page.locator('.login-form input[type=\"text\"], input[placeholder*=\"账号\"], input[placeholder*=\"用户名\"]').first + login_pwd = page.locator('.login-form input[type=\"password\"], input[placeholder*=\"密码\"]').first + + login_user.fill(account) + login_pwd.fill(new_pwd) + page.screenshot(path=os.path.join(output_dir, '25_input_new_password.png')) + + login_submit = page.locator('.login-button, button:has-text(\"登录\")').first + login_submit.click() + page.wait_for_timeout(3500) + + write_log(f' 新密码登录后 URL: {page.url}') + page.screenshot(path=os.path.join(output_dir, '26_logged_in_success.png')) + + # 检查是否成功进入邮箱主界面 + if '#/' in page.url and '#/login' not in page.url: + write_log(' [SUCCESS] 新密码验证成功!顺利进入系统主界面。') + else: + write_log(' [WARNING] 登录后未进入预期邮箱主页,请检查。') + else: + write_log(f' [FAILED] 重置后未返回登录页,当前停留于: {page.url}') + + browser.close() + write_log('=== 忘记密码全流程端到端测试结束 ===') \ No newline at end of file diff --git a/test/tools/send_test_email.py b/test/tools/send_test_email.py new file mode 100644 index 00000000..75724bd4 --- /dev/null +++ b/test/tools/send_test_email.py @@ -0,0 +1,32 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('47.108.78.12', port=22, username='root', password='10102442Lb!', timeout=10) + +s = '''import smtplib, ssl +from email.mime.text import MIMEText + +for target, subj in [ + ('gfxezc@163.com', 'Smail DirectMail Test - Admin'), + ('xuanspark@163.com', 'Smail DirectMail Test - User'), + ('likb_blue@163.com', 'Smail DirectMail Test - SuperAdmin') +]: + msg = MIMEText('Smail DirectMail test outbound delivery', 'plain', 'utf-8') + msg['From'] = 'xuans@solit.cn' + msg['To'] = target + msg['Subject'] = subj + + context = ssl.create_default_context() + with smtplib.SMTP_SSL('smtpdm.aliyun.com', 465, context=context) as server: + server.login('xuans@solit.cn', 'Solit12ABcd') + res = server.sendmail('xuans@solit.cn', [target], msg.as_string()) + print(f'Send to {target}: {res}') +''' +sftp = ssh.open_sftp() +with sftp.file('/tmp/test_send_all.py', 'w') as f: + f.write(s) +sftp.close() +_, out, err = ssh.exec_command('python3 /tmp/test_send_all.py') +print('OUT:\n', out.read().decode()) +print('ERR:\n', err.read().decode()) +ssh.close() diff --git a/test/tools/test_client_credentials.py b/test/tools/test_client_credentials.py new file mode 100644 index 00000000..07276ef1 --- /dev/null +++ b/test/tools/test_client_credentials.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +import urllib.request, ssl, json, http.cookiejar + +ctx = ssl.create_default_context() +ctx.check_hostname = False +ctx.verify_mode = ssl.CERT_NONE +cj = http.cookiejar.CookieJar() +opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ctx), urllib.request.HTTPCookieProcessor(cj)) + +# 1. 登录 xuans +login_payload = json.dumps({'account': 'xuans', 'password': '12345678'}).encode('utf-8') +req = urllib.request.Request('https://47.108.78.12/api/v1/login', data=login_payload, headers={ + 'Content-Type': 'application/json', + 'Host': 'mail.solit.cn', + 'Origin': 'https://mail.solit.cn' +}) +resp = opener.open(req, timeout=5) +print('Login resp:', resp.read().decode()) + +csrf_token = '' +for c in cj: + if c.name == 'smail_csrf': + csrf_token = c.value + +headers = { + 'Content-Type': 'application/json', + 'Host': 'mail.solit.cn', + 'Origin': 'https://mail.solit.cn', + 'Referer': 'https://mail.solit.cn/', + 'X-CSRF-Token': csrf_token +} + +# 2. 读取客户端授权码列表 +req_list = urllib.request.Request('https://47.108.78.12/api/v1/security/client-credentials/list', headers=headers) +try: + r_list = opener.open(req_list, timeout=5) + print('List client credentials:\n', r_list.read().decode()) +except urllib.error.HTTPError as e: + print('List error:', e.code, e.read().decode()) + +# 3. 输入密码验密 reveal 授权码明文 +req_reveal = urllib.request.Request( + 'https://47.108.78.12/api/v1/security/client-credentials/reveal', + data=json.dumps({'password': '12345678'}).encode('utf-8'), + headers=headers +) +try: + r_reveal = opener.open(req_reveal, timeout=5) + print('Reveal client credentials:\n', r_reveal.read().decode()) +except urllib.error.HTTPError as e: + print('Reveal error:', e.code, e.read().decode()) diff --git a/test/tools/test_echo.py b/test/tools/test_echo.py new file mode 100644 index 00000000..852f6936 --- /dev/null +++ b/test/tools/test_echo.py @@ -0,0 +1,8 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command("echo HELLO") +print(stdout:, stdout.read().decode()) +print(stderr:, stderr.read().decode()) +ssh.close() diff --git a/test/tools/test_edit_user_pwd.py b/test/tools/test_edit_user_pwd.py new file mode 100644 index 00000000..47d2afce --- /dev/null +++ b/test/tools/test_edit_user_pwd.py @@ -0,0 +1,40 @@ +import urllib.request, ssl, json, http.cookiejar +ctx = ssl.create_default_context() +ctx.check_hostname = False +ctx.verify_mode = ssl.CERT_NONE +cj = http.cookiejar.CookieJar() +opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ctx), urllib.request.HTTPCookieProcessor(cj)) + +# 登录 xuans +login_payload = json.dumps({"account": "xuans", "password": "12345678"}).encode("utf-8") +req = urllib.request.Request("https://47.108.78.12/api/v1/login", data=login_payload, headers={ + "Content-Type": "application/json", + "Host": "mail.solit.cn", + "Origin": "https://mail.solit.cn" +}) +opener.open(req, timeout=5) + +csrf_token = "" +for c in cj: + if c.name == "smail_csrf": + csrf_token = c.value + +headers = { + "Content-Type": "application/json", + "Host": "mail.solit.cn", + "Origin": "https://mail.solit.cn", + "Referer": "https://mail.solit.cn/", + "X-CSRF-Token": csrf_token +} + +edit_payload = json.dumps({ + "id": 2, + "account": "testuser2", + "username": "普通用户", + "password": "Password123!", + "disabled": 0, + "role": "user" +}).encode("utf-8") +req_edit = urllib.request.Request("https://47.108.78.12/api/v1/user/edit", data=edit_payload, headers=headers) +resp_edit = opener.open(req_edit, timeout=5) +print("Edit user response:", resp_edit.read().decode()) diff --git a/test/tools/test_find_req.py b/test/tools/test_find_req.py new file mode 100644 index 00000000..08b4ae43 --- /dev/null +++ b/test/tools/test_find_req.py @@ -0,0 +1,7 @@ +p = 'backend/controllers/security.go' +with open(p, 'rb') as f: + data = f.read() + +idx = data.find(b'func RequestVerification') +idx_end = data.find(b'// VerifyVerification', idx) +print(data[idx:idx_end].decode('utf-8')) \ No newline at end of file diff --git a/test/tools/test_forgot_password_e2e.py b/test/tools/test_forgot_password_e2e.py new file mode 100644 index 00000000..891e8f32 --- /dev/null +++ b/test/tools/test_forgot_password_e2e.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +import os, sys, time, json +from playwright.sync_api import sync_playwright + +output_dir = os.path.join(os.getcwd(), 'test-log', '2026-09-14', '截图') +log_file = os.path.join(os.getcwd(), 'test-log', '2026-09-14', '测试log', 'forgot_password_acceptance.log') +os.makedirs(output_dir, exist_ok=True) +os.makedirs(os.path.dirname(log_file), exist_ok=True) + +def write_log(msg): + ts = time.strftime('%Y-%m-%d %H:%M:%S') + line = f'[{ts}] {msg}' + print(line) + with open(log_file, 'a', encoding='utf-8') as f: + f.write(line + '\n') + +with sync_playwright() as p: + browser = p.chromium.launch( + headless=True, + args=[ + '--no-sandbox', + '--ignore-certificate-errors', + '--host-resolver-rules=MAP mail.solit.cn 47.108.78.12, MAP solit.cn 47.108.78.12' + ] + ) + context = browser.new_context(viewport={'width': 1440, 'height': 900}, ignore_https_errors=True) + page = context.new_page() + + write_log('=== 开始忘记密码全流程测试 ===') + + # 1. 登录页检查忘记密码入口 + write_log('1. 打开登录页面') + page.goto('https://mail.solit.cn/#/login', wait_until='networkidle') + page.screenshot(path=os.path.join(output_dir, '10_login_page_forgot_btn.png')) + + forgot_btn = page.locator('button.forgot-password-btn, a:has-text("忘记密码")') + write_log(f' 忘记密码入口数量: {forgot_btn.count()}') + if forgot_btn.count() > 0: + forgot_btn.first.click() + page.wait_for_timeout(1500) + else: + page.goto('https://mail.solit.cn/#/forgot-password', wait_until='networkidle') + page.wait_for_timeout(1500) + + write_log(f' 当前页面 URL: {page.url}') + page.screenshot(path=os.path.join(output_dir, '11_forgot_password_page.png')) + + # 2. 检查页面关键元素与边界提示 + boundary_alert = page.locator('.reset-boundary') + write_log(f' 超级管理员限制警示框存在: {boundary_alert.count() > 0}') + if boundary_alert.count() > 0: + write_log(f' 警示内容: {boundary_alert.inner_text().strip()}') + + # 3. 检查切换验证方式 + switch_btn = page.locator('.switch-button') + write_log(f' 切换方式按钮文本: {switch_btn.inner_text().strip()}') + switch_btn.click() + page.wait_for_timeout(500) + write_log(f' 切换后按钮文本: {switch_btn.inner_text().strip()}') + page.screenshot(path=os.path.join(output_dir, '12_switched_to_offline.png')) + + # 4. 测试以超级管理员账号 xuans 进行离线恢复(验证安全拒绝边界) + write_log('2. 测试超级管理员账号 xuans 在 Web 端重置(必须被安全拒绝)') + user_input = page.locator('.recovery-form input').nth(0) + user_input.fill('xuans') + + # 离线输入:密保问题、密保答案、恢复码、新密码、确认密码 + q_input = page.locator('.recovery-form input').nth(1) + a_input = page.locator('.recovery-form input').nth(2) + code_input = page.locator('.recovery-form input').nth(3) + p1_input = page.locator('.recovery-form input').nth(4) + p2_input = page.locator('.recovery-form input').nth(5) + + q_input.fill('幸运数字') + a_input.fill('8') + code_input.fill('REC-1234-5678') + p1_input.fill('NewPass1234!') + p2_input.fill('NewPass1234!') + + submit_btn = page.locator('.recovery-footer button.recovery-action-btn') + submit_btn.click() + page.wait_for_timeout(2000) + page.screenshot(path=os.path.join(output_dir, '13_superadmin_denied.png')) + + state_span = page.locator('.verification-state') + msg_box = page.locator('.el-message') + write_log(f' 状态文本: {state_span.inner_text() if state_span.count() > 0 else "无"}') + for i in range(msg_box.count()): + write_log(f' 提示消息: {msg_box.nth(i).inner_text().strip()}') + + browser.close() diff --git a/test/tools/test_generate_codes_api.py b/test/tools/test_generate_codes_api.py new file mode 100644 index 00000000..da851c3f --- /dev/null +++ b/test/tools/test_generate_codes_api.py @@ -0,0 +1,35 @@ +import urllib.request, ssl, json, http.cookiejar +ctx = ssl.create_default_context() +ctx.check_hostname = False +ctx.verify_mode = ssl.CERT_NONE +cj = http.cookiejar.CookieJar() +opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ctx), urllib.request.HTTPCookieProcessor(cj)) + +# 登录 xuans +login_payload = json.dumps({"account": "xuans", "password": "12345678"}).encode("utf-8") +req = urllib.request.Request("https://47.108.78.12/api/v1/login", data=login_payload, headers={ + "Content-Type": "application/json", + "Host": "mail.solit.cn", + "Origin": "https://mail.solit.cn" +}) +opener.open(req, timeout=5) + +csrf_token = "" +for c in cj: + if c.name == "smail_csrf": + csrf_token = c.value + +headers = { + "Content-Type": "application/json", + "Host": "mail.solit.cn", + "Origin": "https://mail.solit.cn", + "Referer": "https://mail.solit.cn/", + "X-CSRF-Token": csrf_token +} + +# 调用管理接口为 testuser2 (id=2) 生成 recovery codes +gen_payload = json.dumps({"user_id": 2, "count": 5}).encode("utf-8") +req_gen = urllib.request.Request("https://47.108.78.12/api/v1/user/security/recovery-codes/generate", data=gen_payload, headers=headers) +resp_gen = opener.open(req_gen, timeout=5) +data = json.loads(resp_gen.read().decode("utf-8")) +print("Generate recovery codes response:", data) diff --git a/test/tools/test_inbound_info.py b/test/tools/test_inbound_info.py new file mode 100644 index 00000000..281b76ba --- /dev/null +++ b/test/tools/test_inbound_info.py @@ -0,0 +1,17 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) +stdin, stdout, stderr = ssh.exec_command('python3 -c "import sqlite3; conn=sqlite3.connect(\x27/opt/smail/data/smail.db\x27); c=conn.cursor(); print(c.execute(\x27SELECT id, user_id, email_id, is_read, is_trash FROM user_email ORDER BY id DESC LIMIT 5\x27).fetchall())"') +print("User emails top 5:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('python3 -c "import sqlite3; conn=sqlite3.connect(\x27/opt/smail/data/smail.db\x27); c=conn.cursor(); print(c.execute(\x27SELECT id, subject, send_time FROM email ORDER BY id DESC LIMIT 5\x27).fetchall())"') +print("Emails top 5:\n", stdout.read().decode()) +stdin, stdout, stderr = ssh.exec_command('python3 -c "import sqlite3; conn=sqlite3.connect(\x27/opt/smail/data/smail.db\x27); c=conn.cursor(); print(c.execute(\x27SELECT id, name, user_name FROM user\x27).fetchall())"') +print("Users:\n", stdout.read().decode()) +ssh.close() + + + + + + diff --git a/test/tools/test_list_ext.py b/test/tools/test_list_ext.py new file mode 100644 index 00000000..96ad9963 --- /dev/null +++ b/test/tools/test_list_ext.py @@ -0,0 +1,9 @@ +p = 'backend/controllers/security.go' +with open(p, 'rb') as f: + data = f.read() + +idx = data.find(b'external-email/list') +if idx == -1: + idx = data.find(b'external-email') +print('idx:', idx) +print(data[idx-100:idx+400].decode('utf-8', errors='replace')) \ No newline at end of file diff --git a/test/tools/test_roles_ui.py b/test/tools/test_roles_ui.py new file mode 100644 index 00000000..e69de29b diff --git a/test/tools/test_smtpdm_ports.py b/test/tools/test_smtpdm_ports.py new file mode 100644 index 00000000..e69de29b diff --git a/test/tools/test_testuser2_login.py b/test/tools/test_testuser2_login.py new file mode 100644 index 00000000..12db5f89 --- /dev/null +++ b/test/tools/test_testuser2_login.py @@ -0,0 +1,20 @@ +import urllib.request, ssl, json, http.cookiejar +ctx = ssl.create_default_context() +ctx.check_hostname = False +ctx.verify_mode = ssl.CERT_NONE +cj = http.cookiejar.CookieJar() +opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ctx), urllib.request.HTTPCookieProcessor(cj)) + +for pwd in ["12345678", "123456", "testuser2", "Password123!", "Tester123456!"]: + login_payload = json.dumps({"account": "testuser2", "password": pwd}).encode("utf-8") + req = urllib.request.Request("https://47.108.78.12/api/v1/login", data=login_payload, headers={ + "Content-Type": "application/json", + "Host": "mail.solit.cn", + "Origin": "https://mail.solit.cn" + }) + try: + resp = opener.open(req, timeout=5) + res = json.loads(resp.read().decode()) + print(f"Pwd {pwd} -> errorNo: {res.get('errorNo')}, errorMsg: {res.get('errorMsg')}") + except Exception as e: + print(f"Pwd {pwd} -> exception: {e}") diff --git a/test/tools/test_validate_api.py b/test/tools/test_validate_api.py new file mode 100644 index 00000000..dbe0f0fa --- /dev/null +++ b/test/tools/test_validate_api.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +import requests, json +import urllib3 +urllib3.disable_warnings() + +s = requests.Session() +# 47.108.78.12 80 端口 +r_login = s.post('http://47.108.78.12:80/api/login', json={'username': 'xuans', 'password': '12345678'}) +print('Login:', r_login.status_code, r_login.text) + +r_get = s.get('http://47.108.78.12:80/api/v1/config/mail') +print('Get mail:', r_get.status_code, r_get.text) + +# 模拟前端发送的 validate 请求 +data = r_get.json().get('data', {}) +generation = data.get('meta', {}).get('generation', 2) + +# 用户在前端填入的配置 +payload = json.loads(json.dumps(data)) +# 注意前端 parsePayload 会对 payload 做一些处理: +# delete payload.relayPasswordStatus +# if (payload.relay && 'password' in payload.relay) delete payload.relay.password +if 'relayPasswordStatus' in payload: + del payload['relayPasswordStatus'] +if 'relay' in payload and 'password' in payload['relay']: + del payload['relay']['password'] + +payload['relay']['relayHost'] = 'smtpm.aliyun.com' +payload['relay']['relayPort'] = 587 +payload['relay']['username'] = 'xuans' +payload['relay']['from'] = 'xuans@solit.cn' +payload['relay']['fromDomain'] = 'solit.cn' + +secretChanges = { + 'relay.password': {'operation': 'replace', 'value': {'plaintext': 'Solit12ABcd'}} +} + +req_body = { + 'expectedGeneration': generation, + 'payload': payload, + 'secretChanges': secretChanges, + 'confirmation': {'confirmed': True, 'method': 'recent_auth'} +} + +print('Testing validate with smtpm.aliyun.com...') +r_val = s.post('http://47.108.78.12:80/api/v1/config/mail/validate', json=req_body) +print('Validate res:', r_val.status_code, r_val.text) + +# 再测一次直接 PUT (UpdateConfig) +r_put = s.put('http://47.108.78.12:80/api/v1/config/mail', json=req_body) +print('PUT res:', r_put.status_code, r_put.text) \ No newline at end of file diff --git a/test/tools/test_validate_remote.py b/test/tools/test_validate_remote.py new file mode 100644 index 00000000..bd98cb14 --- /dev/null +++ b/test/tools/test_validate_remote.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +import requests +import urllib3 +urllib3.disable_warnings() + +session = requests.Session() +login_res = session.post('https://mail.solit.cn/api/login', json={'username': 'xuans', 'password': '12345678'}, verify=False) +print('Login status:', login_res.status_code, login_res.text) + +get_res = session.get('https://mail.solit.cn/api/v1/config/mail', verify=False) +print('Config mail get:', get_res.status_code, get_res.text) + +status_res = session.get('https://mail.solit.cn/api/v1/config/status', verify=False) +print('Config status:', status_res.status_code, status_res.text) \ No newline at end of file diff --git a/test/tools/testuser2_credential_info.json b/test/tools/testuser2_credential_info.json new file mode 100644 index 00000000..59e17738 --- /dev/null +++ b/test/tools/testuser2_credential_info.json @@ -0,0 +1,13 @@ +{ + "account": "testuser2", + "current_password": "UserPass88!", + "question": "毕业学校", + "answer": "清华大学", + "codes": [ + "IGWRL-ZC7BC-L52E7IH7NQ", + "MFHHG-C5ERA-U2JP3OYSCQ", + "5WGFS-MANVR-S7YFSTRGCA", + "YFOJ3-HK237-Y4MUBIC5GA", + "4TUAM-6TEJN-PCEHCBISVA" + ] +} \ No newline at end of file diff --git a/test/tools/testuser2_setup_security.py b/test/tools/testuser2_setup_security.py new file mode 100644 index 00000000..0f07d48e --- /dev/null +++ b/test/tools/testuser2_setup_security.py @@ -0,0 +1,60 @@ +import urllib.request, ssl, json, http.cookiejar +ctx = ssl.create_default_context() +ctx.check_hostname = False +ctx.verify_mode = ssl.CERT_NONE +cj = http.cookiejar.CookieJar() +opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ctx), urllib.request.HTTPCookieProcessor(cj)) + +# 1. 登录 testuser2 +login_payload = json.dumps({"account": "testuser2", "password": "Password123!"}).encode("utf-8") +req = urllib.request.Request("https://47.108.78.12/api/v1/login", data=login_payload, headers={ + "Content-Type": "application/json", + "Host": "mail.solit.cn", + "Origin": "https://mail.solit.cn" +}) +resp = opener.open(req, timeout=5) +data = json.loads(resp.read().decode()) +print("Testuser2 login resp:", data) + +csrf_token = "" +for c in cj: + if c.name == "smail_csrf": + csrf_token = c.value + +headers = { + "Content-Type": "application/json", + "Host": "mail.solit.cn", + "Origin": "https://mail.solit.cn", + "Referer": "https://mail.solit.cn/", + "X-CSRF-Token": csrf_token +} + +# 2. 如果需要强制修改密码,先修改密码 +if data.get("data", {}).get("must_change_password"): + print("Testuser2 must change password, updating...") + pwd_payload = json.dumps({ + "old_password": "Password123!", + "new_password": "UserPass1234!" + }).encode("utf-8") + req_chg = urllib.request.Request("https://47.108.78.12/api/v1/settings/modify_password", data=pwd_payload, headers=headers) + resp_chg = opener.open(req_chg, timeout=5) + print("Change password resp:", resp_chg.read().decode()) + +# 3. 设置密保问题 +q_payload = json.dumps({ + "question": "毕业学校", + "answer": "清华大学" +}).encode("utf-8") +req_q = urllib.request.Request("https://47.108.78.12/api/v1/security/credentials/question", data=q_payload, headers=headers) +resp_q = opener.open(req_q, timeout=5) +print("Set question resp:", resp_q.read().decode()) + +# 4. 生成一组离线恢复码 +codes_payload = json.dumps({"count": 5}).encode("utf-8") +req_c = urllib.request.Request("https://47.108.78.12/api/v1/security/credentials/recovery-codes", data=codes_payload, headers=headers) +resp_c = opener.open(req_c, timeout=5) +c_data = json.loads(resp_c.read().decode()) +print("Generate recovery codes resp:", c_data) +if c_data.get("data", {}).get("codes"): + with open("test/tools/testuser2_codes.json", "w", encoding="utf-8") as f: + json.dump(c_data["data"], f, ensure_ascii=False, indent=2) diff --git a/test/tools/testuser2_setup_security_v2.py b/test/tools/testuser2_setup_security_v2.py new file mode 100644 index 00000000..9e03bcfc --- /dev/null +++ b/test/tools/testuser2_setup_security_v2.py @@ -0,0 +1,67 @@ +import urllib.request, ssl, json, http.cookiejar +ctx = ssl.create_default_context() +ctx.check_hostname = False +ctx.verify_mode = ssl.CERT_NONE +cj = http.cookiejar.CookieJar() +opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ctx), urllib.request.HTTPCookieProcessor(cj)) + +# 1. 登录 testuser2 +login_payload = json.dumps({"account": "testuser2", "password": "Password123!"}).encode("utf-8") +req = urllib.request.Request("https://47.108.78.12/api/v1/login", data=login_payload, headers={ + "Content-Type": "application/json", + "Host": "mail.solit.cn", + "Origin": "https://mail.solit.cn" +}) +resp = opener.open(req, timeout=5) +data = json.loads(resp.read().decode()) +print("Testuser2 login resp:", data.get("errorNo"), data.get("errorMsg")) + +csrf_token = "" +for c in cj: + if c.name == "smail_csrf": + csrf_token = c.value + +headers = { + "Content-Type": "application/json", + "Host": "mail.solit.cn", + "Origin": "https://mail.solit.cn", + "Referer": "https://mail.solit.cn/", + "X-CSRF-Token": csrf_token +} + +# 2. 如果需要强制修改密码,先修改密码 +if data.get("data", {}).get("must_change_password"): + print("Testuser2 must change password, updating...") + pwd_payload = json.dumps({ + "current_password": "Password123!", + "password": "UserPass88!" + }).encode("utf-8") + req_chg = urllib.request.Request("https://47.108.78.12/api/v1/settings/modify_password", data=pwd_payload, headers=headers) + resp_chg = opener.open(req_chg, timeout=5) + print("Change password resp:", resp_chg.read().decode()) + +# 3. 设置密保问题 +q_payload = json.dumps({ + "question": "毕业学校", + "answer": "清华大学" +}).encode("utf-8") +req_q = urllib.request.Request("https://47.108.78.12/api/v1/security/credentials/question", data=q_payload, headers=headers) +resp_q = opener.open(req_q, timeout=5) +print("Set question resp:", resp_q.read().decode()) + +# 4. 生成一组离线恢复码 (/api/v1/security/credentials/recovery-codes/generate) +codes_payload = json.dumps({"count": 5}).encode("utf-8") +req_c = urllib.request.Request("https://47.108.78.12/api/v1/security/credentials/recovery-codes/generate", data=codes_payload, headers=headers) +resp_c = opener.open(req_c, timeout=5) +c_data = json.loads(resp_c.read().decode()) +print("Generate recovery codes resp errorNo:", c_data.get("errorNo")) +print("Codes generated:", c_data.get("data", {}).get("codes")) +if c_data.get("data", {}).get("codes"): + with open("test/tools/testuser2_codes.json", "w", encoding="utf-8") as f: + json.dump({ + "account": "testuser2", + "password": "UserPass88!", + "question": "毕业学校", + "answer": "清华大学", + "codes": c_data["data"]["codes"] + }, f, ensure_ascii=False, indent=2) diff --git a/test/tools/update_remote_email_config.py b/test/tools/update_remote_email_config.py new file mode 100644 index 00000000..5878b2fb --- /dev/null +++ b/test/tools/update_remote_email_config.py @@ -0,0 +1,23 @@ +import paramiko +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect("47.108.78.12", port=22, username="root", password="10102442Lb!", timeout=10) + +cmd = """python3 -c ' +import json +with open("/opt/smail/config/email.json", "r") as f: + d = json.load(f) +d["policy"]["externalRecoveryEnabled"] = True +with open("/opt/smail/config/email.json", "w") as f: + json.dump(d, f, indent=2) +' +""" +stdin, stdout, stderr = ssh.exec_command(cmd) +print("Stderr:", stderr.read().decode()) + +stdin, stdout, stderr = ssh.exec_command("cat /opt/smail/config/email.json") +print("email.json:\n", stdout.read().decode()) + +stdin, stdout, stderr = ssh.exec_command("killall -9 smail") +print("Killall:", stdout.read().decode()) +ssh.close() diff --git a/test/tools/writer.py b/test/tools/writer.py new file mode 100644 index 00000000..df424b26 --- /dev/null +++ b/test/tools/writer.py @@ -0,0 +1,2 @@ +import sys, binascii +open(sys.argv[1], 'wb').write(binascii.unhexlify(sys.argv[2])) diff --git a/test/write_helper.py b/test/write_helper.py new file mode 100644 index 00000000..b72f1c7b --- /dev/null +++ b/test/write_helper.py @@ -0,0 +1,2 @@ +import sys, binascii +open(sys.argv[1], 'wb').write(binascii.unhexlify(sys.argv[2]))