1use 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
17pub 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#[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 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 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 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 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
160pub 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 let Some(path) = api_path(request.uri().path(), state.config.compat_mode) else {
184 return next.run(request).await;
185 };
186
187 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 Err(error) => {
209 let mut refusal = error_response_for(&state.config, error).map(Body::from);
210 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
224fn api_path(path: &str, compat_mode: bool) -> Option<&str> {
232 fn under(path: &str, mount: &str) -> bool {
234 path == mount || path.strip_prefix(mount).is_some_and(|r| r.starts_with('/'))
235 }
236
237 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 compat_mode.then_some(path)
266}
267
268fn 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 #[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 #[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 "/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 #[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 #[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 #[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 #[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}