Skip to main content

postrust_server/
app.rs

1//! Request handling: the axum side of the REST API.
2//!
3//! The pipeline itself -- authentication, parsing, planning, execution -- is
4//! `postrust-engine`, so that a host without axum can run it too. What stays
5//! here is what only a router has: the handler signature, and the `OPTIONS`
6//! layer that has to see paths before the mount is stripped off.
7
8use crate::state::AppState;
9use axum::{
10    body::Body,
11    extract::{Request, State},
12    response::Response,
13};
14use postrust_engine::error_response_for;
15use std::sync::Arc;
16
17/// Main request handler.
18pub async fn handle_request(State(state): State<Arc<AppState>>, request: Request) -> Response {
19    #[cfg(not(feature = "pg-socket"))]
20    let response = postrust_engine::handle(state.as_ref(), request).await;
21    #[cfg(feature = "pg-socket")]
22    let response = postrust_engine::handle(&via_socket::ViaSocket(state), request).await;
23    response.map(Body::from)
24}
25
26/// The test-only `pg-socket` feature: REST through the Worker's driver.
27#[cfg(feature = "pg-socket")]
28mod via_socket {
29    use crate::state::AppState;
30    use postrust_auth::{JwtCache, JwtConfig};
31    use postrust_core::db::tokio_postgres::config::{Host, SslMode};
32    use postrust_core::db::tokio_postgres::{Config, NoTls};
33    use postrust_core::db::{BoxFuture, Connector, PgSocketDb};
34    use postrust_core::{AppConfig, SchemaCache};
35    use std::sync::{Arc, Mutex};
36    use tokio::sync::RwLockReadGuard;
37
38    /// A TCP socket per request, where a Worker opens one to Hyperdrive.
39    ///
40    /// Or why there can be none: a database URI this build cannot use fails
41    /// each request with the reason, rather than panicking inside one.
42    pub struct Tcp(Result<(String, u16), String>);
43
44    impl Connector for Tcp {
45        type Stream = tokio::net::TcpStream;
46        type Tls = NoTls;
47
48        async fn connect(&self) -> std::io::Result<tokio::net::TcpStream> {
49            let (host, port) = self
50                .0
51                .as_ref()
52                .map_err(|e| std::io::Error::other(e.clone()))?;
53            let stream = tokio::net::TcpStream::connect((host.as_str(), *port)).await?;
54            stream.set_nodelay(true)?;
55            Ok(stream)
56        }
57
58        fn tls(&self) -> NoTls {
59            NoTls
60        }
61
62        fn spawn(&self, connection: BoxFuture<'static, ()>) {
63            tokio::spawn(connection);
64        }
65
66        async fn sleep(&self, duration: std::time::Duration) {
67            tokio::time::sleep(duration).await
68        }
69    }
70
71    /// The database a URI names, as this build can reach it: plain TCP, no
72    /// TLS. `sslmode=require` is refused here, saying so, rather than at
73    /// every connection as a failed TLS handshake.
74    pub(super) fn database(uri: &str) -> PgSocketDb<Tcp> {
75        let parsed = uri
76            .parse::<Config>()
77            .map_err(|e| format!("pg-socket: the database URI does not parse: {e}"));
78        let (config, target) = match parsed {
79            Ok(config) => {
80                let target = match (config.get_hosts().first(), config.get_ports().first()) {
81                    _ if config.get_ssl_mode() == SslMode::Require => Err(
82                        "pg-socket: this build connects without TLS, and the database URI \
83                         says sslmode=require"
84                            .to_string(),
85                    ),
86                    (Some(Host::Tcp(host)), port) => {
87                        Ok((host.clone(), port.copied().unwrap_or(5432)))
88                    }
89                    _ => Err("pg-socket: the database URI needs a TCP host".to_string()),
90                };
91                (config, target)
92            }
93            Err(e) => (Config::new(), Err(e)),
94        };
95        match &target {
96            Ok((host, port)) => tracing::warn!(
97                "pg-socket: REST requests run through the socket driver, one connection \
98                 per request to {host}:{port}. This build is for testing."
99            ),
100            Err(e) => tracing::error!("{e}"),
101        }
102        PgSocketDb::new(Tcp(target), config)
103    }
104
105    /// One database per URI, made the first time a request names it.
106    ///
107    /// Per URI rather than one for the process: the first request's state used
108    /// to decide it for every state after, so a second `AppState` -- a test's,
109    /// logging in as another role -- was served through the first one's
110    /// login. Leaked, since a database is needed for as long as the process
111    /// runs, and a process has a URI or two.
112    static DBS: Mutex<Vec<(String, &'static PgSocketDb<Tcp>)>> = Mutex::new(Vec::new());
113
114    fn database_for(uri: &str) -> &'static PgSocketDb<Tcp> {
115        let mut known = DBS.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
116        if let Some((_, db)) = known.iter().find(|(known, _)| known == uri) {
117            return db;
118        }
119        let db: &'static PgSocketDb<Tcp> = Box::leak(Box::new(database(uri)));
120        known.push((uri.to_string(), db));
121        db
122    }
123
124    /// The server's state, with the socket driver in place of the pool.
125    ///
126    /// Owns its `Arc` rather than borrowing: a borrowed state gives the
127    /// request future a lifetime the router cannot prove `Send` for every
128    /// choice of.
129    pub struct ViaSocket(pub Arc<AppState>);
130
131    impl postrust_engine::State for ViaSocket {
132        type Db = PgSocketDb<Tcp>;
133        type SchemaCache<'a>
134            = RwLockReadGuard<'a, SchemaCache>
135        where
136            Self: 'a;
137
138        fn db(&self) -> &PgSocketDb<Tcp> {
139            database_for(&self.0.config.db_uri)
140        }
141
142        async fn schema_cache(&self) -> RwLockReadGuard<'_, SchemaCache> {
143            self.0.schema_cache().await
144        }
145
146        fn config(&self) -> &AppConfig {
147            &self.0.config
148        }
149
150        fn jwt_config(&self) -> &JwtConfig {
151            &self.0.jwt_config
152        }
153
154        fn jwt_cache(&self) -> Option<&JwtCache> {
155            self.0.jwt_cache.as_ref()
156        }
157    }
158}
159
160/// Answer an `OPTIONS` with the methods its resource actually has.
161///
162/// This runs outside the CORS layer because that layer answers every `OPTIONS`
163/// itself and never calls what it wraps -- `if parts.method == Method::OPTIONS`
164/// returns immediately, preflight or not. So an `OPTIONS` never reached the
165/// request handler at all: the body was empty because nothing built one, and
166/// `Allow` was missing because the code that knows the answer was never asked.
167///
168/// The CORS layer still writes the response, and this adds to it, so a
169/// preflight keeps every header it had.
170pub async fn options_allow(
171    State(state): State<Arc<AppState>>,
172    request: Request,
173    next: axum::middleware::Next,
174) -> Response {
175    if request.method() != http::Method::OPTIONS {
176        return next.run(request).await;
177    }
178
179    // Not every route is the API's. A layer added to the router wraps every
180    // one of them, so `/admin` and `/_` arrive here too, and answering those
181    // with what a table allows -- or with "no such table" -- takes a working
182    // preflight and turns it into a 404.
183    let Some(path) = api_path(request.uri().path(), state.config.compat_mode) else {
184        return next.run(request).await;
185    };
186
187    // Built here, from borrows, before anything is awaited: `axum::body::Body`
188    // is `Send` but not `Sync`, and `&T` is `Send` only where `T` is `Sync`, so
189    // a `&Request` held across an await makes the whole future not `Send` --
190    // which a middleware has to be.
191    let probe = probe_request(&request, path);
192    let allow = match probe {
193        Ok(probe) => postrust_engine::allow(state.as_ref(), probe).await,
194        Err(error) => Err(error),
195    };
196
197    let mut response = next.run(request).await;
198
199    match allow {
200        Ok(allow) => {
201            if let Ok(value) = http::HeaderValue::from_str(&allow) {
202                response.headers_mut().insert(http::header::ALLOW, value);
203            }
204            response
205        }
206        // Asking what may be done with a table that is not there is answered
207        // the same way as asking for the table.
208        Err(error) => {
209            let mut refusal = error_response_for(&state.config, error).map(Body::from);
210            // The CORS layer wrote its headers on the response this replaces.
211            // Without them a browser reports the request as blocked by CORS
212            // rather than as the 404 it earned, which is a different thing to
213            // go and fix.
214            for (name, value) in response.headers() {
215                if name.as_str().starts_with("access-control-") || name == http::header::VARY {
216                    refusal.headers_mut().append(name, value.clone());
217                }
218            }
219            refusal
220        }
221    }
222}
223
224/// The path the API parser should read, or `None` where the request names
225/// something that is not the REST surface.
226///
227/// A layer added with `Router::layer` wraps each endpoint, and `nest` puts its
228/// prefix-stripping *inside* that endpoint -- so the path seen here is the one
229/// the client wrote and still carries the mount it arrived under, unlike the
230/// one the handler reads.
231fn api_path(path: &str, compat_mode: bool) -> Option<&str> {
232    /// Whether a path names a mount point or something under it.
233    fn under(path: &str, mount: &str) -> bool {
234        path == mount || path.strip_prefix(mount).is_some_and(|r| r.starts_with('/'))
235    }
236
237    // Every mount this server answers itself. In compatibility mode the REST
238    // surface sits at the root, so a path not named here is read as a table:
239    // `/healthz` becomes a question about a table called `healthz`, and the
240    // two-segment ones are not a valid resource at all. Either way the
241    // refusal replaces the preflight the CORS layer wrote, which is how a
242    // browser comes to report a working endpoint as blocked.
243    for mount in [
244        "/admin",
245        "/_",
246        "/api/graphql",
247        "/healthz",
248        "/v1/version",
249        "/v1/graphql",
250        "/v1alpha1/graphql",
251    ] {
252        if under(path, mount) {
253            return None;
254        }
255    }
256    if under(path, "/api") {
257        return Some(match path.strip_prefix("/api") {
258            None | Some("") => "/",
259            Some(rest) => rest,
260        });
261    }
262    // Everywhere else the REST surface exists only in compatibility mode,
263    // where it answers at the root. Without it `/` is this server's own
264    // directory of endpoints, which allows what its handler allows.
265    compat_mode.then_some(path)
266}
267
268/// The request again, with an empty body and the API's own path, for the
269/// parser to read.
270fn probe_request(
271    request: &Request,
272    path: &str,
273) -> Result<http::Request<bytes::Bytes>, postrust_core::Error> {
274    let target = match request.uri().query() {
275        Some(query) => format!("{path}?{query}"),
276        None => path.to_string(),
277    };
278    let mut builder = http::Request::builder()
279        .method(request.method().clone())
280        .uri(target);
281    for (name, value) in request.headers() {
282        builder = builder.header(name, value);
283    }
284    builder
285        .body(bytes::Bytes::new())
286        .map_err(|e| postrust_core::Error::Internal(e.to_string()))
287}
288
289#[cfg(test)]
290mod tests {
291    /// The REST surface is mounted at `/api`, and an `OPTIONS` reaches the
292    /// layer before the mount is stripped off.
293    #[test]
294    fn the_api_path_is_the_one_below_the_mount() {
295        assert_eq!(super::api_path("/api/items", false), Some("/items"));
296        assert_eq!(super::api_path("/api/rpc/f", false), Some("/rpc/f"));
297        assert_eq!(super::api_path("/api", false), Some("/"));
298        assert_eq!(super::api_path("/api/", false), Some("/"));
299    }
300
301    /// Answering for a route that is not the API's turns a working preflight
302    /// into a 404 for a table nobody asked about.
303    #[test]
304    fn a_route_that_is_not_the_api_is_left_alone() {
305        for path in [
306            "/admin",
307            "/admin/swagger",
308            "/_",
309            "/_/health",
310            "/api/graphql",
311            // The endpoints a Hasura deployment has. `/v1/graphql` is the one
312            // every generated client is pointed at, so answering its preflight
313            // with "no such table" breaks the surface this branch exists to
314            // offer -- and only in compatibility mode, where nothing else
315            // looks wrong.
316            "/healthz",
317            "/v1/version",
318            "/v1/graphql",
319            "/v1/graphql/ws",
320            "/v1alpha1/graphql",
321        ] {
322            assert_eq!(super::api_path(path, false), None, "{path}");
323            assert_eq!(super::api_path(path, true), None, "{path}");
324        }
325    }
326
327    /// The mounts are whole segments here too: a table may be called `v1`.
328    #[test]
329    fn a_hasura_mount_does_not_swallow_a_table_beside_it() {
330        assert_eq!(super::api_path("/v1", true), Some("/v1"));
331        assert_eq!(super::api_path("/healthzz", true), Some("/healthzz"));
332        assert_eq!(super::api_path("/v1alpha1", true), Some("/v1alpha1"));
333    }
334
335    /// Outside the mounts the surface exists only in compatibility mode,
336    /// where PostgREST's own paths answer at the root.
337    #[test]
338    fn the_root_is_the_api_only_in_compatibility_mode() {
339        assert_eq!(super::api_path("/items", true), Some("/items"));
340        assert_eq!(super::api_path("/", true), Some("/"));
341        assert_eq!(super::api_path("/items", false), None);
342        assert_eq!(super::api_path("/", false), None);
343    }
344
345    /// A mount is a path segment: `/apiary` is not under `/api`.
346    #[test]
347    fn a_mount_matches_whole_segments_only() {
348        assert_eq!(super::api_path("/apiary", true), Some("/apiary"));
349        assert_eq!(
350            super::api_path("/administrators", true),
351            Some("/administrators")
352        );
353    }
354
355    /// A database URI the socket build cannot use fails the request that
356    /// needs it, saying why. It used to panic inside the request handler.
357    #[cfg(feature = "pg-socket")]
358    #[tokio::test]
359    async fn an_unusable_database_uri_fails_requests_rather_than_the_process() {
360        use postrust_core::db::Db;
361
362        for (uri, says) in [
363            ("postgres://u@:what/db", "does not parse"),
364            (
365                "postgres://u@localhost/db?sslmode=require",
366                "sslmode=require",
367            ),
368            ("host=/var/run/postgresql dbname=db", "TCP host"),
369        ] {
370            let db = super::via_socket::database(uri);
371            match db.begin().await {
372                Err(e) => assert!(e.to_string().contains(says), "{uri}: {e}"),
373                Ok(_) => panic!("{uri}: connected"),
374            }
375        }
376    }
377}