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
25 changes: 25 additions & 0 deletions docs-developers/frontend/connect-and-addresses.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,31 @@ home pick is dropped the moment the device changes network, before anything else
sent. Any new code path that talks to a home address must keep this order.
:::

### Pairing at a self-signed server's home HTTP

A server on its own (self-signed) certificate also answers plain HTTP to the home
network ([Home HTTP](../server/configuration.md#home-http-tlshome_http)), and its pairing
links carry `home=http://<host:port>` beside an `https://` base, plus the server's id as
`sid=` (`parsePairingScan` reads it into `PairingScan.serverId`). The app can't trust
the certificate, so `useLinkPairing` exchanges at the base first and, only when that
couldn't connect (`withFallback`: never after an `ApiError`, the server's own answer),
asks `pairingFallback` whether to try the home address once. It says yes only when all
of these hold: native (an https page can't call http), not a timeout (a host that never
answered would only time out again), the base is https and the home is plain http on
the **same host and port** (an absent port is 443 for https, 80 for http), the link
carries a `sid`, and a tokenless `GET <home>/api/v1/server` answers with exactly that
id. The exchange's own `server_id` must match too. The connection is stored with the
https base as `serverUrl` (it fails safely by TLS on another network) and the home as
its address, and `finishConnect` sets the home pick (`answeredAt`) before the connection
is stored, so its first requests go home; the address runner then keeps it by the usual
probe. Android allows cleartext for this (`expo-build-properties`
`android.usesCleartextTraffic`); iOS's default ATS allows local networking (IP
addresses, `.local` and single-label names), not a `.lan` or `.home.arpa` name.

When pairing or the address field can't reach an https server at all, the message
names a self-signed certificate as a likely cause (`selfSignedMayBeWhy`: native, https,
not a timeout, and no http retry was made).

### Where the pick lives (`src/api/address-route.ts`)

The pick is **in memory only**: `useAddressRoute` holds `{ [connectionId]: url }`;
Expand Down
36 changes: 28 additions & 8 deletions docs-developers/server/api/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,11 @@ Response `200` - the pairing payload (`PairingPayload` in `internal/api/qr.go`):
```json
{
"server_name": "AudioSilo",
"server_id": "kx8Qz1c7m2Vw0aB3dEfGh",
"base_url": "https://books.example.com",
"pairing_token": "3vJx0eKQm9WZbT5nR8sHc2fLdA7yUqPgVi4oXk1NwsE",
"uri": "audiosilo://connect?server=https%3A%2F%2Fbooks.example.com&token=3vJx0eKQ…",
"web_url": "https://books.example.com/web/connect?token=3vJx0eKQ…",
"uri": "audiosilo://connect?server=https%3A%2F%2Fbooks.example.com&token=3vJx0eKQ…&sid=kx8Qz1c7m2Vw0aB3dEfGh",
"web_url": "https://books.example.com/web/connect?token=3vJx0eKQ…&sid=kx8Qz1c7m2Vw0aB3dEfGh",
"qr_png_data_uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg…",
"links": {
"web": "https://books.example.com/web",
Expand All @@ -211,7 +212,11 @@ app via Universal/App Links on claimed domains, else the embedded web player);
`uri` is the custom-scheme equivalent for an explicit "Open in app" action.
`links.ios`/`links.android` (store links) are omitted until the store apps ship.
`base_url` honors the configured `public_url`, falling back to the request host.
`server_name` is the server's display name, as `GET /server`'s `name`.
`server_name` is the server's display name, as `GET /server`'s `name`, and
`server_id` its stable id, as `GET /server`'s `server_id`. Both links end with that id as
`sid=` (after the address params below; absent only on a server without an id, which
the launcher always mints), so a client can check who answers at an address before
sending the token there.
`addresses` is the server's [home and away addresses](#home-and-away-addresses), omitted
when it has neither; both links then carry them too (below).

Expand All @@ -224,7 +229,7 @@ answers, and [`GET /addresses`](#get-apiv1addresses) carry the same object:

| Field | Type | Meaning |
|---|---|---|
| `home` | string, optional | The server's address on the household network: the configured [`lan_url`](../configuration.md#home-address-lan_url), else the address **this request** arrived on (scheme + `Host`) when that is a home-network host and the request did not come through a proxy ([the rule](../configuration.md#home-address-lan_url)) |
| `home` | string, optional | The server's address on the household network: the configured [`lan_url`](../configuration.md#home-address-lan_url), else the address **this request** arrived on (scheme + `Host`) when that is a home-network host and the request did not come through a proxy ([the rule](../configuration.md#home-address-lan_url)). On a self-signed server with [home HTTP](../configuration.md#home-http-tlshome_http) the derived one is always `http://`, on the request's port |
| `away` | string, optional | The address that works from anywhere: the configured `public_url` |

