开始任务:超级管理员支持安全中心通过外部邮箱验证码修改密码
This commit is contained in:
parent
0abb06baaf
commit
1344cacfe2
BIN
backend/out/linux_amd64/smail
Normal file
BIN
backend/out/linux_amd64/smail
Normal file
Binary file not shown.
BIN
backend/out/smail
Normal file
BIN
backend/out/smail
Normal file
Binary file not shown.
8
test/generate_files.py
Normal file
8
test/generate_files.py
Normal file
@ -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')
|
||||
18
test/run_deploy.py
Normal file
18
test/run_deploy.py
Normal file
@ -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()
|
||||
7
test/tools/cat_remote_configs.py
Normal file
7
test/tools/cat_remote_configs.py
Normal file
@ -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()
|
||||
9
test/tools/check_auth_codes.py
Normal file
9
test/tools/check_auth_codes.py
Normal file
@ -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()
|
||||
12
test/tools/check_db_full.py
Normal file
12
test/tools/check_db_full.py
Normal file
@ -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()
|
||||
|
||||
12
test/tools/check_emails_10.py
Normal file
12
test/tools/check_emails_10.py
Normal file
@ -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()
|
||||
|
||||
12
test/tools/check_err24.py
Normal file
12
test/tools/check_err24.py
Normal file
@ -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()
|
||||
|
||||
9
test/tools/check_external.py
Normal file
9
test/tools/check_external.py
Normal file
@ -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()
|
||||
12
test/tools/check_inbox_query.py
Normal file
12
test/tools/check_inbox_query.py
Normal file
@ -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()
|
||||
|
||||
13
test/tools/check_log_send.py
Normal file
13
test/tools/check_log_send.py
Normal file
@ -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()
|
||||
|
||||
|
||||
7
test/tools/check_remote_services.py
Normal file
7
test/tools/check_remote_services.py
Normal file
@ -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()
|
||||
19
test/tools/check_row22.py
Normal file
19
test/tools/check_row22.py
Normal file
@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
0
test/tools/check_security_state.py
Normal file
0
test/tools/check_security_state.py
Normal file
12
test/tools/check_srv.py
Normal file
12
test/tools/check_srv.py
Normal file
@ -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()
|
||||
|
||||
12
test/tools/check_ue.py
Normal file
12
test/tools/check_ue.py
Normal file
@ -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()
|
||||
|
||||
29
test/tools/debug_ssh.py
Normal file
29
test/tools/debug_ssh.py
Normal file
@ -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()
|
||||
41
test/tools/decrypt_external.py
Normal file
41
test/tools/decrypt_external.py
Normal file
@ -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()
|
||||
11
test/tools/dump_emails.py
Normal file
11
test/tools/dump_emails.py
Normal file
@ -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()
|
||||
6
test/tools/dump_mod_struct.py
Normal file
6
test/tools/dump_mod_struct.py
Normal file
@ -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]))
|
||||
7
test/tools/dump_req_ver.py
Normal file
7
test/tools/dump_req_ver.py
Normal file
@ -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])
|
||||
7
test/tools/dump_settings.py
Normal file
7
test/tools/dump_settings.py
Normal file
@ -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'))
|
||||
7
test/tools/dump_settings_imports.py
Normal file
7
test/tools/dump_settings_imports.py
Normal file
@ -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'))
|
||||
6
test/tools/dump_settings_target.py
Normal file
6
test/tools/dump_settings_target.py
Normal file
@ -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])
|
||||
8
test/tools/find_req_ver.py
Normal file
8
test/tools/find_req_ver.py
Normal file
@ -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
|
||||
36
test/tools/fix_remote_config_digest.py
Normal file
36
test/tools/fix_remote_config_digest.py
Normal file
@ -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()
|
||||
0
test/tools/inspect_remote.py
Normal file
0
test/tools/inspect_remote.py
Normal file
9
test/tools/inspect_schema.py
Normal file
9
test/tools/inspect_schema.py
Normal file
@ -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()
|
||||
12
test/tools/make_files.py
Normal file
12
test/tools/make_files.py
Normal file
@ -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))
|
||||
13
test/tools/patch_delivery.py
Normal file
13
test/tools/patch_delivery.py
Normal file
@ -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')
|
||||
13
test/tools/patch_load_user.py
Normal file
13
test/tools/patch_load_user.py
Normal file
@ -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')
|
||||
25
test/tools/patch_recovery_service.py
Normal file
25
test/tools/patch_recovery_service.py
Normal file
@ -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')
|
||||
69
test/tools/patch_settings_go.py
Normal file
69
test/tools/patch_settings_go.py
Normal file
@ -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')
|
||||
17
test/tools/patch_store.py
Normal file
17
test/tools/patch_store.py
Normal file
@ -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')
|
||||
11
test/tools/query_arbitrary.py
Normal file
11
test/tools/query_arbitrary.py
Normal file
@ -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()
|
||||
16
test/tools/query_emails.py
Normal file
16
test/tools/query_emails.py
Normal file
@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
11
test/tools/query_remote_details.py
Normal file
11
test/tools/query_remote_details.py
Normal file
@ -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()
|
||||
11
test/tools/query_remote_tables.py
Normal file
11
test/tools/query_remote_tables.py
Normal file
@ -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()
|
||||
9
test/tools/query_remote_testuser.py
Normal file
9
test/tools/query_remote_testuser.py
Normal file
@ -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()
|
||||
9
test/tools/query_remote_users.py
Normal file
9
test/tools/query_remote_users.py
Normal file
@ -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()
|
||||
22
test/tools/run_check_ext.py
Normal file
22
test/tools/run_check_ext.py
Normal file
@ -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()
|
||||
23
test/tools/run_check_hex.py
Normal file
23
test/tools/run_check_hex.py
Normal file
@ -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()
|
||||
30
test/tools/run_check_hex2.py
Normal file
30
test/tools/run_check_hex2.py
Normal file
@ -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()
|
||||
10
test/tools/run_check_hex3.py
Normal file
10
test/tools/run_check_hex3.py
Normal file
@ -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()
|
||||
28
test/tools/run_check_logs.py
Normal file
28
test/tools/run_check_logs.py
Normal file
@ -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()
|
||||
27
test/tools/run_check_names.py
Normal file
27
test/tools/run_check_names.py
Normal file
@ -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()
|
||||
31
test/tools/run_check_users.py
Normal file
31
test/tools/run_check_users.py
Normal file
@ -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()
|
||||
70
test/tools/run_decrypt_creds.py
Normal file
70
test/tools/run_decrypt_creds.py
Normal file
@ -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()
|
||||
146
test/tools/run_forgot_password_acceptance.py
Normal file
146
test/tools/run_forgot_password_acceptance.py
Normal file
@ -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('=== 忘记密码全流程端到端测试结束 ===')
|
||||
32
test/tools/send_test_email.py
Normal file
32
test/tools/send_test_email.py
Normal file
@ -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()
|
||||
51
test/tools/test_client_credentials.py
Normal file
51
test/tools/test_client_credentials.py
Normal file
@ -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())
|
||||
8
test/tools/test_echo.py
Normal file
8
test/tools/test_echo.py
Normal file
@ -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()
|
||||
40
test/tools/test_edit_user_pwd.py
Normal file
40
test/tools/test_edit_user_pwd.py
Normal file
@ -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())
|
||||
7
test/tools/test_find_req.py
Normal file
7
test/tools/test_find_req.py
Normal file
@ -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'))
|
||||
91
test/tools/test_forgot_password_e2e.py
Normal file
91
test/tools/test_forgot_password_e2e.py
Normal file
@ -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()
|
||||
35
test/tools/test_generate_codes_api.py
Normal file
35
test/tools/test_generate_codes_api.py
Normal file
@ -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)
|
||||
17
test/tools/test_inbound_info.py
Normal file
17
test/tools/test_inbound_info.py
Normal file
@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
9
test/tools/test_list_ext.py
Normal file
9
test/tools/test_list_ext.py
Normal file
@ -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'))
|
||||
0
test/tools/test_roles_ui.py
Normal file
0
test/tools/test_roles_ui.py
Normal file
0
test/tools/test_smtpdm_ports.py
Normal file
0
test/tools/test_smtpdm_ports.py
Normal file
20
test/tools/test_testuser2_login.py
Normal file
20
test/tools/test_testuser2_login.py
Normal file
@ -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}")
|
||||
51
test/tools/test_validate_api.py
Normal file
51
test/tools/test_validate_api.py
Normal file
@ -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)
|
||||
14
test/tools/test_validate_remote.py
Normal file
14
test/tools/test_validate_remote.py
Normal file
@ -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)
|
||||
13
test/tools/testuser2_credential_info.json
Normal file
13
test/tools/testuser2_credential_info.json
Normal file
@ -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"
|
||||
]
|
||||
}
|
||||
60
test/tools/testuser2_setup_security.py
Normal file
60
test/tools/testuser2_setup_security.py
Normal file
@ -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)
|
||||
67
test/tools/testuser2_setup_security_v2.py
Normal file
67
test/tools/testuser2_setup_security_v2.py
Normal file
@ -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)
|
||||
23
test/tools/update_remote_email_config.py
Normal file
23
test/tools/update_remote_email_config.py
Normal file
@ -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()
|
||||
2
test/tools/writer.py
Normal file
2
test/tools/writer.py
Normal file
@ -0,0 +1,2 @@
|
||||
import sys, binascii
|
||||
open(sys.argv[1], 'wb').write(binascii.unhexlify(sys.argv[2]))
|
||||
2
test/write_helper.py
Normal file
2
test/write_helper.py
Normal file
@ -0,0 +1,2 @@
|
||||
import sys, binascii
|
||||
open(sys.argv[1], 'wb').write(binascii.unhexlify(sys.argv[2]))
|
||||
Loading…
Reference in New Issue
Block a user