1use crate::state::AppState;
9use axum::{
10 body::Body,
11 extract::{Request, State},
12 response::Response,
13};
14use postrust_engine::error_response;
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 verbatim_db_errors = state.config.compat_mode;
188
189 let probe = probe_request(&request, path);
194 let allow = match probe {
195 Ok(probe) => postrust_engine::allow(state.as_ref(), probe).await,
196 Err(error) => Err(error),
197 };
198
199 let mut response = next.run(request).await;
200
201 match allow {
202 Ok(allow) => {
203 if let Ok(value) = http::HeaderValue::from_str(&allow) {
204 response.headers_mut().insert(http::header::ALLOW, value);
205 }
206 response
207 }
208 Err(error) => {
211 let mut refusal = error_response(error, verbatim_db_errors).map(Body::from);
212 for (name, value) in response.headers() {
217 if name.as_str().starts_with("access-control-") || name == http::header::VARY {
218 refusal.headers_mut().append(name, value.clone());
219 }
220 }
221 refusal
222 }
223 }
224}
225
226fn api_path(path: &str, compat_mode: bool) -> Option<&str> {
234 fn under(path: &str, mount: &str) -> bool {
236 path == mount || path.strip_prefix(mount).is_some_and(|r| r.starts_with('/'))
237 }
238
239 for mount in [
246 "/admin",
247 "/_",
248 "/api/graphql",
249 "/healthz",
250 "/v1/version",
251 "/v1/graphql",
252 "/v1alpha1/graphql",
253 ] {
254 if under(path, mount) {
255 return None;
256 }
257 }
258 if under(path, "/api") {
259 return Some(match path.strip_prefix("/api") {
260 None | Some("") => "/",
261 Some(rest) => rest,
262 });
263 }
264 compat_mode.then_some(path)
268}
269
270fn probe_request(
273 request: &Request,
274 path: &str,
275) -> Result<http::Request<bytes::Bytes>, postrust_core::Error> {
276 let target = match request.uri().query() {
277 Some(query) => format!("{path}?{query}"),
278 None => path.to_string(),
279 };
280 let mut builder = http::Request::builder()
281 .method(request.method().clone())
282 .uri(target);
283 for (name, value) in request.headers() {
284 builder = builder.header(name, value);
285 }
286 builder
287 .body(bytes::Bytes::new())
288 .map_err(|e| postrust_core::Error::Internal(e.to_string()))
289}
290
291#[cfg(test)]
292mod tests {
293 #[test]
296 fn the_api_path_is_the_one_below_the_mount() {
297 assert_eq!(super::api_path("/api/items", false), Some("/items"));
298 assert_eq!(super::api_path("/api/rpc/f", false), Some("/rpc/f"));
299 assert_eq!(super::api_path("/api", false), Some("/"));
300 assert_eq!(super::api_path("/api/", false), Some("/"));
301 }
302
303 #[test]
306 fn a_route_that_is_not_the_api_is_left_alone() {
307 for path in [
308 "/admin",
309 "/admin/swagger",
310 "/_",
311 "/_/health",
312 "/api/graphql",
313 "/healthz",
319 "/v1/version",
320 "/v1/graphql",
321 "/v1/graphql/ws",
322 "/v1alpha1/graphql",
323 ] {
324 assert_eq!(super::api_path(path, false), None, "{path}");
325 assert_eq!(super::api_path(path, true), None, "{path}");
326 }
327 }
328
329 #[test]
331 fn a_hasura_mount_does_not_swallow_a_table_beside_it() {
332 assert_eq!(super::api_path("/v1", true), Some("/v1"));
333 assert_eq!(super::api_path("/healthzz", true), Some("/healthzz"));
334 assert_eq!(super::api_path("/v1alpha1", true), Some("/v1alpha1"));
335 }
336
337 #[test]
340 fn the_root_is_the_api_only_in_compatibility_mode() {
341 assert_eq!(super::api_path("/items", true), Some("/items"));
342 assert_eq!(super::api_path("/", true), Some("/"));
343 assert_eq!(super::api_path("/items", false), None);
344 assert_eq!(super::api_path("/", false), None);
345 }
346
347 #[test]
349 fn a_mount_matches_whole_segments_only() {
350 assert_eq!(super::api_path("/apiary", true), Some("/apiary"));
351 assert_eq!(
352 super::api_path("/administrators", true),
353 Some("/administrators")
354 );
355 }
356
357 #[cfg(feature = "pg-socket")]
360 #[tokio::test]
361 async fn an_unusable_database_uri_fails_requests_rather_than_the_process() {
362 use postrust_core::db::Db;
363
364 for (uri, says) in [
365 ("postgres://u@:what/db", "does not parse"),
366 (
367 "postgres://u@localhost/db?sslmode=require",
368 "sslmode=require",
369 ),
370 ("host=/var/run/postgresql dbname=db", "TCP host"),
371 ] {
372 let db = super::via_socket::database(uri);
373 match db.begin().await {
374 Err(e) => assert!(e.to_string().contains(says), "{uri}: {e}"),
375 Ok(_) => panic!("{uri}: connected"),
376 }
377 }
378 }
379}