mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-06 02:48:25 +00:00
refactor(server): remove inbound TLS termination
Remove server-side TLS listener support so Fabro only binds plain TCP or Unix sockets, and update docs/tests around proxy-terminated HTTPS. This also drops the removed [server.listen.tls] config shape and the inbound TLS-specific diagnostics, fixtures, and integration coverage.
This commit is contained in:
parent
49ef284ed5
commit
914778c8a8
41 changed files with 406 additions and 625 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -1951,8 +1951,6 @@ dependencies = [
|
|||
"hmac",
|
||||
"http-body-util",
|
||||
"httpmock",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"jsonwebtoken",
|
||||
"mime_guess",
|
||||
|
|
@ -1960,9 +1958,6 @@ dependencies = [
|
|||
"object_store",
|
||||
"rand 0.9.4",
|
||||
"regex",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -1971,18 +1966,15 @@ dependencies = [
|
|||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"toml 0.8.23",
|
||||
"toml_edit",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"ulid",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"x509-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -93,8 +93,9 @@ Once `https://<FABRO_DOMAIN>/health` returns `ok`, two things to grab:
|
|||
2. **Point your local CLI at the server** — add the URL to `~/.fabro/settings.toml`:
|
||||
|
||||
```toml title="~/.fabro/settings.toml"
|
||||
[server]
|
||||
target = "https://fabro.example.com/api/v1"
|
||||
[cli.target]
|
||||
type = "http"
|
||||
url = "https://fabro.example.com/api/v1"
|
||||
```
|
||||
|
||||
Then commands like `fabro model list --server <url>` will hit your Droplet.
|
||||
|
|
@ -131,6 +132,6 @@ To pin a specific version instead of `:nightly`, edit `docker-compose.yaml` and
|
|||
Auth, dev tokens, submitting runs, and pointing the CLI at your deployment.
|
||||
</Card>
|
||||
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">
|
||||
Full `settings.toml` reference — TLS, auth methods, concurrency, and more.
|
||||
Full `settings.toml` reference — reverse-proxy TLS, auth methods, concurrency, and more.
|
||||
</Card>
|
||||
</Columns>
|
||||
|
|
|
|||
|
|
@ -75,8 +75,9 @@ Once the deploy is healthy, Fly exposes a `<app>.fly.dev` URL (or your custom do
|
|||
2. **Point your local CLI at the server** — add the Fly URL to `~/.fabro/settings.toml`:
|
||||
|
||||
```toml title="~/.fabro/settings.toml"
|
||||
[server]
|
||||
target = "https://<your-app>.fly.dev/api/v1"
|
||||
[cli.target]
|
||||
type = "http"
|
||||
url = "https://<your-app>.fly.dev/api/v1"
|
||||
```
|
||||
|
||||
Then commands like `fabro model list --server <url>` will hit your Fly instance.
|
||||
|
|
|
|||
|
|
@ -52,8 +52,9 @@ Once the deploy is healthy, Railway exposes a `*.up.railway.app` URL (or your cu
|
|||
2. **Point your local CLI at the server** — add the Railway URL to `~/.fabro/settings.toml`:
|
||||
|
||||
```toml title="~/.fabro/settings.toml"
|
||||
[server]
|
||||
target = "https://<your-service>.up.railway.app/api/v1"
|
||||
[cli.target]
|
||||
type = "http"
|
||||
url = "https://<your-service>.up.railway.app/api/v1"
|
||||
```
|
||||
|
||||
Then commands like `fabro model list --server <url>` will hit your Railway instance.
|
||||
|
|
@ -77,6 +78,6 @@ Railway re-pulls the GHCR image on every deploy. `Dockerfile.deploy` references
|
|||
Auth, dev tokens, submitting runs, and pointing the CLI at your deployment.
|
||||
</Card>
|
||||
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">
|
||||
Full `settings.toml` reference — TLS, auth methods, concurrency, and more.
|
||||
Full `settings.toml` reference — reverse-proxy TLS, auth methods, concurrency, and more.
|
||||
</Card>
|
||||
</Columns>
|
||||
|
|
|
|||
|
|
@ -58,8 +58,9 @@ Once the deploy is healthy, Render exposes a `*.onrender.com` URL (or your custo
|
|||
2. **Point your local CLI at the server** — add the Render URL to `~/.fabro/settings.toml`:
|
||||
|
||||
```toml title="~/.fabro/settings.toml"
|
||||
[server]
|
||||
target = "https://<your-service>.onrender.com/api/v1"
|
||||
[cli.target]
|
||||
type = "http"
|
||||
url = "https://<your-service>.onrender.com/api/v1"
|
||||
```
|
||||
|
||||
Then commands like `fabro model list --server <url>` will hit your Render instance.
|
||||
|
|
@ -83,6 +84,6 @@ Render re-pulls the GHCR image on every deploy. `Dockerfile.deploy` references t
|
|||
Auth, dev tokens, submitting runs, and pointing the CLI at your deployment.
|
||||
</Card>
|
||||
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">
|
||||
Full `settings.toml` reference — TLS, auth methods, concurrency, and more.
|
||||
Full `settings.toml` reference — reverse-proxy TLS, auth methods, concurrency, and more.
|
||||
</Card>
|
||||
</Columns>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Both interfaces use the same workflow engine, the same Graphviz files, and the s
|
|||
| **Events** | Printed to stderr | Streamed via SSE |
|
||||
| **Persistence** | Checkpoint files only | Persistent run store + checkpoint files |
|
||||
| **Web UI** | Not available | Full React interface |
|
||||
| **Authentication** | None | JWT and/or mTLS |
|
||||
| **Authentication** | None | Dev token and/or GitHub OAuth |
|
||||
|
||||
## Starting the server
|
||||
|
||||
|
|
@ -118,25 +118,26 @@ Send the `X-Fabro-Demo: 1` header on any API request to get static mock data wit
|
|||
The CLI can target a running Fabro server for commands that support a remote API. Configure `~/.fabro/settings.toml`:
|
||||
|
||||
```toml title="settings.toml"
|
||||
[server]
|
||||
target = "https://fabro.example.com:3000/api/v1"
|
||||
[cli.target]
|
||||
type = "http"
|
||||
url = "https://fabro.example.com/api/v1"
|
||||
```
|
||||
|
||||
Or use the `--server` flag:
|
||||
|
||||
```bash
|
||||
fabro model list --server https://fabro.example.com:3000/api/v1
|
||||
fabro model list --server https://fabro.example.com/api/v1
|
||||
```
|
||||
|
||||
`fabro model list` and `fabro model test` honor `[server].target` by default unless you explicitly pass `--storage-dir`. `fabro exec` remains a local agent session and only uses the server when you pass `--server`.
|
||||
`fabro model list` and `fabro model test` honor `[cli.target]` by default unless you explicitly pass `--storage-dir`. `fabro exec` remains a local agent session and only uses the server when you pass `--server`.
|
||||
|
||||
See [User Configuration](/reference/user-configuration#server-section) for the full connection options, including mTLS setup.
|
||||
See [User Configuration](/reference/user-configuration#cli.target-section) for the full connection options, including client certificates for proxy-terminated HTTPS endpoints.
|
||||
|
||||
## Next steps
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">
|
||||
Full settings.toml reference — authentication, TLS, run defaults, and more.
|
||||
Full settings.toml reference — authentication, reverse-proxy TLS, run defaults, and more.
|
||||
</Card>
|
||||
<Card title="Deploy to Railway" icon="train" href="/administration/deploy-railway">
|
||||
Step-by-step guide for deploying Fabro on Railway.
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ Fabro is single-tenant software designed for small, trusted teams. The following
|
|||
|
||||
### Authentication
|
||||
|
||||
- **Enable authentication.** Fabro supports GitHub OAuth and Tailscale header-based auth for the web app. Do not use `insecure_disabled` outside of local development.
|
||||
- **Configure a username allowlist.** Both GitHub and Tailscale auth support `allowed_usernames` in `settings.toml`. An empty allowlist rejects all requests.
|
||||
- **Use JWT to connect the web app to the API.** Configure `FABRO_JWT_PRIVATE_KEY` on the web app and `FABRO_JWT_PUBLIC_KEY` on the API server. JWT tokens are Ed25519-signed and short-lived (30 seconds).
|
||||
- **Use mTLS for machine-to-machine API access.** Configure `[api.tls]` in `settings.toml` with server cert, key, and CA. Set client auth to `Required` for programmatic clients (CI, scripts).
|
||||
- **Enable authentication.** Fabro supports `dev-token` and GitHub OAuth. Do not disable auth outside of local development or controlled demos.
|
||||
- **Configure a username allowlist for GitHub OAuth.** `[server.auth.github].allowed_usernames` should contain the exact GitHub users allowed to log in. An empty list rejects everyone.
|
||||
- **Configure the session secret used by the web flow.** `SESSION_SECRET` should be provisioned with a strong value on long-lived deployments. If you also provision `FABRO_JWT_PRIVATE_KEY` and `FABRO_JWT_PUBLIC_KEY`, treat them as server runtime secrets, but they are not what currently gates browser auth.
|
||||
- **Terminate HTTPS or mTLS upstream when needed.** Fabro's listener is plain HTTP/Unix only. If CI, scripts, or a browser must connect over HTTPS, terminate TLS at a reverse proxy or load balancer and keep the Fabro listener on a private network.
|
||||
|
||||
### Secrets
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ Legacy `server.toml`, `user.toml`, and `cli.toml` are ignored with a warning. Re
|
|||
|
||||
The CLI-only `[cli.*]` sections (including `[cli.target]`) belong in the client machine's `settings.toml`. They tell CLI commands how to reach a server. The server process does not read `[cli.*]` for its own binding or routing.
|
||||
|
||||
Fabro does not terminate inbound TLS directly. Bind `[server.listen]` to a Unix socket or plain TCP port, and terminate HTTPS or mTLS at a reverse proxy, load balancer, or platform ingress in front of Fabro. Use `[server.api].url` and `[server.web].url` for those external HTTPS URLs.
|
||||
|
||||
### Full reference
|
||||
|
||||
```toml title="settings.toml"
|
||||
|
|
@ -31,10 +33,6 @@ _version = 1
|
|||
type = "tcp"
|
||||
address = "0.0.0.0:3000"
|
||||
|
||||
[server.listen.tls]
|
||||
cert = "/etc/fabro/tls/cert.pem"
|
||||
key = "/etc/fabro/tls/key.pem"
|
||||
|
||||
[server.api]
|
||||
url = "https://fabro.example.com/api/v1"
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ The `fabro doctor` command validates your installation:
|
|||
```bash
|
||||
fabro doctor # Local config checks + live server diagnostics
|
||||
fabro doctor --verbose # Show detailed output for each check
|
||||
fabro doctor --server https://fabro.example.com:3000/api/v1
|
||||
fabro doctor --server https://fabro.example.com/api/v1
|
||||
```
|
||||
|
||||
It checks:
|
||||
|
|
@ -29,7 +29,7 @@ It checks:
|
|||
|
||||
**Port already in use** — Change the port with `fabro server start --port 3001` or stop the conflicting process.
|
||||
|
||||
**SSE streams disconnecting** — If using a reverse proxy, ensure buffering is disabled and the connection timeout is long enough for workflow runs. See the [reverse proxy example](/administration/deployment#binding-and-tls).
|
||||
**SSE streams disconnecting** — If using a reverse proxy, ensure buffering is disabled and the connection timeout is long enough for workflow runs. See the [DigitalOcean reverse-proxy example](/administration/deploy-digital-ocean).
|
||||
|
||||
**Run config validation errors** — Use `fabro preflight` to validate without executing:
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Demo mode is activated **per-request** by sending a header:
|
|||
X-Fabro-Demo: 1
|
||||
```
|
||||
|
||||
When the server receives this header, it routes the request to a parallel set of demo handlers that return static JSON instead of hitting the real backend. Authentication is bypassed — no JWT or mTLS credentials are needed.
|
||||
When the server receives this header, it routes the request to a parallel set of demo handlers that return static JSON instead of hitting the real backend. Authentication is bypassed — no credentials are needed.
|
||||
|
||||
Requests **without** the header are routed to the real API as usual, so demo and production traffic coexist on the same server.
|
||||
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ The Fabro API is a REST API for managing workflow runs, interactive sessions, an
|
|||
|
||||
## Base URL
|
||||
|
||||
The versioned API is served by `fabro server start`, which defaults to:
|
||||
By default, `fabro server start` listens on the Unix socket `~/.fabro/fabro.sock`. If you bind Fabro to TCP instead, the versioned API is served at a URL like:
|
||||
|
||||
```
|
||||
http://localhost:3000/api/v1
|
||||
```
|
||||
|
||||
The base URL is configurable via `settings.toml`:
|
||||
The advertised public API URL is configurable via `settings.toml`:
|
||||
|
||||
```toml title="settings.toml"
|
||||
[server.api]
|
||||
|
|
@ -63,19 +63,9 @@ When `"github"` is enabled, browser users can sign in through GitHub OAuth. Succ
|
|||
|
||||
When `[server.web].enabled = true`, the server requires `SESSION_SECRET` and issues a private `__fabro_session` cookie after successful login. The cookie is session transport only; the underlying bootstrap method remains `dev-token` or `github`, and that provenance is preserved in run metadata.
|
||||
|
||||
### HTTPS
|
||||
### HTTPS and Reverse Proxies
|
||||
|
||||
If you want HTTPS on the listener, configure shared TLS on `[server.listen.tls]`:
|
||||
|
||||
```toml title="settings.toml"
|
||||
[server.listen]
|
||||
type = "tcp"
|
||||
address = "0.0.0.0:3000"
|
||||
|
||||
[server.listen.tls]
|
||||
cert = "/etc/fabro/tls/cert.pem"
|
||||
key = "/etc/fabro/tls/key.pem"
|
||||
```
|
||||
Fabro's listener is plain HTTP (or a Unix socket) only. If you want a public HTTPS endpoint, terminate TLS at a reverse proxy, load balancer, or platform ingress and point it at Fabro's internal listener.
|
||||
|
||||
## Errors
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ CLI mode is ideal for:
|
|||
fabro server start
|
||||
```
|
||||
|
||||
`fabro server start` starts an HTTP server (default `127.0.0.1:3000`) with persistent run storage. Runs are submitted via the REST API and executed asynchronously.
|
||||
`fabro server start` starts an HTTP server, binding to `~/.fabro/fabro.sock` by default (or plain TCP when configured), with persistent run storage. Runs are submitted via the REST API and executed asynchronously. Public HTTPS, when needed, is terminated upstream by a reverse proxy or platform ingress.
|
||||
|
||||
### Configuration
|
||||
|
||||
|
|
@ -38,11 +38,11 @@ Key server config options:
|
|||
|
||||
| Setting | Description |
|
||||
|---|---|
|
||||
| `api.host` / `api.port` | Bind address (default `127.0.0.1:3000`) |
|
||||
| `api.authentication_strategies` | Auth methods: `jwt`, `mtls`, or both |
|
||||
| `api.tls` | Optional HTTPS with cert/key/CA paths |
|
||||
| `max_concurrent_runs` | Scheduler concurrency limit (default 5) |
|
||||
| `[llm]`, `[sandbox]`, `[vars]` | Defaults applied to every run (overridable per-run) |
|
||||
| `server.listen` | Bind transport: Unix socket or plain TCP listener |
|
||||
| `server.api.url` / `server.web.url` | External HTTPS URLs advertised to clients |
|
||||
| `server.auth.methods` | Bootstrap auth methods: `dev-token`, `github`, or both |
|
||||
| `server.scheduler.max_concurrent_runs` | Scheduler concurrency limit (default 5) |
|
||||
| `[run.*]` | Defaults applied to every run (overridable per-run) |
|
||||
|
||||
### Run lifecycle
|
||||
|
||||
|
|
@ -61,10 +61,10 @@ In API mode, human-in-the-loop questions are served over HTTP instead of termina
|
|||
|
||||
### Authentication
|
||||
|
||||
API mode supports two authentication strategies, configurable in `settings.toml`:
|
||||
API mode supports two bootstrap auth methods, configurable in `settings.toml`:
|
||||
|
||||
- **JWT** — EdDSA-signed tokens (used by the web UI)
|
||||
- **mTLS** — Mutual TLS with client certificates (used for service-to-service communication)
|
||||
- **`dev-token`** — Bearer token access for operators and automation
|
||||
- **`github`** — GitHub OAuth for browser users, resulting in a session cookie
|
||||
|
||||
### Demo mode
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ Demo mode is per-request: send the `X-Fabro-Demo: 1` HTTP header to get static m
|
|||
The web UI is a React app (`apps/fabro-web`) that connects to the API server. Start it alongside `fabro server start`:
|
||||
|
||||
```bash
|
||||
fabro server start # API on port 3000
|
||||
fabro server start # API on ~/.fabro/fabro.sock by default
|
||||
cd apps/fabro-web && bun run dev # rebuilds web assets on change; refresh the browser
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ name = "claude-sonnet-4-5"
|
|||
|
||||
[cli.target]
|
||||
type = "http"
|
||||
url = "https://fabro.example.com:3000/api/v1"
|
||||
url = "https://fabro.example.com/api/v1"
|
||||
```
|
||||
|
||||
`[cli.exec]` config applies to `fabro exec`. `[run.model]` sets the default workflow model/provider for commands like `fabro run` and `fabro preflight`. `[cli.target]` stores connection info for commands that can target a remote Fabro server.
|
||||
|
|
@ -767,7 +767,7 @@ Check environment and integration health. `fabro doctor` always performs live se
|
|||
```bash
|
||||
fabro doctor
|
||||
fabro doctor -v
|
||||
fabro doctor --server https://fabro.example.com:3000/api/v1
|
||||
fabro doctor --server https://fabro.example.com/api/v1
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ _version = 1
|
|||
|
||||
[cli.target]
|
||||
type = "http"
|
||||
url = "https://fabro.example.com:3000/api/v1"
|
||||
url = "https://fabro.example.com/api/v1"
|
||||
|
||||
[cli.target.tls]
|
||||
cert = "~/.fabro/tls/client.crt"
|
||||
|
|
@ -245,7 +245,7 @@ Connection info for commands that target a remote Fabro server.
|
|||
```toml title="settings.toml"
|
||||
[cli.target]
|
||||
type = "http"
|
||||
url = "https://fabro.example.com:3000/api/v1"
|
||||
url = "https://fabro.example.com/api/v1"
|
||||
```
|
||||
|
||||
| Key | Description |
|
||||
|
|
@ -257,14 +257,14 @@ url = "https://fabro.example.com:3000/api/v1"
|
|||
`fabro model` uses `[cli.target]` by default when no explicit `--storage-dir` is passed. An explicit `--server` flag overrides the configured target:
|
||||
|
||||
```bash
|
||||
fabro model list --server https://fabro.example.com:3000/api/v1
|
||||
fabro model list --server https://fabro.example.com/api/v1
|
||||
```
|
||||
|
||||
`fabro exec` does not automatically use `[cli.target]`. It only routes model traffic through a Fabro server when you pass `--server` for that invocation.
|
||||
|
||||
### `[cli.target.tls]` section
|
||||
|
||||
Optional mTLS configuration for authenticating with an HTTP target. When present, the CLI presents a client certificate during the TLS handshake.
|
||||
Optional client-certificate configuration for authenticating with an HTTP target. When present, the CLI presents a client certificate during the TLS handshake with your external HTTPS endpoint or reverse proxy.
|
||||
|
||||
```toml title="settings.toml"
|
||||
[cli.target.tls]
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@ use fabro_types::settings::server::{
|
|||
ServerAuthMethod, ServerAuthSettings, ServerIntegrationsLayer, ServerIntegrationsSettings,
|
||||
ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerIpAllowlistOverrideSettings,
|
||||
ServerIpAllowlistSettings, ServerLayer, ServerListenLayer, ServerListenSettings,
|
||||
ServerListenTlsLayer, ServerLoggingSettings, ServerSchedulerSettings, ServerSettings,
|
||||
ServerSlateDbLayer, ServerSlateDbSettings, ServerStorageLayer, ServerStorageSettings,
|
||||
ServerWebLayer, ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings,
|
||||
TlsConfig,
|
||||
ServerLoggingSettings, ServerSchedulerSettings, ServerSettings, ServerSlateDbLayer,
|
||||
ServerSlateDbSettings, ServerStorageLayer, ServerStorageSettings, ServerWebLayer,
|
||||
ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings,
|
||||
};
|
||||
use fabro_util::Home;
|
||||
|
||||
|
|
@ -18,7 +17,7 @@ use super::{ResolveError, default_interp, parse_socket_addr, require_interp};
|
|||
|
||||
pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> ServerSettings {
|
||||
let storage = resolve_storage(layer.storage.as_ref());
|
||||
let (listen, _valid_tls) = resolve_listen(layer.listen.as_ref(), errors);
|
||||
let listen = resolve_listen(layer.listen.as_ref(), errors);
|
||||
let web = resolve_web(layer.api.as_ref(), layer.web.as_ref());
|
||||
let auth = resolve_auth(layer.auth.as_ref(), errors);
|
||||
let ip_allowlist = resolve_ip_allowlist(layer.ip_allowlist.as_ref(), errors);
|
||||
|
|
@ -65,49 +64,27 @@ fn resolve_storage(layer: Option<&ServerStorageLayer>) -> ServerStorageSettings
|
|||
fn resolve_listen(
|
||||
layer: Option<&ServerListenLayer>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> (ServerListenSettings, bool) {
|
||||
) -> ServerListenSettings {
|
||||
match layer {
|
||||
None => (
|
||||
ServerListenSettings::Unix {
|
||||
path: default_interp(Home::from_env().socket_path()),
|
||||
},
|
||||
false,
|
||||
),
|
||||
Some(ServerListenLayer::Unix { path }) => (
|
||||
ServerListenSettings::Unix {
|
||||
path: path
|
||||
.clone()
|
||||
.unwrap_or_else(|| default_interp(Home::from_env().socket_path())),
|
||||
},
|
||||
false,
|
||||
),
|
||||
Some(ServerListenLayer::Tcp { address, tls }) => {
|
||||
None => ServerListenSettings::Unix {
|
||||
path: default_interp(Home::from_env().socket_path()),
|
||||
},
|
||||
Some(ServerListenLayer::Unix { path }) => ServerListenSettings::Unix {
|
||||
path: path
|
||||
.clone()
|
||||
.unwrap_or_else(|| default_interp(Home::from_env().socket_path())),
|
||||
},
|
||||
Some(ServerListenLayer::Tcp { address }) => {
|
||||
let address = parse_socket_addr(
|
||||
&require_interp(address.as_ref(), "server.listen.address", errors),
|
||||
"server.listen.address",
|
||||
errors,
|
||||
);
|
||||
let (tls, valid_tls) = resolve_tls(tls.as_ref(), errors);
|
||||
(ServerListenSettings::Tcp { address, tls }, valid_tls)
|
||||
ServerListenSettings::Tcp { address }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_tls(
|
||||
layer: Option<&ServerListenTlsLayer>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> (Option<TlsConfig>, bool) {
|
||||
let Some(layer) = layer else {
|
||||
return (None, false);
|
||||
};
|
||||
|
||||
let cert = require_interp(layer.cert.as_ref(), "server.listen.tls.cert", errors);
|
||||
let key = require_interp(layer.key.as_ref(), "server.listen.tls.key", errors);
|
||||
let valid = layer.cert.is_some() && layer.key.is_some();
|
||||
|
||||
(Some(TlsConfig { cert, key }), valid)
|
||||
}
|
||||
|
||||
fn resolve_web(_api: Option<&ServerApiLayer>, layer: Option<&ServerWebLayer>) -> ServerWebSettings {
|
||||
let layer = layer.expect("defaults.toml should provide server.web defaults");
|
||||
|
||||
|
|
|
|||
|
|
@ -27,10 +27,7 @@ _version = 1
|
|||
|
||||
[server.listen]
|
||||
type = "tcp"
|
||||
address = "127.0.0.1:3000"
|
||||
|
||||
[server.listen.tls]
|
||||
cert = "/tmp/server.pem"
|
||||
address = "not-a-socket-addr"
|
||||
|
||||
[server.auth]
|
||||
methods = ["github"]
|
||||
|
|
@ -50,7 +47,7 @@ provider = "not-a-provider"
|
|||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
assert!(rendered.contains("server.listen.tls.key"));
|
||||
assert!(rendered.contains("server.listen.address"));
|
||||
assert!(rendered.contains("server.auth.github.allowed_usernames"));
|
||||
assert!(rendered.contains("run.sandbox.provider"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,8 +65,8 @@ fn resolves_server_defaults_from_empty_settings() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn reports_tls_shape_errors() {
|
||||
let file = parse(
|
||||
fn parsing_rejects_inbound_listener_tls_configuration() {
|
||||
let err = fabro_config::parse_settings_layer(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
|
|
@ -76,19 +76,11 @@ address = "127.0.0.1:32276"
|
|||
|
||||
[server.listen.tls]
|
||||
cert = "/etc/fabro/server.pem"
|
||||
|
||||
"#,
|
||||
);
|
||||
)
|
||||
.expect_err("listener TLS should be rejected at parse time");
|
||||
|
||||
let errors = fabro_config::resolve_server_from_file(&file)
|
||||
.expect_err("incomplete tls config should fail");
|
||||
let rendered = errors
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
assert!(rendered.contains("server.listen.tls.key"));
|
||||
assert!(err.to_string().contains("unknown field `tls`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -47,14 +47,6 @@ tokio-stream = { workspace = true, features = ["sync"] }
|
|||
base64.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-rustls = "0.26"
|
||||
rustls = { version = "0.23", default-features = false, features = ["std", "ring"] }
|
||||
rustls-pemfile = "2"
|
||||
rustls-pki-types = "1"
|
||||
hyper = "1"
|
||||
hyper-util = { version = "0.1", features = ["tokio", "server-auto", "http1", "http2"] }
|
||||
tower-service = "0.3"
|
||||
x509-parser = "0.16"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml = "0.9"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::Engine as _;
|
||||
|
|
@ -47,37 +46,6 @@ fn decode_pem_value(name: &str, value: &str) -> Result<String, String> {
|
|||
String::from_utf8(bytes).map_err(|e| format!("{name} base64 decoded to invalid UTF-8: {e}"))
|
||||
}
|
||||
|
||||
fn validate_tls_cert(pem: &str, now_epoch: i64) -> Result<String, String> {
|
||||
let mut reader = std::io::Cursor::new(pem.as_bytes());
|
||||
let certs: Vec<_> = rustls_pemfile::certs(&mut reader)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("failed to parse certificate PEM: {e}"))?;
|
||||
if certs.is_empty() {
|
||||
return Err("no certificates found in PEM".to_string());
|
||||
}
|
||||
let (_, parsed) = x509_parser::parse_x509_certificate(&certs[0])
|
||||
.map_err(|e| format!("failed to parse X.509 certificate: {e}"))?;
|
||||
let not_after = parsed.validity().not_after.timestamp();
|
||||
if not_after <= now_epoch {
|
||||
return Err("certificate has expired".to_string());
|
||||
}
|
||||
let cn = parsed
|
||||
.subject()
|
||||
.iter_common_name()
|
||||
.next()
|
||||
.and_then(|cn| cn.as_str().ok())
|
||||
.unwrap_or("(no CN)");
|
||||
Ok(format!("CN={cn}, valid"))
|
||||
}
|
||||
|
||||
fn validate_tls_private_key(pem: &str) -> Result<(), String> {
|
||||
let mut reader = std::io::Cursor::new(pem.as_bytes());
|
||||
rustls_pemfile::private_key(&mut reader)
|
||||
.map_err(|e| format!("failed to parse private key PEM: {e}"))?
|
||||
.ok_or_else(|| "no private key found in PEM".to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_session_secret(value: &str) -> Result<(), String> {
|
||||
session_secret::validate_session_secret(value)
|
||||
}
|
||||
|
|
@ -492,11 +460,6 @@ async fn check_brave_search(state: &AppState) -> CheckResult {
|
|||
}
|
||||
|
||||
fn check_crypto(state: &AppState) -> CheckResult {
|
||||
let settings_file = state
|
||||
.settings
|
||||
.read()
|
||||
.expect("settings lock poisoned")
|
||||
.clone();
|
||||
let resolved_server_settings = state.server_settings();
|
||||
|
||||
let mut details = Vec::new();
|
||||
|
|
@ -559,40 +522,6 @@ fn check_crypto(state: &AppState) -> CheckResult {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(listen) = settings_file
|
||||
.server
|
||||
.as_ref()
|
||||
.and_then(|s| s.listen.as_ref())
|
||||
{
|
||||
use fabro_types::settings::server::ServerListenLayer;
|
||||
|
||||
if let ServerListenLayer::Tcp { tls: Some(tls), .. } = listen {
|
||||
let read = |raw: Option<String>, label: &str| -> Result<String, String> {
|
||||
let Some(path_str) = raw else {
|
||||
return Err(format!("server.listen.tls.{label} is not configured"));
|
||||
};
|
||||
let path = PathBuf::from(&path_str);
|
||||
let expanded = fabro_config::expand_tilde(&path);
|
||||
std::fs::read_to_string(&expanded)
|
||||
.map_err(|e| format!("{}: {e}", expanded.display()))
|
||||
};
|
||||
match (
|
||||
read(tls.cert.as_ref().map(InterpString::as_source), "cert"),
|
||||
read(tls.key.as_ref().map(InterpString::as_source), "key"),
|
||||
) {
|
||||
(Ok(cert_pem), Ok(key_pem)) => {
|
||||
if let Err(err) = validate_tls_cert(&cert_pem, chrono::Utc::now().timestamp()) {
|
||||
errors.push(err);
|
||||
}
|
||||
if let Err(err) = validate_tls_private_key(&key_pem) {
|
||||
errors.push(err);
|
||||
}
|
||||
}
|
||||
_ => errors.push("failed to read TLS files".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
CheckResult {
|
||||
name: "Crypto".to_string(),
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ pub mod server;
|
|||
mod server_secrets;
|
||||
mod settings_view;
|
||||
pub mod static_files;
|
||||
pub mod tls;
|
||||
pub mod web_auth;
|
||||
|
||||
pub use error::{ApiError, Error, Result};
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ use crate::server::{
|
|||
reconcile_incomplete_runs_on_startup, shutdown_active_workers, spawn_scheduler,
|
||||
};
|
||||
use crate::server_secrets::ServerSecrets;
|
||||
use crate::tls::{build_rustls_config, serve_tls_with_shutdown};
|
||||
|
||||
const TEST_IN_MEMORY_STORE_ENV: &str = "FABRO_TEST_IN_MEMORY_STORE";
|
||||
pub const DEFAULT_TCP_PORT: u16 = 32276;
|
||||
|
|
@ -246,7 +245,6 @@ fn bind_override_layer(bind: BindRequest) -> SettingsLayer {
|
|||
},
|
||||
BindRequest::Tcp(address) => ServerListenLayer::Tcp {
|
||||
address: Some(InterpString::parse(&address.to_string())),
|
||||
tls: None,
|
||||
},
|
||||
BindRequest::TcpHost(_) => {
|
||||
unreachable!("host-only bind requests are handled before building a settings override")
|
||||
|
|
@ -515,12 +513,6 @@ where
|
|||
}
|
||||
});
|
||||
|
||||
// Branch: TLS, plain TCP, or Unix socket
|
||||
let tls_settings = match &resolved_server_settings.listen {
|
||||
ServerListenSettings::Tcp { tls, .. } => tls.clone(),
|
||||
ServerListenSettings::Unix { .. } => None,
|
||||
};
|
||||
|
||||
let bound_listener = bind_listener(&bind_request).await?;
|
||||
let bind_addr = bound_listener.bind.clone();
|
||||
if bound_listener.used_random_port_fallback {
|
||||
|
|
@ -563,38 +555,19 @@ where
|
|||
|
||||
match bound_listener.listener {
|
||||
BoundListener::Unix(listener) => {
|
||||
if tls_settings.is_some() {
|
||||
warn!("TLS is configured but not supported on Unix sockets; ignoring TLS settings");
|
||||
}
|
||||
announce_server_ready(&bind_addr, styles);
|
||||
axum::serve(listener, router)
|
||||
.with_graceful_shutdown(wait_for_shutdown(shutdown_rx.clone()))
|
||||
.await?;
|
||||
}
|
||||
BoundListener::Tcp(listener) => {
|
||||
if let Some(ref tls_settings) = tls_settings {
|
||||
let rustls_config = build_rustls_config(tls_settings)?;
|
||||
let tls_acceptor = tokio_rustls::TlsAcceptor::from(rustls_config);
|
||||
|
||||
info!("TLS enabled");
|
||||
announce_server_ready(&bind_addr, styles);
|
||||
|
||||
serve_tls_with_shutdown(
|
||||
listener,
|
||||
tls_acceptor,
|
||||
router,
|
||||
wait_for_shutdown(shutdown_rx.clone()),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
announce_server_ready(&bind_addr, styles);
|
||||
axum::serve(
|
||||
listener,
|
||||
router.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.with_graceful_shutdown(wait_for_shutdown(shutdown_rx.clone()))
|
||||
.await?;
|
||||
}
|
||||
announce_server_ready(&bind_addr, styles);
|
||||
axum::serve(
|
||||
listener,
|
||||
router.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.with_graceful_shutdown(wait_for_shutdown(shutdown_rx.clone()))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,8 @@
|
|||
//!
|
||||
//! Per the requirements doc, only the transport bind needs redaction now:
|
||||
//!
|
||||
//! - `server.listen` — the whole subtree. Bind address reveals network
|
||||
//! topology; `[server.listen.tls]` cert/key paths reveal the host filesystem
|
||||
//! layout.
|
||||
//! - `server.listen` — the whole subtree. Bind addresses and socket paths
|
||||
//! reveal network topology and host filesystem layout.
|
||||
//!
|
||||
//! ## Why that's all
|
||||
//!
|
||||
|
|
@ -132,10 +131,6 @@ _version = 1
|
|||
[server.listen]
|
||||
type = "tcp"
|
||||
address = "127.0.0.1:32276"
|
||||
|
||||
[server.listen.tls]
|
||||
cert = "/etc/fabro/tls/cert.pem"
|
||||
key = "/etc/fabro/tls/key.pem"
|
||||
"#,
|
||||
);
|
||||
let redacted = redact_for_api(&settings);
|
||||
|
|
@ -249,10 +244,6 @@ _version = 1
|
|||
type = "tcp"
|
||||
address = "127.0.0.1:32276"
|
||||
|
||||
[server.listen.tls]
|
||||
cert = "/etc/fabro/tls/cert.pem"
|
||||
key = "/etc/fabro/tls/key.pem"
|
||||
|
||||
[server.auth]
|
||||
methods = ["github", "dev-token"]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
use std::future::Future;
|
||||
use std::path::Path;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::extract::ConnectInfo;
|
||||
use fabro_types::settings::{InterpString, TlsConfig};
|
||||
use rustls::ServerConfig;
|
||||
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use tokio::net::TcpListener;
|
||||
use tracing::error;
|
||||
|
||||
/// Build a rustls `ServerConfig` from the `[server.listen.tls]` configuration.
|
||||
pub fn build_rustls_config(tls_settings: &TlsConfig) -> anyhow::Result<Arc<ServerConfig>> {
|
||||
let cert = resolve_path(&tls_settings.cert)?;
|
||||
let key_path = resolve_path(&tls_settings.key)?;
|
||||
|
||||
let certs = load_certs(&cert);
|
||||
let key = load_private_key(&key_path);
|
||||
|
||||
let config = ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, key)
|
||||
.expect("invalid server certificate or key");
|
||||
|
||||
Ok(Arc::new(config))
|
||||
}
|
||||
|
||||
pub async fn serve_tls(
|
||||
listener: TcpListener,
|
||||
tls_acceptor: tokio_rustls::TlsAcceptor,
|
||||
router: axum::Router,
|
||||
) -> anyhow::Result<()> {
|
||||
serve_tls_with_shutdown(listener, tls_acceptor, router, std::future::pending()).await
|
||||
}
|
||||
|
||||
/// Serve requests over TLS until the supplied shutdown future resolves.
|
||||
pub async fn serve_tls_with_shutdown<F>(
|
||||
listener: TcpListener,
|
||||
tls_acceptor: tokio_rustls::TlsAcceptor,
|
||||
router: axum::Router,
|
||||
shutdown: F,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
F: Future<Output = ()> + Send,
|
||||
{
|
||||
use hyper::body::Incoming;
|
||||
use hyper::service::service_fn;
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use hyper_util::server::conn::auto::Builder;
|
||||
use tower_service::Service;
|
||||
|
||||
let builder = Builder::new(TokioExecutor::new());
|
||||
let mut shutdown = Pin::from(Box::new(shutdown));
|
||||
|
||||
loop {
|
||||
let accepted = tokio::select! {
|
||||
() = &mut shutdown => return Ok(()),
|
||||
accepted = listener.accept() => accepted?,
|
||||
};
|
||||
let (tcp_stream, remote_addr) = accepted;
|
||||
|
||||
let tls_acceptor = tls_acceptor.clone();
|
||||
let router = router.clone();
|
||||
let builder = builder.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let tls_stream = match tls_acceptor.accept(tcp_stream).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!(%remote_addr, "TLS handshake failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let io = TokioIo::new(tls_stream);
|
||||
|
||||
let service = service_fn(move |mut req: hyper::Request<Incoming>| {
|
||||
let mut router = router.clone();
|
||||
async move {
|
||||
req.extensions_mut().insert(ConnectInfo(remote_addr));
|
||||
router.call(req).await
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(e) = builder.serve_connection(io, service).await {
|
||||
error!(%remote_addr, "connection error: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub use fabro_config::expand_tilde;
|
||||
|
||||
fn resolve_path(value: &InterpString) -> anyhow::Result<std::path::PathBuf> {
|
||||
let resolved = value
|
||||
.resolve(|name| std::env::var(name).ok())
|
||||
.with_context(|| format!("failed to resolve {}", value.as_source()))?;
|
||||
Ok(expand_tilde(Path::new(&resolved.value)))
|
||||
}
|
||||
|
||||
fn load_certs(path: &Path) -> Vec<CertificateDer<'static>> {
|
||||
let path = expand_tilde(path);
|
||||
let file = std::fs::File::open(&path)
|
||||
.unwrap_or_else(|e| panic!("failed to open certificate file {}: {e}", path.display()));
|
||||
let mut reader = std::io::BufReader::new(file);
|
||||
rustls_pemfile::certs(&mut reader)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap_or_else(|e| panic!("failed to parse certificates from {}: {e}", path.display()))
|
||||
}
|
||||
|
||||
fn load_private_key(path: &Path) -> PrivateKeyDer<'static> {
|
||||
let path = expand_tilde(path);
|
||||
let file = std::fs::File::open(&path)
|
||||
.unwrap_or_else(|e| panic!("failed to open private key file {}: {e}", path.display()));
|
||||
let mut reader = std::io::BufReader::new(file);
|
||||
rustls_pemfile::private_key(&mut reader)
|
||||
.unwrap_or_else(|e| panic!("failed to parse private key from {}: {e}", path.display()))
|
||||
.unwrap_or_else(|| panic!("no private key found in {}", path.display()))
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBRjCB+aADAgECAhRKD83+hLEUl2GQUrsSgNaDBjPrpDAFBgMrZXAwETEPMA0G
|
||||
A1UEAwwGVGVzdENBMB4XDTI2MDQwNTE2MjEzM1oXDTM2MDQwMjE2MjEzM1owETEP
|
||||
MA0GA1UEAwwGVGVzdENBMCowBQYDK2VwAyEA3vVnIRyxAa9q+qtf0OPWoOUKff1D
|
||||
Pq5LpXPUTh1nrJejYzBhMB0GA1UdDgQWBBT88UyTLCWai4vJtkS5K0zutivZOTAf
|
||||
BgNVHSMEGDAWgBT88UyTLCWai4vJtkS5K0zutivZOTAPBgNVHRMBAf8EBTADAQH/
|
||||
MA4GA1UdDwEB/wQEAwIBBjAFBgMrZXADQQBUmXc96ILueacLnf7kSJS35wiCl044
|
||||
Js8vwgQuTkJ9SDhuCOt88E4b9vZMhx2kOBLiwTyTdOILhVECPE9FZicD
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBMjCB5aADAgECAhRjMLlP+97gUZFyv5k1WdriOASrLDAFBgMrZXAwETEPMA0G
|
||||
A1UEAwwGVGVzdENBMB4XDTI2MDQwNTE2MjEzM1oXDTM2MDQwMjE2MjEzM1owEzER
|
||||
MA8GA1UEAwwIdGVzdHVzZXIwKjAFBgMrZXADIQCYYub304Ilt7lkzkN5plpIlGCo
|
||||
xR8wL18Xob7/hW2SWqNNMEswCQYDVR0TBAIwADAdBgNVHQ4EFgQUL4ve1GH0sFJ+
|
||||
33TwQP1R6oactHYwHwYDVR0jBBgwFoAU/PFMkywlmouLybZEuStM7rYr2TkwBQYD
|
||||
K2VwA0EAEpBsV5kpyuEF3t5GzuxELDJgtVxGLpZD5PsPqj+wxv5j6TeOwCE/LRRV
|
||||
JsKYJt3SMdqySx84dfscPD9c5HMPCw==
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIME1COxOi67I+kdoIH+ms4c0zKA8D7M8SkeJyjC89+pj
|
||||
-----END PRIVATE KEY-----
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIMr+udNo63lm79G+2xETGqoQsMJUvpbTUFhXdgKNI10C
|
||||
-----END PRIVATE KEY-----
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEA93ZZOd4zYtjwgdzSw+brqyWM9USG5INKCGWUEHRVRBw=
|
||||
-----END PUBLIC KEY-----
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBOTCB7KADAgECAhRjMLlP+97gUZFyv5k1WdriOASrKzAFBgMrZXAwETEPMA0G
|
||||
A1UEAwwGVGVzdENBMB4XDTI2MDQwNTE2MjEzM1oXDTM2MDQwMjE2MjEzM1owFDES
|
||||
MBAGA1UEAwwJbG9jYWxob3N0MCowBQYDK2VwAyEAn8X6FEFjCq5MKfiSVNKjRY5p
|
||||
TKdDrASo29olFWz8qy+jUzBRMA8GA1UdEQQIMAaHBH8AAAEwHQYDVR0OBBYEFL7n
|
||||
tv01dMhzLJ0dzTo7tEAXrYjuMB8GA1UdIwQYMBaAFPzxTJMsJZqLi8m2RLkrTO62
|
||||
K9k5MAUGAytlcANBAGpMB98RKLprVHiagV1Myj08TK2Lz4+K+Hs2fhUVMgRP9JXV
|
||||
vf8tC77XV/fH9wIKeaPvsupFO73AdD0BQZb8bQ0=
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIAX0EXHZDH5uU2h5ctNqHfa9hMtO9tfoM1kCRL9JHGtA
|
||||
-----END PRIVATE KEY-----
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBMzCB5qADAgECAhRT8XV12gL48jHEcgn7qTk7b5ocbzAFBgMrZXAwEjEQMA4G
|
||||
A1UEAwwHV3JvbmdDQTAeFw0yNjA0MDUxNjIxMzNaFw0zNjA0MDIxNjIxMzNaMBMx
|
||||
ETAPBgNVBAMMCGludHJ1ZGVyMCowBQYDK2VwAyEADrp6dr+UfNhzR6guiNU5ns0c
|
||||
Y97Ari4gVZnh8DE1MB6jTTBLMAkGA1UdEwQCMAAwHQYDVR0OBBYEFIF2t8T34ktN
|
||||
Y276k4702JltR0iEMB8GA1UdIwQYMBaAFNFUQIdydtMb4t9g8+0s9DHh0pYIMAUG
|
||||
AytlcANBAFO7sA+Po2qFaTRSdpxuAQIbywHiF92uyombcfQkQPgbVbAA3oH9gh32
|
||||
4uG4c1OCE+w1AI1f2/EpC4zZRPZVAwI=
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIAU8EOnIZ26wKCqJ/WTcoCBHETbSYsILQ9zxddB92JVQ
|
||||
-----END PRIVATE KEY-----
|
||||
37
lib/crates/fabro-server/tests/it/api/docs.rs
Normal file
37
lib/crates/fabro-server/tests/it/api/docs.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
fn read_doc(relative_path: &str) -> String {
|
||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../../")
|
||||
.join(relative_path);
|
||||
std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_server_docs_describe_the_unix_socket_default() {
|
||||
let architecture = read_doc("docs/reference/architecture.mdx");
|
||||
assert!(
|
||||
architecture.contains("~/.fabro/fabro.sock"),
|
||||
"architecture doc should mention the default Unix socket bind"
|
||||
);
|
||||
|
||||
let api_overview = read_doc("docs/api-reference/overview.mdx");
|
||||
assert!(
|
||||
api_overview.contains("~/.fabro/fabro.sock"),
|
||||
"API overview should mention the default Unix socket bind"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn security_doc_does_not_require_jwt_keys_for_the_current_web_flow() {
|
||||
let security = read_doc("docs/administration/security.mdx");
|
||||
assert!(
|
||||
security.contains("SESSION_SECRET"),
|
||||
"security doc should still mention the session secret"
|
||||
);
|
||||
assert!(
|
||||
!security.contains("`FABRO_JWT_PRIVATE_KEY`, `FABRO_JWT_PUBLIC_KEY`, and `SESSION_SECRET`"),
|
||||
"security doc should not describe JWT keys as required for the current web flow"
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
mod docs;
|
||||
mod routing;
|
||||
mod runs;
|
||||
mod settings;
|
||||
mod system;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod tls;
|
||||
mod tcp;
|
||||
|
|
|
|||
|
|
@ -21,10 +21,6 @@ _version = 1
|
|||
type = "tcp"
|
||||
address = "127.0.0.1:32276"
|
||||
|
||||
[server.listen.tls]
|
||||
cert = "/etc/fabro/tls/cert.pem"
|
||||
key = "/etc/fabro/tls/key.pem"
|
||||
|
||||
[server.auth]
|
||||
methods = ["dev-token", "github"]
|
||||
|
||||
|
|
|
|||
|
|
@ -19,10 +19,6 @@ _version = 1
|
|||
type = "tcp"
|
||||
address = "127.0.0.1:32276"
|
||||
|
||||
[server.listen.tls]
|
||||
cert = "/etc/fabro/tls/cert.pem"
|
||||
key = "/etc/fabro/tls/key.pem"
|
||||
|
||||
[server.storage]
|
||||
root = "/srv/fabro"
|
||||
|
||||
|
|
|
|||
280
lib/crates/fabro-server/tests/it/api/tcp.rs
Normal file
280
lib/crates/fabro-server/tests/it/api/tcp.rs
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
use std::net::SocketAddr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
#[cfg(unix)]
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use fabro_server::bind::Bind;
|
||||
use fabro_server::ip_allowlist::{IpAllowlist, IpAllowlistConfig};
|
||||
use fabro_server::jwt_auth::{AuthMode, ConfiguredAuth};
|
||||
use fabro_server::serve::{ServeArgs, serve_command};
|
||||
use fabro_server::server::{
|
||||
RouterOptions, build_router, build_router_with_options, create_app_state,
|
||||
};
|
||||
use fabro_types::settings::ServerAuthMethod;
|
||||
use fabro_util::terminal::Styles;
|
||||
use tempfile::TempDir;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::helpers::api;
|
||||
|
||||
const TEST_DEV_TOKEN: &str =
|
||||
"fabro_dev_abababababababababababababababababababababababababababababababab";
|
||||
|
||||
async fn start_tcp_server(auth_mode: AuthMode) -> SocketAddr {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
let state = create_app_state();
|
||||
let router = build_router(state, auth_mode);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = axum::serve(
|
||||
listener,
|
||||
router.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
addr
|
||||
}
|
||||
|
||||
async fn start_tcp_server_with_allowlist(
|
||||
auth_mode: AuthMode,
|
||||
ip_allowlist: Arc<IpAllowlistConfig>,
|
||||
) -> SocketAddr {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
let state = create_app_state();
|
||||
let router =
|
||||
build_router_with_options(state, auth_mode, ip_allowlist, RouterOptions::default());
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = axum::serve(
|
||||
listener,
|
||||
router.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
addr
|
||||
}
|
||||
|
||||
fn build_client() -> fabro_http::HttpClient {
|
||||
fabro_http::test_http_client().unwrap()
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn build_unix_client(path: &Path) -> fabro_http::HttpClient {
|
||||
fabro_http::HttpClientBuilder::new()
|
||||
.unix_socket(path)
|
||||
.no_proxy()
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn write_test_config(tempdir: &TempDir, settings: &str) -> PathBuf {
|
||||
let config_path = tempdir.path().join("settings.toml");
|
||||
std::fs::write(&config_path, settings).unwrap();
|
||||
std::fs::write(
|
||||
tempdir.path().join("server.env"),
|
||||
format!("FABRO_DEV_TOKEN={TEST_DEV_TOKEN}\n"),
|
||||
)
|
||||
.unwrap();
|
||||
config_path
|
||||
}
|
||||
|
||||
async fn spawn_served_listener(
|
||||
settings: impl AsRef<str>,
|
||||
) -> (JoinHandle<anyhow::Result<()>>, Bind, TempDir) {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let config_path = write_test_config(&tempdir, settings.as_ref());
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::new(false)));
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
let mut tx = Some(tx);
|
||||
let storage_dir = tempdir.path().to_path_buf();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
Box::pin(serve_command(
|
||||
ServeArgs {
|
||||
bind: None,
|
||||
web: false,
|
||||
no_web: true,
|
||||
model: None,
|
||||
provider: None,
|
||||
sandbox: None,
|
||||
max_concurrent_runs: None,
|
||||
config: Some(config_path),
|
||||
#[cfg(debug_assertions)]
|
||||
watch_web: false,
|
||||
},
|
||||
styles,
|
||||
Some(storage_dir),
|
||||
move |bind| {
|
||||
let sender = tx.take().expect("server should only report readiness once");
|
||||
sender.send(bind.clone()).ok();
|
||||
Ok(())
|
||||
},
|
||||
))
|
||||
.await
|
||||
});
|
||||
|
||||
let bind = rx.await.expect("server should report its bind address");
|
||||
(handle, bind, tempdir)
|
||||
}
|
||||
|
||||
async fn wait_for_tcp_health(addr: SocketAddr) {
|
||||
let client = build_client();
|
||||
let url = format!("http://127.0.0.1:{}/health", addr.port());
|
||||
|
||||
for _ in 0..50 {
|
||||
if let Ok(response) = client.get(&url).send().await {
|
||||
if response.status() == 200 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
|
||||
panic!("timed out waiting for TCP health endpoint at {url}");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn wait_for_unix_health(path: &Path) {
|
||||
let client = build_unix_client(path);
|
||||
|
||||
for _ in 0..50 {
|
||||
if let Ok(response) = client.get("http://fabro/health").send().await {
|
||||
if response.status() == 200 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
|
||||
panic!(
|
||||
"timed out waiting for Unix socket health endpoint at {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tcp_accepts_plain_http_requests() {
|
||||
let (handle, bind, _tempdir) = spawn_served_listener(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[server.listen]
|
||||
type = "tcp"
|
||||
address = "127.0.0.1:0"
|
||||
|
||||
[server.auth]
|
||||
methods = ["dev-token"]
|
||||
"#,
|
||||
)
|
||||
.await;
|
||||
let addr = match bind {
|
||||
Bind::Tcp(addr) => addr,
|
||||
Bind::Unix(path) => panic!("expected TCP bind, got unix socket at {}", path.display()),
|
||||
};
|
||||
wait_for_tcp_health(addr).await;
|
||||
|
||||
let client = build_client();
|
||||
|
||||
let response = client
|
||||
.get(format!("http://127.0.0.1:{}{}", addr.port(), api("/runs")))
|
||||
.bearer_auth(TEST_DEV_TOKEN)
|
||||
.send()
|
||||
.await
|
||||
.expect("plain HTTP request should succeed");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tcp_dev_token_auth_uses_bearer_auth() {
|
||||
let auth_mode = AuthMode::Enabled(ConfiguredAuth {
|
||||
methods: vec![ServerAuthMethod::DevToken],
|
||||
dev_token: Some(TEST_DEV_TOKEN.to_string()),
|
||||
});
|
||||
let addr = start_tcp_server(auth_mode).await;
|
||||
let client = build_client();
|
||||
let url = format!("http://127.0.0.1:{}{}", addr.port(), api("/runs"));
|
||||
|
||||
let unauthorized = client.get(&url).send().await.unwrap();
|
||||
assert_eq!(unauthorized.status(), 401);
|
||||
|
||||
let authorized = client
|
||||
.get(url)
|
||||
.bearer_auth(TEST_DEV_TOKEN)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(authorized.status(), 200);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn unix_socket_accepts_plain_http_requests() {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let socket_path = std::env::temp_dir().join(format!("fabro-server-it-{unique}.sock"));
|
||||
let (handle, bind, _tempdir) = spawn_served_listener(format!(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[server.listen]
|
||||
type = "unix"
|
||||
path = "{}"
|
||||
|
||||
[server.auth]
|
||||
methods = ["dev-token"]
|
||||
"#,
|
||||
socket_path.display()
|
||||
))
|
||||
.await;
|
||||
let path = match bind {
|
||||
Bind::Unix(path) => path,
|
||||
Bind::Tcp(addr) => panic!("expected Unix bind, got TCP address {addr}"),
|
||||
};
|
||||
wait_for_unix_health(&path).await;
|
||||
|
||||
let response = build_unix_client(&path)
|
||||
.get(format!("http://fabro{}", api("/runs")))
|
||||
.bearer_auth(TEST_DEV_TOKEN)
|
||||
.send()
|
||||
.await
|
||||
.expect("Unix-socket HTTP request should succeed");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
handle.abort();
|
||||
std::fs::remove_file(&path).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tcp_ip_allowlist_uses_connect_info() {
|
||||
let addr = start_tcp_server_with_allowlist(
|
||||
AuthMode::Disabled,
|
||||
Arc::new(IpAllowlistConfig {
|
||||
allowlist: IpAllowlist::new(vec!["10.0.0.0/8".parse().unwrap()]),
|
||||
trusted_proxy_count: 0,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let client = build_client();
|
||||
|
||||
let response = client
|
||||
.get(format!("http://127.0.0.1:{}{}", addr.port(), api("/runs")))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), 403);
|
||||
}
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use fabro_server::ip_allowlist::{IpAllowlist, IpAllowlistConfig};
|
||||
use fabro_server::jwt_auth::{AuthMode, ConfiguredAuth};
|
||||
use fabro_server::server::{
|
||||
RouterOptions, build_router, build_router_with_options, create_app_state,
|
||||
};
|
||||
use fabro_server::tls::build_rustls_config;
|
||||
use fabro_types::settings::{InterpString, ServerAuthMethod, TlsConfig};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use crate::helpers::api;
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/mtls")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
struct PkiPaths {
|
||||
ca_cert: PathBuf,
|
||||
server_cert: PathBuf,
|
||||
server_key: PathBuf,
|
||||
}
|
||||
|
||||
fn fixture_pki() -> PkiPaths {
|
||||
PkiPaths {
|
||||
ca_cert: fixture_path("ca.crt"),
|
||||
server_cert: fixture_path("server.crt"),
|
||||
server_key: fixture_path("server.key"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_tls_server(tls_settings: &TlsConfig, auth_mode: AuthMode) -> std::net::SocketAddr {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
let rustls_config = build_rustls_config(tls_settings).unwrap();
|
||||
let tls_acceptor = tokio_rustls::TlsAcceptor::from(rustls_config);
|
||||
|
||||
let state = create_app_state();
|
||||
let router = build_router(state, auth_mode);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = fabro_server::tls::serve_tls(listener, tls_acceptor, router).await;
|
||||
});
|
||||
|
||||
addr
|
||||
}
|
||||
|
||||
async fn start_tls_server_with_allowlist(
|
||||
tls_settings: &TlsConfig,
|
||||
auth_mode: AuthMode,
|
||||
ip_allowlist: Arc<IpAllowlistConfig>,
|
||||
) -> std::net::SocketAddr {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
let rustls_config = build_rustls_config(tls_settings).unwrap();
|
||||
let tls_acceptor = tokio_rustls::TlsAcceptor::from(rustls_config);
|
||||
|
||||
let state = create_app_state();
|
||||
let router =
|
||||
build_router_with_options(state, auth_mode, ip_allowlist, RouterOptions::default());
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = fabro_server::tls::serve_tls(listener, tls_acceptor, router).await;
|
||||
});
|
||||
|
||||
addr
|
||||
}
|
||||
|
||||
fn build_client(ca_cert_path: &Path) -> fabro_http::HttpClient {
|
||||
let ca_pem = std::fs::read(ca_cert_path).unwrap();
|
||||
let ca_cert = fabro_http::tls::Certificate::from_pem(&ca_pem).unwrap();
|
||||
|
||||
fabro_http::HttpClientBuilder::new()
|
||||
.add_root_certificate(ca_cert)
|
||||
.no_proxy()
|
||||
.use_rustls_tls()
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn install_crypto_provider() {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
}
|
||||
|
||||
fn tls_settings(pki: &PkiPaths) -> TlsConfig {
|
||||
TlsConfig {
|
||||
cert: InterpString::parse(&pki.server_cert.to_string_lossy()),
|
||||
key: InterpString::parse(&pki.server_key.to_string_lossy()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tls_accepts_requests_without_client_cert() {
|
||||
install_crypto_provider();
|
||||
let pki = fixture_pki();
|
||||
let addr = start_tls_server(&tls_settings(&pki), AuthMode::Disabled).await;
|
||||
let client = build_client(&pki.ca_cert);
|
||||
|
||||
let response = client
|
||||
.get(format!("https://127.0.0.1:{}{}", addr.port(), api("/runs")))
|
||||
.send()
|
||||
.await
|
||||
.expect("request over TLS should succeed without a client certificate");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tls_dev_token_auth_does_not_require_client_cert() {
|
||||
install_crypto_provider();
|
||||
let pki = fixture_pki();
|
||||
let dev_token = "fabro_dev_abababababababababababababababababababababababababababababababab";
|
||||
let auth_mode = AuthMode::Enabled(ConfiguredAuth {
|
||||
methods: vec![ServerAuthMethod::DevToken],
|
||||
dev_token: Some(dev_token.to_string()),
|
||||
});
|
||||
let addr = start_tls_server(&tls_settings(&pki), auth_mode).await;
|
||||
let client = build_client(&pki.ca_cert);
|
||||
let url = format!("https://127.0.0.1:{}{}", addr.port(), api("/runs"));
|
||||
|
||||
let unauthorized = client.get(&url).send().await.unwrap();
|
||||
assert_eq!(unauthorized.status(), 401);
|
||||
|
||||
let authorized = client.get(url).bearer_auth(dev_token).send().await.unwrap();
|
||||
assert_eq!(authorized.status(), 200);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tls_ip_allowlist_uses_connect_info() {
|
||||
install_crypto_provider();
|
||||
let pki = fixture_pki();
|
||||
let addr = start_tls_server_with_allowlist(
|
||||
&tls_settings(&pki),
|
||||
AuthMode::Disabled,
|
||||
Arc::new(IpAllowlistConfig {
|
||||
allowlist: IpAllowlist::new(vec!["10.0.0.0/8".parse().unwrap()]),
|
||||
trusted_proxy_count: 0,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let client = build_client(&pki.ca_cert);
|
||||
|
||||
let response = client
|
||||
.get(format!("https://127.0.0.1:{}{}", addr.port(), api("/runs")))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), 403);
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ pub use server::{
|
|||
ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerIpAllowlistOverrideSettings,
|
||||
ServerIpAllowlistSettings, ServerLayer, ServerListenSettings, ServerLoggingSettings,
|
||||
ServerSchedulerSettings, ServerSettings, ServerSlateDbSettings, ServerStorageSettings,
|
||||
ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings, TlsConfig,
|
||||
ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings,
|
||||
};
|
||||
pub use size::{ParseSizeError, Size};
|
||||
pub use splice_array::{SPLICE_MARKER, SpliceArray, SpliceArrayError};
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ mod tests {
|
|||
DockerfileSource, McpServerSettings, McpTransport, RunAgentSettings, RunGoal, RunSettings,
|
||||
};
|
||||
use crate::settings::server::{
|
||||
ObjectStoreSettings, ServerListenSettings, ServerSettings, ServerSlateDbSettings, TlsConfig,
|
||||
ObjectStoreSettings, ServerListenSettings, ServerSettings, ServerSlateDbSettings,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
@ -119,19 +119,11 @@ mod tests {
|
|||
assert_eq!(
|
||||
serde_json::to_value(ServerListenSettings::Tcp {
|
||||
address: "127.0.0.1:8080".parse().unwrap(),
|
||||
tls: Some(TlsConfig {
|
||||
cert: InterpString::parse("/tmp/server.crt"),
|
||||
key: InterpString::parse("/tmp/server.key"),
|
||||
}),
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"type": "tcp",
|
||||
"address": "127.0.0.1:8080",
|
||||
"tls": {
|
||||
"cert": "/tmp/server.crt",
|
||||
"key": "/tmp/server.key"
|
||||
}
|
||||
"address": "127.0.0.1:8080"
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ pub enum ServerListenSettings {
|
|||
Tcp {
|
||||
#[serde(serialize_with = "serialize_socket_addr")]
|
||||
address: SocketAddr,
|
||||
tls: Option<TlsConfig>,
|
||||
},
|
||||
Unix {
|
||||
path: InterpString,
|
||||
|
|
@ -52,21 +51,6 @@ impl Default for ServerListenSettings {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct TlsConfig {
|
||||
pub cert: InterpString,
|
||||
pub key: InterpString,
|
||||
}
|
||||
|
||||
impl Default for TlsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cert: InterpString::parse(""),
|
||||
key: InterpString::parse(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct ServerApiSettings {
|
||||
pub url: Option<InterpString>,
|
||||
|
|
@ -307,16 +291,13 @@ pub struct ServerLayer {
|
|||
pub integrations: Option<ServerIntegrationsLayer>,
|
||||
}
|
||||
|
||||
/// `[server.listen]` — shared bind transport. TLS lives under
|
||||
/// `[server.listen.tls]`.
|
||||
/// `[server.listen]` — shared bind transport.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")]
|
||||
pub enum ServerListenLayer {
|
||||
Tcp {
|
||||
#[serde(default)]
|
||||
address: Option<InterpString>,
|
||||
#[serde(default)]
|
||||
tls: Option<ServerListenTlsLayer>,
|
||||
},
|
||||
Unix {
|
||||
#[serde(default)]
|
||||
|
|
@ -324,15 +305,6 @@ pub enum ServerListenLayer {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ServerListenTlsLayer {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cert: Option<InterpString>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub key: Option<InterpString>,
|
||||
}
|
||||
|
||||
/// `[server.api]` — API surface settings.
|
||||
///
|
||||
/// `url` is an optional public URL; it is **not** derived from `server.listen`.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue