# w3cs CPU server: single TLS origin for page + relay websockets.
# WebGPU and crypto.randomUUID need a secure context, and an https page
# may only open wss sockets, so nginx fronts both backends on one origin.
map $http_upgrade $connection_upgrade {
    default upgrade;
    ""      close;
}

# Seat number (from the /seatN/ named capture) to relay port.
# Seat N listens on 8144+N. Seat 6 is deliberately absent: 8150 is
# w3cs-web's port, so a /seat6 route would proxy the page server to itself.
map $seat_number $seat_port {
    1 8145;
    2 8146;
    3 8147;
    4 8148;
    5 8149;
}

# One live replay per public IP, the same policy the AWS video workers
# apply in their signaling server. A seat is busy exactly while its relay
# holds an open /signal WebSocket (that socket IS the seat claim; the
# WebTransport and standby planes cannot hold a seat without it), so
# counting those sockets per client address is the whole rule. Every seat
# is a separate relay process, and the page picks its own seat, so nginx
# is the one place that sees all seats at once.
#
# Addresses in w3cs-trusted-ips.conf map to an empty key, which limit_conn
# does not account: developers test from several tabs and browsers at a
# time. The file is box-local (not in the repo); one "a.b.c.d \"\";" line
# per address, CIDR ranges allowed. deploy-cpu-server.sh creates it empty
# when it is missing. This box is DNS-only behind Cloudflare (no proxy), so
# $binary_remote_addr is the real client.
geo $w3cs_viewer_key {
    default $binary_remote_addr;
    127.0.0.1 "";
    ::1 "";
    include /etc/nginx/w3cs-trusted-ips.conf;
}
limit_conn_zone $w3cs_viewer_key zone=w3cs_viewer:1m;
limit_conn_status 429;
limit_conn_log_level notice;

server {
    # http2 multiplexes the page's many small fetches (worker, renderer
    # modules, cast descriptors) over one connection on first load.
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name london-cpu-worker.war3replays.com;

    ssl_certificate     /etc/ssl/w3cs/w3cs.crt;
    ssl_certificate_key /etc/ssl/w3cs/w3cs.key;

    # Seat-holding signaling socket, one per client address (see the
    # w3cs_viewer zone). Listed before the generic seat route: regex
    # locations match in order of appearance. Only /signal is limited;
    # status probes and the data planes of the session that holds the
    # seat must keep working while the socket is open.
    location ~ ^/seat(?<seat_number>[1-5])/signal$ {
        limit_conn w3cs_viewer 1;
        proxy_pass http://127.0.0.1:$seat_port/signal$is_args$args;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_buffering off;
    }

    # Seats: /seatN/* fronts seat N's relay on 814N. The regex capture builds
    # the backend port; with variables in proxy_pass nginx does not append
    # the query string on its own.
    location ~ ^/seat(?<seat_number>[1-5])(?<seat_path>/.*)$ {
        proxy_pass http://127.0.0.1:$seat_port$seat_path$is_args$args;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_buffering off;
    }

    # Viewer gate probe for the page: a plain request accounted in the same
    # zone as the signaling sockets. While this address already holds a
    # seat the probe answers 429, so the page can say "one replay at a
    # time" instead of searching for seats it can never get. Trusted and
    # loopback addresses always pass. Not `return`: that answers in the
    # rewrite phase, before the preaccess phase where limit_conn counts
    # (verified live: a `return 204` probe passed beside a held socket).
    # empty_gif is a content handler, so the request reaches the limiter.
    location = /viewer-gate {
        limit_conn w3cs_viewer 1;
        add_header Cache-Control no-store always;
        empty_gif;
    }

    # Seat 1's signaling socket directly on the origin (old links and the
    # SSH-tunnel lab flow). Same per-address rule as /seat1/signal.
    location = /signal {
        limit_conn w3cs_viewer 1;
        proxy_pass http://127.0.0.1:8145;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_buffering off;
    }

    # Relay websockets (the ws-transport planes), seat 1 directly on the
    # origin for old links and the SSH-tunnel lab flow.
    location ~ ^/(reliable|frame|control)(/|$) {
        proxy_pass http://127.0.0.1:8145;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_buffering off;
    }

    # Content-addressed resource bundles served by the relay.
    location /resource-bundle/ {
        proxy_pass http://127.0.0.1:8145;
        proxy_read_timeout 120s;
    }

    # Test index, live page, web modules (no-store python server).
    location / {
        proxy_pass http://127.0.0.1:8150;
    }
}

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    # Let's Encrypt HTTP-01 challenges must stay on plain HTTP.
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}
