1use std::collections::HashMap;
19use std::net::SocketAddr;
20use std::sync::{Arc, Mutex, PoisonError};
21
22use axum::extract::{ConnectInfo, Path, Query, Request, State};
23use axum::http::{HeaderMap, StatusCode};
24use axum::response::{IntoResponse, Response};
25use axum::routing::post;
26use axum::{Json, Router};
27use serde::{Deserialize, Serialize};
28
29use cheers_core::{Confirmation, Knock, PrincipalId, TokenVerifier};
30use cheers_server::{
31 Admitted, EdgeVerifier, KnockAuthority, KnockFlowError, OwnershipRow, PendingKnock, Queued, Reconciled, RevocationReader,
32};
33use cheers_verify::verify_device_artifact;
34
35use crate::error::RouteError;
36use crate::me::bearer_from_headers;
37
38#[derive(Debug, Default)]
41pub struct RateLimiter {
42 windows: Mutex<HashMap<String, (i64, u32)>>,
43}
44
45impl RateLimiter {
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn allow(&self, key: &str, limit: u32, now: i64) -> bool {
52 let window = now.div_euclid(60);
53 let mut w = self.windows.lock().unwrap_or_else(PoisonError::into_inner);
54 if w.len() > 10_000 {
55 w.retain(|_, (at, _)| *at == window);
56 }
57 let slot = w.entry(key.to_owned()).or_insert((window, 0));
58 if slot.0 != window {
59 *slot = (window, 0);
60 }
61 if slot.1 >= limit {
62 return false;
63 }
64 slot.1 += 1;
65 true
66 }
67}
68
69pub struct KnockState<V, Rd, S> {
70 pub edge: Arc<EdgeVerifier<V, Rd>>,
72 pub authority: Arc<KnockAuthority<S>>,
73 pub limiter: Arc<RateLimiter>,
74}
75
76impl<V, Rd, S> std::fmt::Debug for KnockState<V, Rd, S> {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.debug_struct("KnockState").finish_non_exhaustive()
79 }
80}
81
82#[derive(Debug, thiserror::Error)]
84pub enum KnockRouteError {
85 #[error(transparent)]
86 Route(#[from] RouteError),
87 #[error(transparent)]
88 Flow(#[from] KnockFlowError),
89 #[error("too many knocks; slow down")]
90 RateLimited,
91 #[error("bad request body: {0}")]
92 BadBody(String),
93}
94
95#[derive(Serialize)]
96struct ErrorBody {
97 error: &'static str,
98 message: String,
99}
100
101impl IntoResponse for KnockRouteError {
102 fn into_response(self) -> Response {
103 use KnockFlowError as F;
104 let flow = match self {
105 KnockRouteError::Route(e) => return e.into_response(),
106 other => other,
107 };
108 let (status, code) = match &flow {
109 KnockRouteError::Route(_) => unreachable!("returned above"),
110 KnockRouteError::RateLimited => (StatusCode::TOO_MANY_REQUESTS, "rate_limited"),
111 KnockRouteError::BadBody(_) => (StatusCode::BAD_REQUEST, "bad_body"),
112 KnockRouteError::Flow(f) => match f {
113 F::Artifact(_) | F::Invalid(_) | F::Standing(_) | F::WrongAuthority { .. } | F::OfferMismatch(_) => {
114 (StatusCode::BAD_REQUEST, "bad_artifact")
115 }
116 F::Expired { .. } => (StatusCode::GONE, "expired"),
117 F::NotAnApprover(_) => (StatusCode::FORBIDDEN, "not_an_approver"),
118 F::Refused(_) => (StatusCode::FORBIDDEN, "admission_refused"),
119 F::PendingFull => (StatusCode::TOO_MANY_REQUESTS, "pending_full"),
120 F::UnknownKnock => (StatusCode::NOT_FOUND, "unknown_knock"),
121 F::UnknownOffer => (StatusCode::NOT_FOUND, "unknown_offer"),
122 F::OfferExhausted => (StatusCode::GONE, "offer_exhausted"),
123 F::OfferExpired => (StatusCode::GONE, "offer_expired"),
124 F::Store(_) => (StatusCode::INTERNAL_SERVER_ERROR, "store"),
125 },
126 };
127 (status, Json(ErrorBody { error: code, message: flow.to_string() })).into_response()
128 }
129}
130
131#[derive(Debug, Clone, Deserialize)]
132pub struct KnockBody {
133 pub knock: String,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct KnockQueued {
139 pub id: String,
140 pub expires_at: i64,
141 pub replaced: bool,
143}
144
145#[derive(Debug, Clone, Deserialize)]
146pub struct PendingQuery {
147 pub resource_kind: String,
148 pub resource_id: String,
149}
150
151#[derive(Debug, Clone, Default, Deserialize)]
152#[serde(deny_unknown_fields)]
153pub struct AdmitKnockBody {
154 #[serde(default)]
156 pub confirmation: Option<Confirmation>,
157 #[serde(default)]
159 pub lease_seconds: Option<i64>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct AdmittedBody {
164 pub jti: String,
165 pub ownership: OwnershipRow,
166}
167
168impl From<Admitted> for AdmittedBody {
169 fn from(a: Admitted) -> Self {
170 Self { jti: a.jti, ownership: a.row }
171 }
172}
173
174#[derive(Debug, Clone, Deserialize)]
175pub struct CreateOfferBody {
176 pub resource_kind: String,
177 pub resource_id: String,
178 pub relation: String,
179 pub max_uses: u32,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct OfferCreated {
184 pub offer: cheers_core::Offer,
185 pub token: String,
186}
187
188#[derive(Debug, Clone, Deserialize)]
189pub struct RedeemBody {
190 pub offer: String,
191 pub knock: String,
192}
193
194#[derive(Debug, Clone, Deserialize)]
195pub struct AdmitUploadBody {
196 pub admit: String,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
200#[serde(tag = "outcome", rename_all = "snake_case")]
201pub enum AdmitUploadOutcome {
202 Accepted { jti: String, ownership: OwnershipRow },
203 Refused { jti: String, refusal: String },
204}
205
206async fn caller<V, Rd, S>(headers: &HeaderMap, state: &KnockState<V, Rd, S>, now: i64) -> Result<PrincipalId, RouteError>
207where
208 V: TokenVerifier + Send + Sync,
209 Rd: RevocationReader,
210{
211 let token = bearer_from_headers(headers)?;
212 match state.edge.verify_at(token, now).await {
213 Ok(claims) => Ok(PrincipalId::user(claims.sub.as_str())),
214 Err(cheers_core::Error::Store(e)) => Err(RouteError::Store(e.to_string())),
215 Err(_) => Err(RouteError::Unauthorized),
216 }
217}
218
219async fn post_knock<V, Rd, S>(
220 State(state): State<Arc<KnockState<V, Rd, S>>>,
221 request: Request,
222) -> Result<(StatusCode, Json<KnockQueued>), KnockRouteError>
223where
224 V: TokenVerifier + Send + Sync,
225 Rd: RevocationReader,
226 S: RevocationReader + Send + Sync,
227{
228 let now = now_unix();
229 let config = *state.authority.config();
230 let source = request
231 .extensions()
232 .get::<ConnectInfo<SocketAddr>>()
233 .map(|c| c.0.ip().to_string())
234 .unwrap_or_else(|| "unknown".into());
235 if !state.limiter.allow(&format!("source:{source}"), config.knocks_per_source_per_minute, now) {
236 return Err(KnockRouteError::RateLimited);
237 }
238 let bytes = axum::body::to_bytes(request.into_body(), 64 * 1024)
239 .await
240 .map_err(|e| KnockRouteError::BadBody(e.to_string()))?;
241 let body: KnockBody = serde_json::from_slice(&bytes).map_err(|e| KnockRouteError::BadBody(e.to_string()))?;
242 let knock: Knock = verify_device_artifact(&body.knock).map_err(KnockFlowError::from)?;
243 let resource = format!("resource:{}/{}", knock.kind, knock.id);
244 if !state.limiter.allow(&resource, config.knocks_per_resource_per_minute, now) {
245 return Err(KnockRouteError::RateLimited);
246 }
247 let (pending, queued) = state.authority.knock(&body.knock, now).await?;
248 Ok((
249 StatusCode::ACCEPTED,
250 Json(KnockQueued { id: pending.id, expires_at: pending.expires_at, replaced: queued == Queued::Replaced }),
251 ))
252}
253
254async fn list_knocks<V, Rd, S>(
255 State(state): State<Arc<KnockState<V, Rd, S>>>,
256 headers: HeaderMap,
257 Query(q): Query<PendingQuery>,
258) -> Result<Json<Vec<PendingKnock>>, KnockRouteError>
259where
260 V: TokenVerifier + Send + Sync,
261 Rd: RevocationReader,
262 S: RevocationReader + Send + Sync,
263{
264 let now = now_unix();
265 let who = caller(&headers, &state, now).await?;
266 Ok(Json(state.authority.pending(&who, &q.resource_kind, &q.resource_id, now).await?))
267}
268
269async fn admit_knock<V, Rd, S>(
270 State(state): State<Arc<KnockState<V, Rd, S>>>,
271 headers: HeaderMap,
272 Path(id): Path<String>,
273 body: Option<Json<AdmitKnockBody>>,
274) -> Result<(StatusCode, Json<AdmittedBody>), KnockRouteError>
275where
276 V: TokenVerifier + Send + Sync,
277 Rd: RevocationReader,
278 S: RevocationReader + Send + Sync,
279{
280 let now = now_unix();
281 let who = caller(&headers, &state, now).await?;
282 let body = body.map(|Json(b)| b).unwrap_or_default();
283 let confirmation = body.confirmation.unwrap_or(Confirmation::Accept);
284 let admitted = state.authority.admit_knock(&who, &id, confirmation, body.lease_seconds, now).await?;
285 Ok((StatusCode::CREATED, Json(admitted.into())))
286}
287
288async fn create_offer<V, Rd, S>(
289 State(state): State<Arc<KnockState<V, Rd, S>>>,
290 headers: HeaderMap,
291 Json(body): Json<CreateOfferBody>,
292) -> Result<(StatusCode, Json<OfferCreated>), KnockRouteError>
293where
294 V: TokenVerifier + Send + Sync,
295 Rd: RevocationReader,
296 S: RevocationReader + Send + Sync,
297{
298 let now = now_unix();
299 let who = caller(&headers, &state, now).await?;
300 let (offer, token) = state
301 .authority
302 .offer(&who, &body.resource_kind, &body.resource_id, &body.relation, body.max_uses, now)
303 .await?;
304 Ok((StatusCode::CREATED, Json(OfferCreated { offer, token })))
305}
306
307async fn redeem_offer<V, Rd, S>(
308 State(state): State<Arc<KnockState<V, Rd, S>>>,
309 Json(body): Json<RedeemBody>,
310) -> Result<(StatusCode, Json<AdmittedBody>), KnockRouteError>
311where
312 S: RevocationReader + Send + Sync,
313{
314 let admitted = state.authority.redeem_offer(&body.offer, &body.knock, now_unix()).await?;
315 Ok((StatusCode::CREATED, Json(admitted.into())))
316}
317
318async fn upload_admit<V, Rd, S>(
319 State(state): State<Arc<KnockState<V, Rd, S>>>,
320 Json(body): Json<AdmitUploadBody>,
321) -> Result<(StatusCode, Json<AdmitUploadOutcome>), KnockRouteError>
322where
323 S: RevocationReader + Send + Sync,
324{
325 Ok(match state.authority.reconcile(&body.admit, now_unix()).await? {
326 Reconciled::Accepted(a) => (StatusCode::OK, Json(AdmitUploadOutcome::Accepted { jti: a.jti, ownership: a.row })),
327 Reconciled::Refused { jti, refusal } => (StatusCode::FORBIDDEN, Json(AdmitUploadOutcome::Refused { jti, refusal })),
328 })
329}
330
331pub fn router<V, Rd, S>(state: Arc<KnockState<V, Rd, S>>) -> Router
335where
336 V: TokenVerifier + Send + Sync + 'static,
337 Rd: RevocationReader + Send + Sync + 'static,
338 S: RevocationReader + Send + Sync + 'static,
339{
340 Router::new()
341 .route("/knock", post(post_knock::<V, Rd, S>).get(list_knocks::<V, Rd, S>))
342 .route("/knock/{id}/admit", post(admit_knock::<V, Rd, S>))
343 .route("/offer", post(create_offer::<V, Rd, S>))
344 .route("/offer/redeem", post(redeem_offer::<V, Rd, S>))
345 .route("/admit", post(upload_admit::<V, Rd, S>))
346 .with_state(state)
347}
348
349fn now_unix() -> i64 {
350 use std::time::{SystemTime, UNIX_EPOCH};
351 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or_default()
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn the_limiter_counts_per_key_per_minute() {
360 let l = RateLimiter::new();
361 assert!(l.allow("a", 2, 600));
362 assert!(l.allow("a", 2, 610));
363 assert!(!l.allow("a", 2, 619));
364 assert!(l.allow("b", 2, 619));
365 assert!(l.allow("a", 2, 660), "a new window resets");
366 }
367}