Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions assets/nginx/00-devkit-tuning.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# canton-devkit: http-context tuning, included ahead of the role server
# blocks via nginx's `include /etc/nginx/conf.d/*.conf` (the `00-` prefix
# sorts it first). This file carries NO server{} block — its directives
# land directly in the http context.
#
# DevKit's role-scoped vhosts (<service>.<role>.<instance>.localhost,
# e.g. grpc-ledger-api.app-provider.localnet-2.localhost) are longer than
# nginx's default 64-byte server-name hash bucket, so without this nginx
# fails to boot with:
# [emerg] could not build server_names_hash, you should increase
# server_names_hash_bucket_size: 64
server_names_hash_bucket_size 128;
27 changes: 18 additions & 9 deletions assets/nginx/app-provider.conf
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# canton-devkit: server_name values are role-scoped instance vhosts of the
# form <service>.<role>.<localnet-name>.localhost (role = app-provider here),
# injected via ${VHOST_*_APP_PROVIDER} env vars by WriteNginxVhostOverlay
# (e.g. wallet.app-provider.localnet-2.localhost). The flat Splice names
# (wallet.localhost, ans.localhost, ...) are intentionally NOT served —
# DevKit advertises only the instance-scoped names so URLs stay unambiguous
# across concurrently running localnets.
#
# NOTE: *.localhost resolves to 127.0.0.1 in browsers, curl, and Go, but NOT
# in the JVM/Node/Python resolvers (and some Rust HTTP clients). Programmatic
# clients on those runtimes must send an explicit `Host:` header (HTTP) or
# `:authority:` pseudo-header (gRPC), or add an /etc/hosts entry.
server {
listen ${APP_PROVIDER_UI_PORT};
server_name ans.localhost;
server_name ${VHOST_ANS_APP_PROVIDER};
location /api/validator {
rewrite ^\/(.*) /$1 break;
proxy_pass http://splice:3${VALIDATOR_ADMIN_API_PORT_SUFFIX}/api/validator;
Expand All @@ -10,7 +22,8 @@ server {
}
}

# Deprecated, use json-ledger-api.localhost instead
# Deprecated, use the json-ledger-api vhost instead. Left on the flat
# canton.localhost name (upstream-deprecated; not worth instance-scoping).
server {
listen ${APP_PROVIDER_UI_PORT};
server_name canton.localhost;
Expand All @@ -22,7 +35,7 @@ server {

server {
listen ${APP_PROVIDER_UI_PORT};
server_name json-ledger-api.localhost;
server_name ${VHOST_JSON_LEDGER_APP_PROVIDER};
location / {
proxy_pass http://canton:3${PARTICIPANT_JSON_API_PORT_SUFFIX};
include /etc/nginx/includes/cors-headers.conf;
Expand All @@ -31,7 +44,7 @@ server {

server {
listen ${APP_PROVIDER_UI_PORT} http2;
server_name grpc-ledger-api.localhost;
server_name ${VHOST_GRPC_LEDGER_APP_PROVIDER};
location / {
grpc_pass grpc://canton:3${PARTICIPANT_LEDGER_API_PORT_SUFFIX};
}
Expand All @@ -40,11 +53,7 @@ server {

server {
listen ${APP_PROVIDER_UI_PORT};
# canton-devkit: `localhost` added so the bare host URL DevKit advertises
# (http://localhost:${APP_PROVIDER_UI_PORT}) routes to the wallet. Upstream
# only listed `wallet.localhost`, so the bare Host fell through to the first
# server block on this port (ans.localhost) and served the name service.
server_name localhost wallet.localhost;
server_name ${VHOST_WALLET_APP_PROVIDER};

# Reverse proxy for /api/validator
location /api/validator {
Expand Down
26 changes: 18 additions & 8 deletions assets/nginx/app-user.conf
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# canton-devkit: server_name values are role-scoped instance vhosts of the
# form <service>.<role>.<localnet-name>.localhost (role = app-user here),
# injected via ${VHOST_*_APP_USER} env vars by WriteNginxVhostOverlay
# (e.g. wallet.app-user.localnet-2.localhost). The flat Splice names
# (wallet.localhost, ans.localhost, ...) are intentionally NOT served —
# DevKit advertises only the instance-scoped names so URLs stay unambiguous
# across concurrently running localnets.
#
# NOTE: *.localhost resolves to 127.0.0.1 in browsers, curl, and Go, but NOT
# in the JVM/Node/Python resolvers (and some Rust HTTP clients). Programmatic
# clients on those runtimes must send an explicit `Host:` header (HTTP) or
# `:authority:` pseudo-header (gRPC), or add an /etc/hosts entry.
server {
listen ${APP_USER_UI_PORT};
server_name ans.localhost;
server_name ${VHOST_ANS_APP_USER};
location /api/validator {
rewrite ^\/(.*) /$1 break;
proxy_pass http://splice:2${VALIDATOR_ADMIN_API_PORT_SUFFIX}/api/validator;
Expand All @@ -10,7 +22,8 @@ server {
}
}

# Deprecated, use json-ledger-api.localhost instead
# Deprecated, use the json-ledger-api vhost instead. Left on the flat
# canton.localhost name (upstream-deprecated; not worth instance-scoping).
server {
listen ${APP_USER_UI_PORT};
server_name canton.localhost;
Expand All @@ -22,7 +35,7 @@ server {

server {
listen ${APP_USER_UI_PORT};
server_name json-ledger-api.localhost;
server_name ${VHOST_JSON_LEDGER_APP_USER};
location / {
proxy_pass http://canton:2${PARTICIPANT_JSON_API_PORT_SUFFIX};
include /etc/nginx/includes/cors-headers.conf;
Expand All @@ -31,18 +44,15 @@ server {

server {
listen ${APP_USER_UI_PORT} http2;
server_name grpc-ledger-api.localhost;
server_name ${VHOST_GRPC_LEDGER_APP_USER};
location / {
grpc_pass grpc://canton:2${PARTICIPANT_LEDGER_API_PORT_SUFFIX};
}
}

server {
listen ${APP_USER_UI_PORT};
# `localhost` already routes to the app-user wallet upstream; kept here so
# DevKit's embedded config matches the app-provider/sv fix and the bare host
# URL keeps working.
server_name localhost wallet.localhost;
server_name ${VHOST_WALLET_APP_USER};

# Reverse proxy for /api/validator
location /api/validator {
Expand Down
31 changes: 20 additions & 11 deletions assets/nginx/sv.conf
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
# canton-devkit: server_name values are instance-scoped vhosts. The SV
# node's own UIs are single-per-instance (scan.<localnet-name>.localhost via
# ${VHOST_SCAN}, sv.<localnet-name>.localhost via ${VHOST_SV}); the sv-role
# wallet is role-scoped (wallet.sv.<localnet-name>.localhost via
# ${VHOST_WALLET_SV}). All injected as env vars by WriteNginxVhostOverlay.
# The flat Splice names (wallet.localhost, scan.localhost, sv.localhost, ...)
# are intentionally NOT served — DevKit advertises only the instance-scoped
# names so URLs stay unambiguous across concurrently running localnets.
#
# NOTE: *.localhost resolves to 127.0.0.1 in browsers, curl, and Go, but NOT
# in the JVM/Node/Python resolvers (and some Rust HTTP clients). Programmatic
# clients on those runtimes must send an explicit `Host:` header (HTTP) or
# `:authority:` pseudo-header (gRPC), or add an /etc/hosts entry.
server {
listen ${SV_UI_PORT};
# canton-devkit: dropped `localhost` from this catch-all. Upstream had
# `server_name localhost _;` which claimed the bare host URL DevKit
# advertises (http://localhost:${SV_UI_PORT}) and tried to serve a static
# dir (/usr/share/nginx/sv-html) that does not exist in the image, so the
# sv wallet URL 404'd. `_` alone keeps the status/CORS default server.
# Catch-all default server: keeps the stub_status endpoint reachable and
# absorbs unmatched Host headers. `_` (not `localhost`) so the bare host
# URL does not fall through to the non-existent /usr/share/nginx/sv-html
# static dir (which 404'd before PR #279).
server_name _;

location = /status {
Expand All @@ -23,7 +35,7 @@ server {

server {
listen ${SV_UI_PORT};
server_name sv.localhost;
server_name ${VHOST_SV};

location /api/sv {
rewrite ^\/(.*) /$1 break;
Expand All @@ -36,7 +48,7 @@ server {

server {
listen ${SV_UI_PORT};
server_name scan.localhost;
server_name ${VHOST_SCAN};

location /api/scan {
rewrite ^\/(.*) /$1 break;
Expand All @@ -53,10 +65,7 @@ server {

server {
listen ${SV_UI_PORT};
# canton-devkit: `localhost` added so the bare host URL DevKit advertises
# (http://localhost:${SV_UI_PORT}) routes to the sv wallet instead of the
# static catch-all above.
server_name localhost wallet.localhost;
server_name ${VHOST_WALLET_SV};

# Reverse proxy for /api/validator
location /api/validator {
Expand Down
18 changes: 10 additions & 8 deletions internal/canton/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,22 @@ func localnetParticipantEndpoint() string {
//
// CRITICAL — the URL scheme/host matters for nginx routing on the SV
// tenant. The /api/scan/* path only matches when the request Host
// header is `scan.localhost` (see Splice conf/nginx/sv.conf — there
// are multiple `server_name` blocks under one listen port; only the
// `scan.localhost` block proxies to the splice scan app). Hitting
// `http://localhost:<sv-port>/api/scan/v0/dso` returns the
// header is the instance-scoped `scan.<instance>.localhost` (see
// assets/nginx/sv.conf — there are multiple `server_name` blocks under
// one listen port; only the scan block proxies to the splice scan app,
// and DevKit serves only the instance-scoped name). This harness runs
// against `localnet up dev`, so the host is `scan.dev.localhost`.
// Hitting `http://localhost:<sv-port>/api/scan/v0/dso` returns the
// sv-html static index (200 with HTML body) because the default
// server_name block serves the SV UI assets.
//
// The Go http.Client uses the URL's host as the Host header by
// default, so the right form is:
//
// export CANTON_DEVKIT_TEST_SCAN_URL=http://scan.localhost:<sv-port>
// export CANTON_DEVKIT_TEST_SCAN_URL=http://scan.dev.localhost:<sv-port>
//
// `scan.localhost` resolves to 127.0.0.1 per RFC 6761; verified on
// macOS + Linux. To find the SV UI host port on a running instance:
// `scan.dev.localhost` resolves to 127.0.0.1 in Go's resolver. To find
// the SV UI host port on a running instance:
//
// docker port <project>-nginx <SV_UI_PORT-from-compose-env>
//
Expand All @@ -81,7 +83,7 @@ func localnetScanBaseURL() string {
if v := os.Getenv("CANTON_DEVKIT_TEST_SCAN_URL"); v != "" {
return v
}
return "http://scan.localhost:4000"
return "http://scan.dev.localhost:4000"
}

// devLocalNetTokenSource returns a TokenSource that signs JWTs with the
Expand Down
14 changes: 8 additions & 6 deletions internal/canton/registry/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,12 @@ type DialOptions struct {
// HostHeader, when non-empty, overrides the HTTP Host header sent
// on every request (req.Host). Needed for Splice LocalNet's nginx
// virtual-host routing: the scan app's `/registry` routes are gated
// behind `server_name scan.localhost`, so a request to the SV UI
// port must carry `Host: scan.localhost` to reach the scan
// upstream rather than the SV-info default vhost. On a real DevNet
// (where scan has its own DNS name) this stays empty.
// behind an instance-scoped `server_name scan.<instance>.localhost`,
// so a request to the SV UI port must carry a matching
// `Host: scan.<instance>.localhost` header to reach the scan
// upstream rather than the SV-info default vhost (DevKit's
// token.resolveRegistryURL supplies this). On a real DevNet (where
// scan has its own DNS name) this stays empty.
HostHeader string

// Version selects the token-standard transfer-instruction registry
Expand Down Expand Up @@ -173,8 +175,8 @@ func (c *Client) doJSON(ctx context.Context, method, path string, body, into any
req.Header.Set("Content-Type", "application/json")
}
// Host-header override for nginx virtual-host routing (LocalNet
// scan registry sits behind `server_name scan.localhost`). Setting
// req.Host — not req.Header.Set("Host", …) — is the correct knob;
// scan registry sits behind `server_name scan.<instance>.localhost`).
// Setting req.Host — not req.Header.Set("Host", …) — is the correct knob;
// net/http reads the Host field, not the header map, for the
// request line's authority.
if c.hostHeader != "" {
Expand Down
31 changes: 17 additions & 14 deletions internal/canton/registry/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,28 @@
// # CRITICAL — Host header / virtual-host routing
//
// Splice's nginx config defines multiple `server_name` blocks under one
// listen port. The /api/scan/* routes only match when the request Host
// header is `scan.localhost`; /api/validator/* needs `wallet.localhost`
// (per-tenant routing). With `Host: localhost` (the implicit default
// when BaseURL is `http://localhost:PORT`), requests fall through to
// the default server block which serves the tenant's SPA HTML — your
// 200-OK response will be `<!DOCTYPE html>` instead of JSON, and the
// decode error message is opaque.
// listen port. On DevKit LocalNet the blocks are instance-scoped: the
// /api/scan/* routes only match Host `scan.<instance>.localhost`;
// /api/validator/* needs `wallet.<instance>.localhost` (per-tenant
// routing). With `Host: localhost` (the implicit default when BaseURL
// is `http://localhost:PORT`), requests fall through to the default
// server block which serves the tenant's SPA HTML — your 200-OK
// response will be `<!DOCTYPE html>` instead of JSON, and the decode
// error message is opaque.
//
// Pass the right virtual host via BaseURL. The `.localhost` TLD
// resolves to 127.0.0.1 per RFC 6761 (verified on macOS + Linux):
// Pass the right virtual host via BaseURL, e.g. for instance "dev":
//
// registry.Dial(registry.DialOptions{
// BaseURL: "http://scan.localhost:<sv-port>", // for /api/scan/*
// BaseURL: "http://wallet.localhost:<tenant-port>", // for /api/validator/*
// BaseURL: "http://scan.dev.localhost:<sv-port>", // for /api/scan/*
// BaseURL: "http://wallet.dev.localhost:<tenant-port>", // for /api/validator/*
// })
//
// If `.localhost` resolution fails in your environment (rare; some
// container runtimes strip it), use IP + the manual Host-header
// workaround via a wrapping http.RoundTripper in DialOptions.HTTPClient.
// *.localhost resolves to 127.0.0.1 in browsers, curl, and Go, but NOT
// in the JVM/Node/Python resolvers (and some Rust HTTP clients). Go's
// net resolver handles the `.localhost` suffix, so this client works
// out of the box; from other runtimes set the Host header explicitly
// (or add an /etc/hosts entry) via a wrapping http.RoundTripper in
// DialOptions.HTTPClient.
//
// # Auth
//
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/localnet/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ func TestEnv_ShellOutputIsPosixQuoted(t *testing.T) {
// Participant Ledger/JSON API ports a dApp dials directly.
"export CANTON_PARTICIPANT_LEDGER_APP_USER_PORT='2901'",
"export CANTON_PARTICIPANT_JSON_APP_USER_PORT='2975'",
// Scan UI surfaced explicitly with the scan.localhost vhost.
"export CANTON_SCAN_UI_URL='http://scan.localhost:4480'",
// Scan UI surfaced explicitly with the instance-scoped scan vhost.
"export CANTON_SCAN_UI_URL='http://scan.demo.localhost:4480'",
"export CANTON_SV_JWT='<redacted>'",
"export CANTON_SV_USER='sv-user'",
"export CANTON_SV_AUDIENCE='sv-aud'",
Expand Down
Loading
Loading