#!/usr/bin/env python3
"""
POC server demonstrating that Chrome resolves payment method manifest
`default_applications` against the merchant-supplied pre-redirect URL
instead of the actual manifest location.

Layout simulating a payment provider on a single origin:
  /merchant.html      - merchant page that constructs PaymentRequest
  /v1/pay             - LEGACY endpoint, 301-redirects to /v2/pay
  /v2/pay             - current endpoint, Link header -> /v2/manifest.json
  /v2/manifest.json   - {"default_applications": ["app.json"]}  (RELATIVE)
  /v2/app.json        - the CORRECT web-app manifest (per W3C spec)
  /v1/app.json        - the WRONG  web-app manifest (stale/attacker-influenced)

The merchant supplies supportedMethods = "http://localhost:9876/v1/pay".
Per the W3C Payment Method Manifest spec, "app.json" must be resolved
against the manifest's own URL (/v2/manifest.json -> /v2/app.json).
Chrome instead resolves it against the merchant-supplied /v1/pay,
producing /v1/app.json.
"""

import http.server
import json
import sys
import threading

HOST, PORT = "127.0.0.1", 9876
RESULT_PATH = "request-log.txt"

log_lock = threading.Lock()
log_file = open(RESULT_PATH, "w")


def log(line):
    with log_lock:
        print(line, file=sys.stderr, flush=True)
        log_file.write(line + "\n")
        log_file.flush()


MERCHANT_HTML = b"""<!doctype html>
<meta charset="utf-8">
<title>merchant</title>
<pre id="out">running...</pre>
<script>
(async () => {
  const out = document.getElementById('out');
  try {
    // Merchant chooses the LEGACY /v1/pay path. The provider 301s it to /v2/pay.
    const req = new PaymentRequest(
      [{ supportedMethods: 'http://localhost:9876/v1/pay' }],
      { total: { label: 'x', amount: { currency: 'USD', value: '1' } } }
    );
    // Init() in the browser process is sent on construction; this just keeps
    // the request alive and gives the crawler time to run.
    const r = await req.hasEnrolledInstrument();
    out.textContent = 'hasEnrolledInstrument = ' + r;
  } catch (e) {
    out.textContent = 'error: ' + e;
  }
})();
</script>
"""

PAYMENT_METHOD_MANIFEST = json.dumps({
    # RELATIVE reference. Spec says: resolve against the manifest URL
    # (/v2/manifest.json) -> /v2/app.json.
    "default_applications": ["app.json"]
}).encode()


def webapp_manifest(tag):
    return json.dumps({
        "name": tag,
        "icons": [{
            "src": "http://localhost:9876/%s/icon.png" % tag.split("-")[0],
            "sizes": "64x64",
            "type": "image/png"
        }],
        "serviceworker": {
            "src": "http://localhost:9876/%s/sw.js" % tag.split("-")[0],
            "scope": "http://localhost:9876/%s/" % tag.split("-")[0],
            "use_cache": False
        },
        "payment": {
            "supported_delegations": ["shippingAddress"]
        }
    }).encode()


class Handler(http.server.BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def log_message(self, fmt, *args):
        pass  # silence default access log

    # ---- request routing -------------------------------------------------

    def do_HEAD(self):
        self._dispatch("HEAD")

    def do_GET(self):
        self._dispatch("GET")

    def _dispatch(self, method):
        path = self.path
        log("[server] %-4s %s" % (method, path))

        if path == "/merchant.html":
            return self._send(200, MERCHANT_HTML, "text/html")

        if path == "/v1/pay":
            # Legacy path -> permanent redirect to current path.
            self.send_response(301)
            self.send_header("Location", "/v2/pay")
            self.send_header("Content-Length", "0")
            self.end_headers()
            return

        if path == "/v2/pay":
            # Current payment-method endpoint advertises its manifest.
            self.send_response(200)
            self.send_header(
                "Link", '</v2/manifest.json>; rel="payment-method-manifest"')
            self.send_header("Content-Type", "text/html")
            self.send_header("Content-Length", "0")
            self.end_headers()
            return

        if path == "/v2/manifest.json":
            return self._send(200, PAYMENT_METHOD_MANIFEST,
                              "application/json")

        if path == "/v1/app.json":
            log("")
            log("==================================================================")
            log("[!!] BUG TRIGGERED: browser fetched /v1/app.json")
            log("[!!] 'app.json' was resolved against the merchant-supplied")
            log("[!!] pre-redirect URL /v1/pay instead of the manifest's own")
            log("[!!] location /v2/manifest.json.")
            log("==================================================================")
            log("")
            return self._send(200, webapp_manifest("v1-WRONG"),
                              "application/manifest+json")

        if path == "/v2/app.json":
            log("[ok] browser fetched /v2/app.json (spec-correct resolution)")
            return self._send(200, webapp_manifest("v2-CORRECT"),
                              "application/manifest+json")

        if path == "/favicon.ico":
            return self._send(404, b"", "text/plain")

        log("[server] 404 for %s" % path)
        return self._send(404, b"not found", "text/plain")

    def _send(self, code, body, ctype):
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        if self.command != "HEAD":
            self.wfile.write(body)


if __name__ == "__main__":
    httpd = http.server.ThreadingHTTPServer((HOST, PORT), Handler)
    log("[server] listening on http://%s:%d" % (HOST, PORT))
    log("[server] open http://localhost:%d/merchant.html" % PORT)
    httpd.serve_forever()
