Rename transport binding to WIP over HTTP
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
name = "wip-fs-server"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Request-time filesystem projection server for WIP over HTTPS"
|
||||
description = "Request-time filesystem projection server for WIP over HTTP"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
publish = false
|
||||
@@ -11,7 +11,7 @@ publish = false
|
||||
http = "1"
|
||||
rustix = { version = "1.1", features = ["fs"] }
|
||||
wip-host = { path = "../wip-host" }
|
||||
wip-https = { path = "../wip-https" }
|
||||
wip-http = { path = "../wip-http" }
|
||||
wip-interface = { path = "../wip-interface" }
|
||||
wip-protocol = { path = "../wip-protocol" }
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
`wip-fs-server` projects one configured Linux filesystem directory onto WIP
|
||||
Worldspace `/` at request time. It does not register the filesystem contents or
|
||||
keep one object instance per entry. It uses `wip-host` for publication and typed
|
||||
operation dispatch and uses the standard `wip-https` routes, JSON codecs,
|
||||
operation dispatch and uses the standard `wip-http` routes, JSON codecs,
|
||||
envelopes, batching, and HTTP status mapping.
|
||||
|
||||
## Security model
|
||||
@@ -28,9 +28,9 @@ envelopes, batching, and HTTP status mapping.
|
||||
- Protocol metadata and errors never contain the backend absolute path. Startup
|
||||
prints only the explicitly configured effective root and security-sensitive
|
||||
modes.
|
||||
- The built-in listener is cleartext and loopback-only. Production deployments
|
||||
must pass `--behind-tls-proxy` and terminate HTTPS at a trusted reverse proxy.
|
||||
Plain development HTTP requires the explicit `--development-http` flag.
|
||||
- The built-in HTTP listener is loopback-only. TLS termination, certificates,
|
||||
authentication, an externally visible URL scheme, and proxy routing are
|
||||
deployment concerns rather than separate WIP transport profiles.
|
||||
|
||||
The implementation currently targets Linux because it uses `O_PATH`,
|
||||
directory-relative descriptor traversal, and `/proc/self/fd` to reopen the same
|
||||
@@ -38,34 +38,32 @@ pinned backend object without resolving the untrusted Worldspace path twice.
|
||||
|
||||
## Run
|
||||
|
||||
Read-only behind a TLS-terminating reverse proxy:
|
||||
Run the loopback listener in read-only mode:
|
||||
|
||||
```console
|
||||
cargo run -p wip-fs-server -- \
|
||||
--root /srv/public-files \
|
||||
--listen 127.0.0.1:8443 \
|
||||
--base-path /wip \
|
||||
--behind-tls-proxy
|
||||
--listen 127.0.0.1:8080 \
|
||||
--base-path /wip
|
||||
```
|
||||
|
||||
Local development with the write capability deliberately enabled:
|
||||
Run with the write capability deliberately enabled:
|
||||
|
||||
```console
|
||||
cargo run -p wip-fs-server -- \
|
||||
--root ./example-files \
|
||||
--listen 127.0.0.1:8080 \
|
||||
--base-path /wip \
|
||||
--development-http \
|
||||
--read-write
|
||||
```
|
||||
|
||||
The server reports the effective root, `ReadOnly`/`ReadWrite` mode, transport
|
||||
mode, endpoint, and actual listen address. It does not enumerate the root.
|
||||
The server reports the effective root, `ReadOnly`/`ReadWrite` mode, endpoint,
|
||||
and actual listen address. It does not enumerate the root.
|
||||
|
||||
## Standard interactions
|
||||
|
||||
The following examples use development HTTP for readability. The JSON is the
|
||||
standard `wip-https` profile; validators and operation `Bytes` are base64 body
|
||||
The following examples use the built-in HTTP listener. The JSON follows the
|
||||
standard WIP over HTTP binding; validators and operation `Bytes` are base64 body
|
||||
fields, never `ETag` or `If-Match` headers.
|
||||
|
||||
Observe only the root Object:
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
//! Request-time filesystem projection served through the standard WIP HTTPS binding.
|
||||
//! Request-time filesystem projection served through the standard WIP over HTTP binding.
|
||||
//!
|
||||
//! This crate deliberately keeps the boundaries visible:
|
||||
//!
|
||||
//! - [`wip_protocol`] owns logical requests and responses;
|
||||
//! - [`wip_host`] owns object publication, validators, and typed dispatch;
|
||||
//! - [`wip_https`] owns routes, JSON, envelopes, batching, and status mapping;
|
||||
//! - [`wip_http`] owns routes, JSON, envelopes, batching, and status mapping;
|
||||
//! - this crate owns the filesystem capability boundary and the real HTTP listener.
|
||||
//!
|
||||
//! TLS is terminated by an explicitly configured reverse proxy. The built-in
|
||||
//! listener accepts cleartext only on loopback, either behind that proxy or when
|
||||
//! [`TransportSecurity::DevelopmentHttp`] is deliberately selected.
|
||||
//! The built-in HTTP listener is intentionally loopback-only. TLS termination,
|
||||
//! certificates, authentication, and externally visible schemes are independent
|
||||
//! deployment concerns rather than WIP transport profiles.
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
@@ -33,20 +33,11 @@ pub enum AccessMode {
|
||||
ReadWrite,
|
||||
}
|
||||
|
||||
/// How HTTPS is provided for the endpoint.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TransportSecurity {
|
||||
/// A trusted reverse proxy terminates TLS and forwards to the loopback listener.
|
||||
ReverseProxyTls,
|
||||
/// Cleartext HTTP is deliberately enabled for local development only.
|
||||
DevelopmentHttp,
|
||||
}
|
||||
|
||||
/// Bounded resource policy applied by the HTTP adapter and filesystem provider.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ResourceLimits {
|
||||
/// Standard JSON, body-size, nesting, and batch limits.
|
||||
pub wire: wip_https::Limits,
|
||||
pub wire: wip_http::Limits,
|
||||
/// Maximum bytes returned by one file read.
|
||||
pub max_file_read_bytes: usize,
|
||||
/// Maximum bytes accepted by one file replacement.
|
||||
@@ -68,7 +59,7 @@ pub struct ResourceLimits {
|
||||
impl Default for ResourceLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
wire: wip_https::Limits::new(256 * 1024, 2 * 1024 * 1024, 64, 64)
|
||||
wire: wip_http::Limits::new(256 * 1024, 2 * 1024 * 1024, 64, 64)
|
||||
.expect("default wire limits are nonzero"),
|
||||
max_file_read_bytes: 1024 * 1024,
|
||||
max_file_write_bytes: 1024 * 1024,
|
||||
@@ -93,8 +84,6 @@ pub struct ServerConfig {
|
||||
pub endpoint_base_path: String,
|
||||
/// Read-only or explicit read-write publication mode.
|
||||
pub access: AccessMode,
|
||||
/// TLS termination or explicit development HTTP policy.
|
||||
pub transport_security: TransportSecurity,
|
||||
/// Request, operation, traversal, and concurrency bounds.
|
||||
pub limits: ResourceLimits,
|
||||
}
|
||||
@@ -102,8 +91,7 @@ pub struct ServerConfig {
|
||||
impl ServerConfig {
|
||||
/// Builds a secure-by-default configuration for `root`.
|
||||
///
|
||||
/// The listener defaults to an ephemeral loopback port, read-only access,
|
||||
/// and TLS termination delegated to a reverse proxy.
|
||||
/// The listener defaults to an ephemeral loopback port and read-only access.
|
||||
#[must_use]
|
||||
pub fn for_root(root: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
@@ -111,7 +99,6 @@ impl ServerConfig {
|
||||
listen: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
|
||||
endpoint_base_path: "/wip".into(),
|
||||
access: AccessMode::ReadOnly,
|
||||
transport_security: TransportSecurity::ReverseProxyTls,
|
||||
limits: ResourceLimits::default(),
|
||||
}
|
||||
}
|
||||
@@ -142,9 +129,9 @@ impl ServerConfig {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn endpoint(&self) -> Result<wip_https::Endpoint, ConfigError> {
|
||||
let endpoint = format!("https://localhost{}", self.endpoint_base_path);
|
||||
wip_https::Endpoint::parse(&endpoint).map_err(|_| ConfigError::InvalidBasePath)
|
||||
pub(crate) fn endpoint(&self) -> Result<wip_http::Endpoint, ConfigError> {
|
||||
let endpoint = format!("http://localhost{}", self.endpoint_base_path);
|
||||
wip_http::Endpoint::parse(&endpoint).map_err(|_| ConfigError::InvalidBasePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,13 @@ use std::error::Error;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use wip_fs_server::{AccessMode, ServerConfig, TransportSecurity, WipFsServer};
|
||||
use wip_fs_server::{AccessMode, ServerConfig, WipFsServer};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut root: Option<PathBuf> = None;
|
||||
let mut listen: SocketAddr = "127.0.0.1:8443".parse()?;
|
||||
let mut listen: SocketAddr = "127.0.0.1:8080".parse()?;
|
||||
let mut base_path = "/wip".to_owned();
|
||||
let mut access = AccessMode::ReadOnly;
|
||||
let mut security = None;
|
||||
let mut arguments = std::env::args().skip(1);
|
||||
while let Some(argument) = arguments.next() {
|
||||
match argument.as_str() {
|
||||
@@ -17,12 +16,6 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
"--listen" => listen = required_value(&mut arguments, "--listen")?.parse()?,
|
||||
"--base-path" => base_path = required_value(&mut arguments, "--base-path")?,
|
||||
"--read-write" => access = AccessMode::ReadWrite,
|
||||
"--development-http" => {
|
||||
set_security(&mut security, TransportSecurity::DevelopmentHttp)?
|
||||
}
|
||||
"--behind-tls-proxy" => {
|
||||
set_security(&mut security, TransportSecurity::ReverseProxyTls)?
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
print_help();
|
||||
return Ok(());
|
||||
@@ -32,20 +25,17 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
|
||||
let root = root.ok_or("--root is required")?;
|
||||
let transport_security =
|
||||
security.ok_or("select exactly one of --behind-tls-proxy or --development-http")?;
|
||||
let mut config = ServerConfig::for_root(root);
|
||||
config.listen = listen;
|
||||
config.endpoint_base_path = base_path;
|
||||
config.access = access;
|
||||
config.transport_security = transport_security;
|
||||
|
||||
let server = WipFsServer::new(config)?;
|
||||
let effective_root = server.effective_root().to_owned();
|
||||
let endpoint = server.endpoint().clone();
|
||||
let bound = server.bind()?;
|
||||
eprintln!(
|
||||
"wip-fs-server root={} access={access:?} transport={transport_security:?} endpoint={} listen={}",
|
||||
"wip-fs-server root={} access={access:?} endpoint={} listen={}",
|
||||
effective_root.display(),
|
||||
endpoint,
|
||||
bound.local_addr()
|
||||
@@ -63,21 +53,10 @@ fn required_value(
|
||||
.ok_or_else(|| format!("{option} requires a value").into())
|
||||
}
|
||||
|
||||
fn set_security(
|
||||
security: &mut Option<TransportSecurity>,
|
||||
value: TransportSecurity,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
if security.replace(value).is_some() {
|
||||
Err("select only one transport-security mode".into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"Usage: wip-fs-server --root PATH [--listen 127.0.0.1:8443] \\\n [--base-path /wip] [--read-write] \\\n (--behind-tls-proxy | --development-http)\n\n\
|
||||
The built-in listener is cleartext and loopback-only. In production, \
|
||||
select --behind-tls-proxy and terminate HTTPS at a trusted reverse proxy."
|
||||
"Usage: wip-fs-server --root PATH [--listen 127.0.0.1:8080] \\\n [--base-path /wip] [--read-write]\n\n\
|
||||
The built-in HTTP listener is loopback-only. TLS termination, authentication, \
|
||||
and external routing are deployment concerns."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use http::{Request, Response, StatusCode};
|
||||
use wip_host::{ExecutionContext, ObjectProvider};
|
||||
use wip_https::{
|
||||
use wip_http::{
|
||||
BatchRequestKind, BatchResponse, BatchResponseItem, BatchSuccess, CodecError, EncodeHttpError,
|
||||
Endpoint, RequestMetadataError, ResourceLimitKind, Route, decode_batch_request,
|
||||
decode_call_operation_metadata, decode_fetch_interface_request, decode_observe_request,
|
||||
@@ -55,7 +55,7 @@ impl WipFsServer {
|
||||
&self.endpoint
|
||||
}
|
||||
|
||||
/// Handles one already-buffered HTTP request with the standard `wip-https` codecs.
|
||||
/// Handles one already-buffered HTTP request with the standard `wip-http` codecs.
|
||||
///
|
||||
/// Listener integrations must enforce the same body limit while collecting bytes.
|
||||
#[must_use]
|
||||
|
||||
@@ -7,8 +7,8 @@ use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tempfile::tempdir;
|
||||
use wip_fs_server::{AccessMode, ServerConfig, TransportSecurity, WipFsServer};
|
||||
use wip_https::{
|
||||
use wip_fs_server::{AccessMode, ServerConfig, WipFsServer};
|
||||
use wip_http::{
|
||||
BatchRequest, BatchRequestItem, BatchRequestKind, BatchResponse, BatchSuccess, DecodedResponse,
|
||||
Endpoint, JSON_CONTENT_TYPE, Limits, Route, decode_batch_request, decode_batch_response,
|
||||
decode_call_operation_response, decode_fetch_interface_response, decode_observe_response,
|
||||
@@ -28,7 +28,6 @@ fn real_listener_serves_three_core_interactions_batch_and_write() {
|
||||
|
||||
let mut config = ServerConfig::for_root(temporary.path());
|
||||
config.access = AccessMode::ReadWrite;
|
||||
config.transport_security = TransportSecurity::DevelopmentHttp;
|
||||
let limits = config.limits.wire;
|
||||
let bound = Arc::new(
|
||||
WipFsServer::new(config)
|
||||
@@ -43,7 +42,7 @@ fn real_listener_serves_three_core_interactions_batch_and_write() {
|
||||
let shutdown = Arc::clone(&shutdown);
|
||||
thread::spawn(move || bound.serve_until(shutdown))
|
||||
};
|
||||
let endpoint = Endpoint::parse(&format!("https://localhost:{}/wip", address.port()))
|
||||
let endpoint = Endpoint::parse(&format!("http://localhost:{}/wip", address.port()))
|
||||
.expect("client endpoint");
|
||||
|
||||
let file_request = ObserveRequest {
|
||||
@@ -215,7 +214,6 @@ fn real_listener_serves_three_core_interactions_batch_and_write() {
|
||||
fn stalled_request_body_times_out_and_releases_the_only_worker() {
|
||||
let temporary = tempdir().expect("temporary root");
|
||||
let mut config = ServerConfig::for_root(temporary.path());
|
||||
config.transport_security = TransportSecurity::DevelopmentHttp;
|
||||
config.limits.max_concurrent_requests = 1;
|
||||
config.limits.request_timeout = Duration::from_millis(200);
|
||||
let limits = config.limits.wire;
|
||||
@@ -245,7 +243,7 @@ fn stalled_request_body_times_out_and_releases_the_only_worker() {
|
||||
stalled.flush().expect("flush partial request");
|
||||
thread::sleep(Duration::from_millis(25));
|
||||
|
||||
let endpoint = Endpoint::parse(&format!("https://localhost:{}/wip", address.port()))
|
||||
let endpoint = Endpoint::parse(&format!("http://localhost:{}/wip", address.port()))
|
||||
.expect("client endpoint");
|
||||
let observe = ObserveRequest {
|
||||
path: "/".into(),
|
||||
@@ -289,7 +287,7 @@ fn batch_keeps_known_malformed_arguments_item_local() {
|
||||
let config = ServerConfig::for_root(temporary.path());
|
||||
let limits = config.limits.wire;
|
||||
let server = WipFsServer::new(config).expect("build server");
|
||||
let endpoint = Endpoint::parse("https://localhost/wip").expect("endpoint");
|
||||
let endpoint = Endpoint::parse("http://localhost/wip").expect("endpoint");
|
||||
let body = br#"{"requests":[{"id":"ok","method":"observe","arguments":{"path":"/","depth":0}},{"id":"bad","method":"observe","arguments":{"path":"/","depth":"bad"}}]}"#;
|
||||
let logical = decode_batch_request(body, limits).expect("decode item-local invalid request");
|
||||
assert!(matches!(
|
||||
@@ -319,7 +317,7 @@ fn call_argument_decoding_does_not_mask_missing_target() {
|
||||
let temporary = tempdir().expect("temporary root");
|
||||
let config = ServerConfig::for_root(temporary.path());
|
||||
let server = WipFsServer::new(config).expect("build server");
|
||||
let endpoint = Endpoint::parse("https://localhost/wip").expect("endpoint");
|
||||
let endpoint = Endpoint::parse("http://localhost/wip").expect("endpoint");
|
||||
let body = br#"{"target":{"path":"/missing"},"interface":{"reference":"unknown"},"operation":"missing","arguments":{"bytes":"not-base64"}}"#;
|
||||
let request = http::Request::builder()
|
||||
.method(http::Method::POST)
|
||||
@@ -343,7 +341,7 @@ fn backend_invalid_unicode_path_is_not_found_not_internal() {
|
||||
let config = ServerConfig::for_root(temporary.path());
|
||||
let limits = config.limits.wire;
|
||||
let server = WipFsServer::new(config).expect("build server");
|
||||
let endpoint = Endpoint::parse("https://localhost/wip").expect("endpoint");
|
||||
let endpoint = Endpoint::parse("http://localhost/wip").expect("endpoint");
|
||||
let request = ObserveRequest {
|
||||
path: "/bad\0name".into(),
|
||||
depth: 0,
|
||||
@@ -368,7 +366,7 @@ fn read_response_size_failure_preserves_non_commit_guarantee() {
|
||||
config.limits.wire = Limits::new(4_096, 1_024, 64, 64).expect("wire limits");
|
||||
let limits = config.limits.wire;
|
||||
let server = WipFsServer::new(config).expect("build server");
|
||||
let endpoint = Endpoint::parse("https://localhost/wip").expect("endpoint");
|
||||
let endpoint = Endpoint::parse("http://localhost/wip").expect("endpoint");
|
||||
|
||||
let observe_request = ObserveRequest {
|
||||
path: "/large".into(),
|
||||
|
||||
Reference in New Issue
Block a user