1use std::collections::HashSet;
2use std::fmt;
3use std::net::SocketAddr;
4use std::sync::Arc;
5
6use axum::Router;
7use axum::body::{Body, Bytes};
8use axum::extract::{ConnectInfo, Request};
9use axum::http::header::{CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_TYPE, SET_COOKIE};
10use axum::http::{HeaderMap, HeaderValue, Method, StatusCode, request::Parts};
11use axum::response::Response;
12use axum::routing::any;
13use http_body_util::{BodyExt, LengthLimitError, Limited};
14use serde_json::{Value, json};
15
16use crate::client_ip::{canonical, client_user_agent};
17use crate::cookies::{CookieWrite, clear_cookie, set_cookie};
18use crate::jwks::JwksCache;
19use crate::manifest::ManifestSource;
20use crate::options::{Builder, Config};
21use crate::origin::check_origin;
22use crate::refresh::Refresher;
23use crate::service_token::ServiceTokens;
24
25const TRANSPORT_HEADER: &str = "x-seamless-auth-transport";
26pub(crate) const DELIVERY_MODE_HEADER: &str = "x-seamless-auth-delivery-mode";
27pub(crate) const MAX_BODY_BYTES: usize = 1 << 20;
28
29#[derive(Clone)]
33pub struct Adapter {
34 pub(crate) inner: Arc<Inner>,
35}
36
37impl fmt::Debug for Adapter {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 f.debug_struct("Adapter")
40 .field("auth_server_url", &self.inner.config.auth_server_url)
41 .finish_non_exhaustive()
42 }
43}
44
45pub(crate) struct Inner {
46 pub config: Config,
47 pub jwks: JwksCache,
48 pub manifest: ManifestSource,
49 pub tokens: ServiceTokens,
50 pub refreshes: Arc<Refresher>,
51 pub console_client: std::sync::OnceLock<Option<reqwest::Client>>,
52}
53
54impl Adapter {
55 pub fn builder(auth_server_url: impl Into<String>) -> Builder {
57 Builder::new(auth_server_url)
58 }
59
60 pub(crate) fn from_config(config: Config) -> Self {
61 let jwks = JwksCache::new(&config.auth_server_url, config.http_client.clone());
62 let manifest = ManifestSource::new(
63 &config.auth_server_url,
64 config.http_client.clone(),
65 config.fetch_manifest,
66 );
67 let tokens = ServiceTokens::new(config.service_secret.clone(), config.jwks_kid.clone());
68 Adapter {
69 inner: Arc::new(Inner {
70 config,
71 jwks,
72 manifest,
73 tokens,
74 refreshes: Arc::default(),
75 console_client: std::sync::OnceLock::new(),
76 }),
77 }
78 }
79
80 pub fn router<S>(&self) -> Router<S>
84 where
85 S: Clone + Send + Sync + 'static,
86 {
87 let inner = self.inner.clone();
88 let handler = move |req: Request| {
89 let inner = inner.clone();
90 async move { inner.serve(req).await }
91 };
92 Router::new()
93 .route("/", any(handler.clone()))
94 .route("/{*path}", any(handler))
95 }
96}
97
98pub(crate) struct Reply {
100 pub status: StatusCode,
101 pub body: Option<Value>,
102 pub raw: Option<reqwest::Response>,
103 pub set: Vec<CookieWrite>,
104 pub clear: Vec<String>,
105}
106
107impl Reply {
108 pub fn new(status: StatusCode, body: Option<Value>) -> Self {
109 Reply {
110 status,
111 body,
112 raw: None,
113 set: Vec::new(),
114 clear: Vec::new(),
115 }
116 }
117
118 pub fn error(status: StatusCode, code: &str) -> Self {
119 Reply::new(status, Some(json!({ "error": code })))
120 }
121
122 pub fn with_set(mut self, set: Vec<CookieWrite>) -> Self {
123 self.set = set;
124 self
125 }
126
127 pub fn with_clear(mut self, clear: Vec<String>) -> Self {
128 self.clear = clear;
129 self
130 }
131}
132
133#[derive(Clone, Default)]
135pub(crate) struct Forwarded {
136 pub ip: Option<String>,
137 pub user_agent: Option<Vec<u8>>,
138}
139
140pub(crate) struct UpstreamCall {
142 pub method: Method,
143 pub path: String,
144 pub query: Option<String>,
145 pub body: Option<Bytes>,
146 pub authorization: Option<String>,
147 pub service: Option<String>,
148 pub external_delivery: bool,
149}
150
151impl UpstreamCall {
152 pub fn new(method: Method, path: impl Into<String>) -> Self {
153 UpstreamCall {
154 method,
155 path: path.into(),
156 query: None,
157 body: None,
158 authorization: None,
159 service: None,
160 external_delivery: false,
161 }
162 }
163}
164
165pub(crate) fn json_response(status: StatusCode, body: &Value) -> Response {
166 let mut res = Response::new(Body::from(body.to_string()));
167 *res.status_mut() = status;
168 res.headers_mut().insert(
169 CONTENT_TYPE,
170 HeaderValue::from_static("application/json; charset=utf-8"),
171 );
172 res
173}
174
175impl Inner {
176 async fn serve(self: Arc<Self>, req: Request) -> Response {
177 let (parts, body) = req.into_parts();
178
179 if let Some(blocked) = check_origin(&self.config, &parts.method, &parts.headers) {
180 return self.write(blocked, false);
181 }
182
183 let bearer = parts
184 .headers
185 .get(TRANSPORT_HEADER)
186 .is_some_and(|v| v.as_bytes().eq_ignore_ascii_case(b"bearer"));
187 let forwarded = self.forwarded(&parts);
188 let path = parts.uri.path();
189 let special = path
192 .split('/')
193 .filter(|s| !s.is_empty())
194 .collect::<Vec<_>>()
195 .join("/")
196 .to_ascii_lowercase();
197
198 let reply = if parts.method == Method::POST && special == "refresh" {
199 self.refresh_route(&parts.headers, &forwarded, bearer).await
200 } else if parts.method == Method::DELETE && special == "logout" {
201 self.logout_route(&parts.headers, &forwarded, bearer, "/logout")
202 .await
203 } else if parts.method == Method::DELETE && special == "logout/all" {
204 self.logout_route(&parts.headers, &forwarded, bearer, "/logout/all")
205 .await
206 } else {
207 let manifest = self.manifest.get().await;
208 match manifest.find(parts.method.as_str(), path) {
209 Some(found) => {
210 self.manifest_route(&parts, body, &forwarded, found, bearer)
211 .await
212 }
213 None => Reply::error(StatusCode::NOT_FOUND, "not_found"),
214 }
215 };
216
217 self.write(reply, bearer)
218 }
219
220 fn forwarded(&self, parts: &Parts) -> Forwarded {
221 let peer = parts
222 .extensions
223 .get::<ConnectInfo<SocketAddr>>()
224 .map(|ConnectInfo(addr)| addr.ip());
225 let ip = match &self.config.client_ip {
226 Some(resolve) => resolve(&parts.headers, peer),
227 None => peer,
228 };
229 Forwarded {
230 ip: ip.map(canonical),
231 user_agent: client_user_agent(&parts.headers),
232 }
233 }
234
235 fn write(&self, reply: Reply, bearer: bool) -> Response {
239 let mut cookies = HeaderMap::new();
240 if !bearer {
241 let mut seen = HashSet::new();
242 for name in reply.clear.iter().filter(|n| seen.insert(n.as_str())) {
243 if let Some(value) = clear_cookie(&self.config, name) {
244 cookies.append(SET_COOKIE, value);
245 }
246 }
247 for cookie in &reply.set {
248 match set_cookie(&self.config, cookie) {
249 Ok(value) => {
250 cookies.append(SET_COOKIE, value);
251 }
252 Err(err) => {
253 tracing::error!(
254 "[seamless-auth] Could not issue cookie {}: {err}",
255 cookie.name
256 );
257 return json_response(
258 StatusCode::INTERNAL_SERVER_ERROR,
259 &json!({ "error": "internal_error" }),
260 );
261 }
262 }
263 }
264 }
265
266 let mut res = if let Some(raw) = reply.raw {
267 let mut headers = HeaderMap::new();
268 for name in [CONTENT_TYPE, CONTENT_DISPOSITION, CACHE_CONTROL] {
269 if let Some(value) = raw.headers().get(&name) {
270 headers.insert(name, value.clone());
271 }
272 }
273 let mut res = Response::new(Body::new(reqwest::Body::from(raw)));
274 *res.status_mut() = reply.status;
275 res.headers_mut().extend(headers);
276 res
277 } else if let Some(body) = &reply.body {
278 json_response(reply.status, body)
279 } else {
280 let mut res = Response::new(Body::empty());
281 *res.status_mut() = reply.status;
282 res
283 };
284
285 for value in cookies.get_all(SET_COOKIE) {
286 res.headers_mut().append(SET_COOKIE, value.clone());
287 }
288 res
289 }
290
291 pub(crate) async fn call(
292 &self,
293 forwarded: &Forwarded,
294 call: UpstreamCall,
295 ) -> Result<reqwest::Response, reqwest::Error> {
296 let mut url = format!("{}{}", self.config.auth_server_url, call.path);
297 if let Some(query) = call.query.as_deref().filter(|q| !q.is_empty()) {
298 url.push('?');
299 url.push_str(query);
300 }
301
302 let mut req = self
303 .config
304 .http_client
305 .request(call.method.clone(), url)
306 .header("accept", "application/json");
307 if let Some(body) = call.body.filter(|_| call.method != Method::GET) {
308 req = req.header("content-type", "application/json").body(body);
309 }
310 if let Some(authorization) = call.authorization {
311 req = req.header("authorization", authorization);
312 }
313 if let Some(service) = call.service {
314 req = req.header("x-seamless-service-token", service);
315 }
316 if let Some(ip) = &forwarded.ip {
317 req = req.header("x-seamless-client-ip", ip.as_str());
318 }
319 if let Some(ua) = forwarded
320 .user_agent
321 .as_ref()
322 .and_then(|ua| HeaderValue::from_bytes(ua).ok())
323 {
324 req = req.header("x-seamless-client-user-agent", ua);
325 }
326 if call.external_delivery {
327 req = req.header(DELIVERY_MODE_HEADER, "external");
328 }
329 req.send().await
330 }
331}
332
333pub(crate) async fn read_limited(
335 mut res: reqwest::Response,
336 max: usize,
337) -> Result<Vec<u8>, String> {
338 if res.content_length().is_some_and(|len| len > max as u64) {
339 return Err("response body too large".into());
340 }
341 let mut out = Vec::new();
342 while let Some(chunk) = res.chunk().await.map_err(|e| e.to_string())? {
343 if out.len() + chunk.len() > max {
344 return Err("response body too large".into());
345 }
346 out.extend_from_slice(&chunk);
347 }
348 Ok(out)
349}
350
351pub(crate) async fn read_json(res: reqwest::Response) -> (Option<Value>, bool) {
354 let Ok(data) = read_limited(res, MAX_BODY_BYTES).await else {
355 return (None, false);
356 };
357 if data.trim_ascii().is_empty() {
358 return (None, true);
359 }
360 match serde_json::from_slice::<Value>(&data) {
361 Ok(Value::Null) => (None, true),
362 Ok(value) => (Some(value), true),
363 Err(_) => (None, false),
364 }
365}
366
367pub(crate) fn failure(status: StatusCode, body: Option<Value>, is_json: bool) -> Reply {
371 match body {
372 Some(Value::Object(obj)) if is_json => Reply::new(status, Some(Value::Object(obj))),
373 _ => Reply::error(status, "upstream_error"),
374 }
375}
376
377pub(crate) enum BodyError {
378 TooLarge,
379 Unreadable,
380}
381
382pub(crate) async fn read_request_body(body: Body) -> Result<Option<Bytes>, BodyError> {
383 match Limited::new(body, MAX_BODY_BYTES).collect().await {
384 Ok(collected) => {
385 let bytes = collected.to_bytes();
386 Ok((!bytes.trim_ascii().is_empty()).then_some(bytes))
387 }
388 Err(err) if err.downcast_ref::<LengthLimitError>().is_some() => Err(BodyError::TooLarge),
389 Err(_) => Err(BodyError::Unreadable),
390 }
391}
392
393pub(crate) fn is_json_content_type(content_type: Option<&HeaderValue>) -> bool {
396 let Some(value) = content_type else {
397 return true;
398 };
399 let Ok(value) = value.to_str() else {
400 return false;
401 };
402 if value.trim().is_empty() {
403 return true;
404 }
405 let media_type = value
406 .split(';')
407 .next()
408 .unwrap_or("")
409 .trim()
410 .to_ascii_lowercase();
411 let Some((kind, subtype)) = media_type.split_once('/') else {
412 return false;
413 };
414 let token = |s: &str| {
415 !s.is_empty()
416 && s.bytes()
417 .all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b))
418 };
419 if !token(kind) || !token(subtype) {
420 return false;
421 }
422 media_type == "application/json" || subtype.ends_with("+json")
423}
424
425pub(crate) fn bearer_token(headers: &HeaderMap) -> Option<&str> {
427 let header = headers.get("authorization")?.to_str().ok()?;
428 if header.len() > 7 && header[..7].eq_ignore_ascii_case("bearer ") {
429 let token = header[7..].trim();
430 return (!token.is_empty()).then_some(token);
431 }
432 None
433}