A `home` equal to the `away` is dropped. On the pairing payloads and the sign-in answers
Expand All @@ -236,11 +241,25 @@ The two links carry the same addresses as extra query parameters **after** the e
ones, only those that are set, so an older client ignores them:

```text
audiosilo://connect?server=https%3A%2F%2Fbooks.example.com&token=…&away=https%3A%2F%2Fbooks.example.com&home=http%3A%2F%2F192.168.1.20%3A8080
https://books.example.com/web/connect?token=…&away=https%3A%2F%2Fbooks.example.com&home=http%3A%2F%2F192.168.1.20%3A8080
audiosilo://connect?server=https%3A%2F%2Fbooks.example.com&token=…&away=https%3A%2F%2Fbooks.example.com&home=http%3A%2F%2F192.168.1.20%3A8080&sid=kx8Qz1c7m2Vw0aB3dEfGh
https://books.example.com/web/connect?token=…&away=https%3A%2F%2Fbooks.example.com&home=http%3A%2F%2F192.168.1.20%3A8080&sid=kx8Qz1c7m2Vw0aB3dEfGh
```

Read them by name: the encoder sorts them (`away` before `home`).
Read them by name: the encoder sorts them (`away` before `home`), and `sid` comes last.

On a self-signed server with [home HTTP](../configuration.md#home-http-tlshome_http) (the
default), the port also answers plain HTTP to home-network clients, and a derived `home`
is `http://` even when the request came over https: a device pairing at
`https://192.168.1.20:8080` gets `home` `http://192.168.1.20:8080` (an https request
without a port gets `http://<host>:443`, the port that answers). `base_url`, `web_url`
and the `uri`'s `server=` stay https, even for a pairing asked for over plain HTTP
(the https base on the same port, `:80` for a Host without one), so a link reads
`…server=https%3A%2F%2F192.168.1.20%3A8080&token=…&home=http%3A%2F%2F192.168.1.20%3A8080&sid=…`.
A native app that can't trust the certificate at `server` exchanges the token at that
`home` instead, when it has the same host and port and a tokenless `GET <home>/api/v1/server`
answers with the link's `sid` (the token then travels in cleartext, so it must reach this
server and no other box at the same private address); the player's rule is in
[Connect and home/away addresses](../../frontend/connect-and-addresses.md).

