Files
mtgodot-poc/tools/40250/register_server.py
T
shenleiandClaude Opus 5.5 3e3708ef06 android-native: auto hunt, in-client account registration, mobile UI polish
- Auto hunt (client-only): PythonPlayerAutoHunt.cpp + AutoHuntScript.inc as the
  embedded mt_autohunt module, AUTO touch button, F9/F8.
- Account registration: tools/40250/register_server.py (rc.d mt_register, :11080)
  inserts into account.account; net.RegisterAccount/GetRegisterAccountResult
  (PythonAccountRegister.cpp) and the mt_register login-window dialog
  (RegisterScript.inc).
- --py-exec also runs in --login-screen mode for scripted login-screen tests.
- Touch controller, window manager and render command updates for the
  Android native client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:15:04 +09:00

131 lines
4.6 KiB
Python

#!/usr/local/bin/python3.9
"""Account registration service for the 40250 server (PORT: 40250 has no in-client registration).
POST /register with a form body `login=..&password=..&social_id=..` inserts a row into
account.account the same way the existing accounts are stored (password = MySQL PASSWORD()).
The reply is one plain-text line the client shows directly: `OK` or `ERR <CODE>`, where CODE is
BAD_LOGIN, BAD_PASSWORD, BAD_SOCIAL_ID, EXISTS, RATE_LIMIT, BAD_REQUEST or SERVER.
Deployed to /usr/metin2/server/register/ and started by /usr/local/etc/rc.d/mt_register.
"""
import argparse
import re
import subprocess
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs
LOGIN_RE = re.compile(r"^[A-Za-z0-9]{4,16}$") # LoginWindow ID_EditLine input_limit 16
PASSWORD_RE = re.compile(r"^[\x21-\x7e]{4,16}$") # printable ASCII, Password_EditLine input_limit 16
SOCIAL_ID_RE = re.compile(r"^[0-9]{7}$") # the delete code introselect asks for
RATE_WINDOW = 3600
RATE_MAX = 5 # successful registrations per IP per hour
MAX_BODY = 1024
_rate_lock = threading.Lock()
_rate = {} # ip -> [timestamps of successful registrations]
def log(msg):
sys.stdout.write(time.strftime("%Y-%m-%d %H:%M:%S ") + msg + "\n")
sys.stdout.flush()
def rate_limited(ip):
now = time.time()
with _rate_lock:
hits = [t for t in _rate.get(ip, []) if now - t < RATE_WINDOW]
_rate[ip] = hits
return len(hits) >= RATE_MAX
def rate_record(ip):
with _rate_lock:
_rate.setdefault(ip, []).append(time.time())
def create_account(login, password, social_id):
# Every value is validated above; the password additionally travels as hex so no quoting
# can leak into the statement.
sql = ("INSERT INTO account.account (login, password, social_id, status, create_time, last_play) "
"VALUES ('%s', PASSWORD(UNHEX('%s')), '%s', 'OK', NOW(), NOW())"
% (login, password.encode("ascii").hex(), social_id))
r = subprocess.run(["/usr/local/bin/mysql", "-uroot", "-N", "-e", sql], capture_output=True, text=True, timeout=10)
if r.returncode == 0:
return "OK"
if "Duplicate entry" in r.stderr or "ERROR 1062" in r.stderr:
return "ERR EXISTS"
log("mysql error: " + r.stderr.strip())
return "ERR SERVER"
class Handler(BaseHTTPRequestHandler):
server_version = "mt-register/1"
def log_message(self, fmt, *args):
pass
def reply(self, code, text):
body = (text + "\n").encode("ascii")
self.send_response(code)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/ping":
self.reply(200, "OK")
else:
self.reply(404, "ERR BAD_REQUEST")
def do_POST(self):
ip = self.client_address[0]
if self.path != "/register":
return self.reply(404, "ERR BAD_REQUEST")
try:
n = int(self.headers.get("Content-Length", "0"))
except ValueError:
n = -1
if n < 0 or n > MAX_BODY:
return self.reply(400, "ERR BAD_REQUEST")
try:
form = parse_qs(self.rfile.read(n).decode("ascii"), keep_blank_values=True)
except (UnicodeDecodeError, ValueError):
return self.reply(400, "ERR BAD_REQUEST")
login = form.get("login", [""])[0]
password = form.get("password", [""])[0]
social_id = form.get("social_id", [""])[0]
if not LOGIN_RE.match(login):
result = "ERR BAD_LOGIN"
elif not PASSWORD_RE.match(password):
result = "ERR BAD_PASSWORD"
elif not SOCIAL_ID_RE.match(social_id):
result = "ERR BAD_SOCIAL_ID"
elif rate_limited(ip):
result = "ERR RATE_LIMIT"
else:
result = create_account(login.lower(), password, social_id)
if result == "OK":
rate_record(ip)
log("%s register %s -> %s" % (ip, login, result))
self.reply(200, result)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--bind", default="0.0.0.0")
ap.add_argument("--port", type=int, default=11080)
args = ap.parse_args()
httpd = ThreadingHTTPServer((args.bind, args.port), Handler)
log("listening on %s:%d" % (args.bind, args.port))
httpd.serve_forever()
if __name__ == "__main__":
main()