A client must not send a token to a `home` address until a tokenless
`GET <home>/api/v1/server` there has answered with the same `server_id`: a private
Expand Down Expand Up @@ -6073,6 +6092,7 @@ and `AUDIOSILO_*` variable each one is, and whether it needs a restart (see
"network": {
"bind": "0.0.0.0:8080",
"tls_mode": "selfsigned",
"tls_home_http": true,
"tls_hosts": [],
"trusted_proxies": ["10.0.0.2/32"],
"cors_origins": []
Expand Down Expand Up @@ -6112,7 +6132,7 @@ and `AUDIOSILO_*` variable each one is, and whether it needs a restart (see
},
"locked": { "players.web_dir": "AUDIOSILO_WEB_DIR" },
"restart_settings": [
"network.bind", "network.tls_mode", "network.tls_hosts", "players.web_dir",
"network.bind", "network.tls_mode", "network.tls_home_http", "network.tls_hosts", "players.web_dir",
"metadata.base_url", "metadata.mode", "demo.enabled", "demo.idle_ttl", "backups.dir"
],
"restart_pending": []
Expand Down
7 changes: 7 additions & 0 deletions docs-developers/server/auth-and-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,13 @@ book's audio and cover as no-cors `<audio>`/`<img>` requests, which `same-site`
would make the browser block. It reaches one book's media only. **HSTS is set only in `autocert` TLS mode**:
pinning it with a self-signed cert would make the certificate warning
impossible to bypass, and behind a reverse proxy (`off`) the proxy owns HSTS.
That also keeps [home HTTP](configuration.md#home-http-tlshome_http) working: on a
self-signed server the port answers plain HTTP to clients whose **connection** comes
from loopback, or from a private or link-local address with a home-network Host (never
judged by `X-Forwarded-For`), and redirects every other plain HTTP client to https.
Sign-ins on the home network then travel unencrypted, the documented trade-off for apps
that can't trust a self-signed certificate; the Host check keeps internet clients that a
NAT or proxy gateway hands over from a private address on https.

CORS (`cors` middleware) is a strict allow-list from `cors_origins`; with no
configured origins, cross-origin browser requests simply get no CORS headers
Expand Down
50 changes: 49 additions & 1 deletion docs-developers/server/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ and where it appears: [Home and away addresses](api/reference.md#home-and-away-a
single-label name. A bare IPv6 host is written in brackets in the address. Never loopback (`127.0.0.0/8`, `::1`, `localhost`): no other device can reach it.
Never carrier-grade NAT space (`100.64.0.0/10`, which Go's `IsPrivate` leaves out): it
is the ISP's network, not the household's. The scheme follows the same rule as the
pairing `base_url` (`https` on a TLS connection, else `http`), and `X-Forwarded-*` is
pairing `base_url` (`https` on a TLS connection, else `http`), except on a server that
serves [home HTTP](#home-http-tlshome_http) (`config.ServesHomeHTTP`): there a derived
home is always `http://`, even for a request that came over https, and keeps the
request's port (`:443` is added when the `Host` has none, since that port is the one
answering http, never 80). A configured `lan_url` is used as it is. `X-Forwarded-*` is
not trusted. A request that came through a reverse proxy (it carries `Forwarded` or
any `X-Forwarded-*` header) never derives a home address: its `Host` is the proxy's
upstream, not an address a device can use.
Expand All @@ -112,6 +116,7 @@ plain `http` behind TLS). Set `lan_url` to give the apps a home address; a confi
| `tls.mode` | `off` \| `selfsigned` \| `autocert`, default `selfsigned` | `off` = plain HTTP (use behind a TLS-terminating proxy); `selfsigned` = generate & persist a self-signed cert (LAN default); `autocert` = Let's Encrypt via ACME |
| `tls.hosts` | []string, `[]` | autocert: hostnames to obtain certificates for. **Required when mode is `autocert`** |
| `tls.cache_dir` | string, `<data>/certs` | autocert: certificate cache directory (re-defaulted to `<data>/certs` whenever empty) |
| `tls.home_http` | bool, `true` | selfsigned only: the bind port also answers plain HTTP to home-network clients, see [Home HTTP](#home-http-tlshome_http). Ignored in the other modes |
| `tls.cert_file` / `tls.key_file` | string, `""` | selfsigned: explicit paths for the persisted cert/key. Empty → `<data>/selfsigned-cert.pem` / `<data>/selfsigned-key.pem` |

Mode-related behaviors worth knowing: HSTS
Expand All @@ -121,6 +126,47 @@ bypass), and with `off` the proxy owns HSTS. In `autocert` mode the server
logs a warning when `bind` is not on port 443, since ACME validation will
fail otherwise.

#### Home HTTP (`tls.home_http`)

The native apps can't trust a self-signed certificate (there is no "proceed anyway" in
an app), so in `selfsigned` mode with `tls.home_http` on (the default;
`AUDIOSILO_TLS_HOME_HTTP`, console `network.tls_home_http`, restart) the bind port
serves both protocols. `internal/server`:

- `splitListener` accepts each connection and, in its own goroutine (a silent client
never blocks the accept loop), reads the first byte under the 10 s header timeout:
`0x16` (a TLS handshake record) makes it a `*tls.Conn`, anything else stays plain. A
connection that sends nothing in time is closed, and so is one still being read when
the server shuts down. One `http.Server` serves both kinds with the usual timeouts
(ALPN offers `h2` and `http/1.1`, as `ServeTLS` would).
- `homeHTTPGate` serves a plain HTTP request only when the **connection's own** remote
address (`r.RemoteAddr`; never `X-Forwarded-For`, which the client writes) is loopback,
or is `config.HomeNetworkIP` (private RFC 1918 / IPv6 ULA, link-local; not CGNAT
`100.64.0.0/10`) **and** the request's Host is a home-network host
(`config.HomeNetworkHost`: a private IP, a `.local`/`.lan`/`.home.arpa`/`.internal` or
single-label name). Any other plain request gets a `308` to the same URL on https (same
Host and port, `:80` for a Host without one), or a `400` when its Host can't make that
URL. TLS requests pass untouched.

Home HTTP is off, whatever `home_http` says, when `tls.cert_file` is set: a certificate
the admin supplied may be one the devices trust (a private CA, mkcert), so they keep https.

The server logs at start that plain HTTP is served to home-network clients. The derived
home address becomes `http://` (see [Home address](#home-address-lan_url)), so a device
paired over the https QR still learns an address it can use. No HSTS is sent on
self-signed, and no CSP carries `upgrade-insecure-requests`, so the console and the web
player also work over http on the LAN (without the secure-context features: the service
worker, the clipboard API).

:::note Gateways and the source address
Docker Desktop (Mac/Windows), rootless Docker, Docker's userland proxy (e.g. for IPv6
connections to a published port) and routers that rewrite a forwarded port's source hand
the server connections from a private address, such as `172.17.0.1`, whoever made them.
The Host check is what keeps an internet client on https there: it names the server by
its public name or address. A client that sends a home-network Host from outside (a
forged one) only chooses cleartext for itself.
:::

### Web player & app links

| Key | Type / default | Meaning |
Expand Down Expand Up @@ -414,6 +460,7 @@ by the settings table in `internal/config/settings.go` (`fields`: each entry's
| `AUDIOSILO_WEB_DIR` | `web_dir` | path |
| `AUDIOSILO_TLS_MODE` | `tls.mode` | `off` / `selfsigned` / `autocert` |
| `AUDIOSILO_TLS_HOSTS` | `tls.hosts` | comma-separated list |
| `AUDIOSILO_TLS_HOME_HTTP` | `tls.home_http` | `strconv.ParseBool` (`false`/`0`/… turns it off) |
| `AUDIOSILO_TRUSTED_PROXIES` | `trusted_proxies` | comma-separated CIDRs |
| `AUDIOSILO_CORS_ORIGINS` | `cors_origins` | comma-separated origins |
| `AUDIOSILO_MAX_UPLOAD_BYTES` | `max_upload_bytes` | integer |
Expand Down Expand Up @@ -467,6 +514,7 @@ restart, and whether the console may change it at all:
| `general.session_days` | `activity.session_days` | `AUDIOSILO_SESSION_DAYS` | at the next daily retention run |
| `network.bind` | `bind` | `AUDIOSILO_BIND` | restart |
| `network.tls_mode` | `tls.mode` | `AUDIOSILO_TLS_MODE` | restart |
| `network.tls_home_http` | `tls.home_http` | `AUDIOSILO_TLS_HOME_HTTP` | restart |
| `network.tls_hosts` | `tls.hosts` | `AUDIOSILO_TLS_HOSTS` | restart |
| `network.trusted_proxies` | `trusted_proxies` | `AUDIOSILO_TRUSTED_PROXIES` | at once |
| `network.cors_origins` | `cors_origins` | `AUDIOSILO_CORS_ORIGINS` | at once |
Expand Down
4 changes: 3 additions & 1 deletion docs-developers/server/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,9 @@ HTTP transport **only**: routing (`api.go` is the full route table), middleware
### `internal/server`

The HTTP(S) server itself: TLS modes (`off` for reverse proxies, `selfsigned`,
`autocert`/Let's Encrypt) and graceful shutdown. `Certificates` reads the
`autocert`/Let's Encrypt), home HTTP on the self-signed port (`splitListener` +
`homeHTTPGate`, see [Home HTTP](configuration.md#home-http-tlshome_http)) and
graceful shutdown. `Certificates` reads the
served certificates back from their files for Health > System.

### `internal/updates`
Expand Down
14 changes: 12 additions & 2 deletions docs-users/admin/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ apps (the same three choices as [Remote access](../getting-started/remote-access
ports 80 and 443 open to the internet, and shows **Certificate names**: one
name per line, like `books.example.com`, each pointing at this server.

With **Self-signed** chosen, the card also shows **Plain HTTP at home**, on out
of the box. The phone apps can't trust a self-signed certificate, so while it is
on the server's port also answers plain HTTP to devices on your home network
(anything else is sent to HTTPS), and that is how the apps connect at home.
Turn it off and the apps can't connect to a self-signed server. See
[Plain HTTP at home](../getting-started/remote-access.md#plain-http-at-home-how-the-phone-apps-connect),
including what it means for your network. It has no effect when you give the
server your own certificate (`tls.cert_file`). `tls.home_http` in `config.yaml` or
`AUDIOSILO_TLS_HOME_HTTP` sets it too, and then locks it here.

The **Certificate** row says how the certificate the server serves now looks:
"Valid" with the days left, "Expires in *N* days" (under two weeks), "Expired",
"Not issued yet" (Let's Encrypt hasn't issued one yet), or "None (plain HTTP)".
Expand All @@ -146,8 +156,8 @@ The **Network** card:
browser, one per line (like `http://localhost:8081`). Usually empty; see
[Settings you can leave alone](../getting-started/remote-access.md#settings-you-can-leave-alone).

The HTTPS mode, the certificate names and the listen address wait for a
restart; trusted proxies and allowed origins apply at once.
The HTTPS mode, plain HTTP at home, the certificate names and the listen
address wait for a restart; trusted proxies and allowed origins apply at once.

### Players & app links

Expand Down
2 changes: 1 addition & 1 deletion docs-users/getting-started/quickstart-docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ By default the server speaks HTTPS with a self-signed certificate, so browse to:
https://your-server:8080
```

Your browser will warn about the certificate the first time - that's expected with the default setup; accept it to continue. [Remote access](./remote-access.md) covers getting a proper certificate or running behind a reverse proxy.
Your browser will warn about the certificate the first time - that's expected with the default setup; accept it to continue. The phone apps can't accept that warning, so at home they connect over plain HTTP instead, which the server offers to devices on your home network ([plain HTTP at home](./remote-access.md#plain-http-at-home-how-the-phone-apps-connect), including a note on Docker Desktop). [Remote access](./remote-access.md) covers getting a proper certificate or running behind a reverse proxy.

- **`/`** - the connect page, where a listener enters an auth code to pair a device.
- **`/admin`** - the admin console. Log in as `admin` with the printed password.
Expand Down
Loading
Loading