Skip to main content

kcode_k1_http/
lib.rs

1use async_trait::async_trait;
2use axum::body::Body;
3use axum::extract::{Request, State};
4use axum::http::header::{ALLOW, CACHE_CONTROL, CONTENT_TYPE, HOST};
5use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri};
6use axum::middleware::{self, Next};
7use axum::response::Response;
8use axum::routing::{MethodRouter, get};
9use axum::{Json, Router};
10use kcode_k1_http_replay::{ReplayError, ReplayWindow};
11use kcode_k1_http_request::{Envelope, VerifyError, registration_public_key};
12use serde::Serialize;
13use std::fmt::{self, Display, Formatter};
14use std::sync::Arc;
15use tower_http::cors::{Any, CorsLayer};
16
17pub use kcode_k1_http_request::CanonicalUsername;
18
19pub struct Config {
20    pub server_id: String,
21    pub public_origin: String,
22    pub max_body_bytes: usize,
23}
24
25#[derive(Clone, Copy, Eq, PartialEq)]
26pub struct Identity {
27    user_id: [u8; 12],
28    public_key: [u8; 32],
29}
30
31impl Identity {
32    pub fn new(user_id: [u8; 12], public_key: [u8; 32]) -> Self {
33        Self {
34            user_id,
35            public_key,
36        }
37    }
38
39    pub fn user_id(&self) -> &[u8; 12] {
40        &self.user_id
41    }
42
43    pub fn public_key(&self) -> &[u8; 32] {
44        &self.public_key
45    }
46}
47
48#[derive(Clone)]
49pub struct Principal {
50    user_id: [u8; 12],
51    username: CanonicalUsername,
52}
53
54impl Principal {
55    pub fn user_id(&self) -> &[u8; 12] {
56        &self.user_id
57    }
58
59    pub fn username(&self) -> &CanonicalUsername {
60        &self.username
61    }
62}
63
64#[derive(Clone)]
65pub struct RegistrationPrincipal {
66    username: CanonicalUsername,
67    public_key: [u8; 32],
68}
69
70impl RegistrationPrincipal {
71    pub fn username(&self) -> &CanonicalUsername {
72        &self.username
73    }
74
75    pub fn public_key(&self) -> &[u8; 32] {
76        &self.public_key
77    }
78}
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub enum IdentityError {
82    Unavailable,
83}
84
85impl Display for IdentityError {
86    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
87        formatter.write_str("identity provider unavailable")
88    }
89}
90
91impl std::error::Error for IdentityError {}
92
93#[async_trait]
94pub trait IdentityProvider: Send + Sync + 'static {
95    async fn lookup(&self, username: &CanonicalUsername)
96    -> Result<Option<Identity>, IdentityError>;
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100pub enum ConfigError {
101    EmptyServerId,
102    InvalidPublicOrigin,
103    InvalidBodyLimit,
104}
105
106impl Display for ConfigError {
107    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
108        formatter.write_str(match self {
109            Self::EmptyServerId => "server ID is empty",
110            Self::InvalidPublicOrigin => "public origin is invalid",
111            Self::InvalidBodyLimit => "body limit is invalid",
112        })
113    }
114}
115
116impl std::error::Error for ConfigError {}
117
118pub struct K1Http {
119    state: Arc<AppState>,
120}
121
122struct AppState {
123    server_id: String,
124    public_origin: String,
125    authority: String,
126    max_body_bytes: usize,
127    replay: ReplayWindow,
128    identities: Arc<dyn IdentityProvider>,
129}
130
131#[derive(Serialize)]
132struct ConfigResponse {
133    protocol: &'static str,
134    server_id: String,
135    public_origin: String,
136}
137
138impl K1Http {
139    pub fn new(
140        config: Config,
141        replay: kcode_k1_http_replay::ReplayWindow,
142        identities: Arc<dyn IdentityProvider>,
143    ) -> Result<Self, ConfigError> {
144        if config.server_id.is_empty() {
145            return Err(ConfigError::EmptyServerId);
146        }
147        if config.max_body_bytes == 0 {
148            return Err(ConfigError::InvalidBodyLimit);
149        }
150        let authority =
151            origin_authority(&config.public_origin).ok_or(ConfigError::InvalidPublicOrigin)?;
152        Ok(Self {
153            state: Arc::new(AppState {
154                server_id: config.server_id,
155                public_origin: config.public_origin,
156                authority,
157                max_body_bytes: config.max_body_bytes,
158                replay,
159                identities,
160            }),
161        })
162    }
163
164    pub fn router(
165        &self,
166        registration: MethodRouter,
167        terms: MethodRouter,
168        authenticated: Router,
169    ) -> Router {
170        let authenticated = authenticated.layer(middleware::from_fn_with_state(
171            self.state.clone(),
172            authenticate,
173        ));
174        let registration = registration.layer(middleware::from_fn_with_state(
175            self.state.clone(),
176            authenticate_registration,
177        ));
178        let configuration = get(configuration).with_state(self.state.clone());
179        Router::new()
180            .nest("/api", authenticated)
181            .route("/api/register", registration)
182            .route("/api/terms", terms)
183            .route("/api/config.json", configuration)
184            .layer(middleware::from_fn(terms_method_guard))
185            .layer(cors())
186            .layer(middleware::from_fn_with_state(self.state.clone(), api_gate))
187    }
188}
189
190async fn configuration(State(state): State<Arc<AppState>>) -> Json<ConfigResponse> {
191    Json(ConfigResponse {
192        protocol: "K1-HTTP-1",
193        server_id: state.server_id.clone(),
194        public_origin: state.public_origin.clone(),
195    })
196}
197
198fn origin_authority(value: &str) -> Option<String> {
199    let uri: Uri = value.parse().ok()?;
200    let scheme = uri.scheme_str()?;
201    if !scheme.eq_ignore_ascii_case("http") && !scheme.eq_ignore_ascii_case("https") {
202        return None;
203    }
204    let authority = uri.authority()?.as_str();
205    if authority.is_empty() || authority.contains('@') || uri.query().is_some() {
206        return None;
207    }
208    if !matches!(uri.path(), "" | "/") {
209        return None;
210    }
211    Some(authority.to_owned())
212}
213
214fn cors() -> CorsLayer {
215    CorsLayer::new()
216        .allow_origin(Any)
217        .allow_methods([
218            Method::GET,
219            Method::POST,
220            Method::PUT,
221            Method::PATCH,
222            Method::DELETE,
223            Method::OPTIONS,
224            Method::HEAD,
225        ])
226        .allow_headers([
227            CONTENT_TYPE,
228            header("k1-username"),
229            header("k1-epoch"),
230            header("k1-nonce"),
231            header("k1-body-sha256"),
232            header("k1-signature"),
233            header("k1-public-key"),
234        ])
235        .expose_headers([header("k1-epoch")])
236}
237
238fn header(value: &'static str) -> HeaderName {
239    HeaderName::from_static(value)
240}
241
242fn set(headers: &mut HeaderMap, name: HeaderName, value: &'static str) {
243    headers.insert(name, HeaderValue::from_static(value));
244}
245
246async fn api_gate(State(state): State<Arc<AppState>>, request: Request, next: Next) -> Response {
247    let authority_ok = single(request.headers(), HOST.as_str())
248        .is_ok_and(|value| value.as_bytes() == state.authority.as_bytes());
249    if !authority_ok {
250        return finish(
251            &state,
252            error(StatusCode::MISDIRECTED_REQUEST, "invalid_request_authority"),
253        )
254        .await;
255    }
256    if state.replay.current_epoch().await.is_err() {
257        return decorate(
258            error(StatusCode::SERVICE_UNAVAILABLE, "epoch_unavailable"),
259            None,
260        );
261    }
262    finish(&state, next.run(request).await).await
263}
264
265async fn terms_method_guard(request: Request, next: Next) -> Response {
266    if request.uri().path() == "/api/terms"
267        && request.method() != Method::GET
268        && request.method() != Method::HEAD
269    {
270        return method_not_allowed("GET, HEAD");
271    }
272    next.run(request).await
273}
274
275async fn finish(state: &AppState, response: Response) -> Response {
276    match state.replay.current_epoch().await {
277        Ok(epoch) => decorate(response, Some(epoch)),
278        Err(_) => decorate(
279            error(StatusCode::SERVICE_UNAVAILABLE, "epoch_unavailable"),
280            None,
281        ),
282    }
283}
284
285fn decorate(mut response: Response, epoch: Option<u64>) -> Response {
286    let headers = response.headers_mut();
287    set(headers, CACHE_CONTROL, "no-store");
288    set(headers, header("x-content-type-options"), "nosniff");
289    set(headers, header("access-control-allow-origin"), "*");
290    set(headers, header("access-control-expose-headers"), "k1-epoch");
291    if let Some(epoch) = epoch
292        && let Ok(value) = HeaderValue::from_str(&epoch.to_string())
293    {
294        headers.insert(header("k1-epoch"), value);
295    }
296    response
297}
298
299fn single<'a>(headers: &'a HeaderMap, name: &str) -> Result<&'a HeaderValue, ()> {
300    let mut values = headers.get_all(name).iter();
301    let value = values.next().ok_or(())?;
302    if values.next().is_some() {
303        return Err(());
304    }
305    Ok(value)
306}
307
308async fn authenticate(
309    State(state): State<Arc<AppState>>,
310    request: Request,
311    next: Next,
312) -> Response {
313    proceed(authenticated_request(&state, request).await, next).await
314}
315
316async fn authenticate_registration(
317    State(state): State<Arc<AppState>>,
318    request: Request,
319    next: Next,
320) -> Response {
321    if request.method() != Method::POST {
322        return method_not_allowed("POST");
323    }
324    proceed(registration_request(&state, request).await, next).await
325}
326
327async fn proceed(result: Result<Request, Response>, next: Next) -> Response {
328    match result {
329        Ok(request) => next.run(request).await,
330        Err(response) => response,
331    }
332}
333
334async fn authenticated_request(state: &AppState, request: Request) -> Result<Request, Response> {
335    let envelope = Envelope::parse(request.headers())
336        .map_err(|_| error(StatusCode::BAD_REQUEST, "malformed_authentication_envelope"))?;
337    validate_epoch(state, envelope.epoch()).await?;
338    let identity = match state.identities.lookup(envelope.username()).await {
339        Ok(Some(identity)) => identity,
340        Ok(None) => return Err(error(StatusCode::UNAUTHORIZED, "authentication_failed")),
341        Err(IdentityError::Unavailable) => {
342            return Err(error(
343                StatusCode::SERVICE_UNAVAILABLE,
344                "identity_provider_unavailable",
345            ));
346        }
347    };
348    let mut request = verified(state, &envelope, request, identity.public_key()).await?;
349    let mut replay_id = [0_u8; 32];
350    replay_id[..12].copy_from_slice(identity.user_id());
351    state
352        .replay
353        .admit(&replay_id, envelope.epoch(), *envelope.nonce())
354        .await
355        .map_err(replay_error)?;
356    request.extensions_mut().insert(Principal {
357        user_id: *identity.user_id(),
358        username: envelope.username().clone(),
359    });
360    Ok(request)
361}
362
363async fn registration_request(state: &AppState, request: Request) -> Result<Request, Response> {
364    let envelope = Envelope::parse(request.headers())
365        .map_err(|_| error(StatusCode::BAD_REQUEST, "malformed_authentication_envelope"))?;
366    if single(request.headers(), "k1-public-key").is_err() {
367        return Err(error(
368            StatusCode::BAD_REQUEST,
369            "malformed_authentication_envelope",
370        ));
371    }
372    let candidate = registration_public_key(request.headers())
373        .map_err(|_| error(StatusCode::UNAUTHORIZED, "authentication_failed"))?;
374    validate_epoch(state, envelope.epoch()).await?;
375    let mut request = verified(state, &envelope, request, &candidate).await?;
376    request.extensions_mut().insert(RegistrationPrincipal {
377        username: envelope.username().clone(),
378        public_key: candidate,
379    });
380    Ok(request)
381}
382
383async fn verified(
384    state: &AppState,
385    envelope: &Envelope,
386    request: Request,
387    public_key: &[u8; 32],
388) -> Result<Request, Response> {
389    envelope
390        .verify(
391            request,
392            &state.server_id,
393            &state.public_origin,
394            public_key,
395            state.max_body_bytes,
396        )
397        .await
398        .map_err(verify_error)
399}
400
401async fn validate_epoch(state: &AppState, epoch: u64) -> Result<(), Response> {
402    state
403        .replay
404        .validate_epoch(epoch)
405        .await
406        .map(|_| ())
407        .map_err(replay_error)
408}
409
410fn replay_error(cause: ReplayError) -> Response {
411    match cause {
412        ReplayError::EpochOutsideWindow { .. } => error(StatusCode::UNAUTHORIZED, "stale_epoch"),
413        ReplayError::Replay { .. } => error(StatusCode::CONFLICT, "replay"),
414        ReplayError::CapacityExceeded { .. } => {
415            error(StatusCode::TOO_MANY_REQUESTS, "nonce_capacity")
416        }
417        _ => error(StatusCode::SERVICE_UNAVAILABLE, "epoch_unavailable"),
418    }
419}
420
421fn verify_error(cause: VerifyError) -> Response {
422    match cause {
423        VerifyError::AuthenticationFailed => {
424            error(StatusCode::UNAUTHORIZED, "authentication_failed")
425        }
426        VerifyError::BodyDigestMismatch => error(StatusCode::BAD_REQUEST, "body_digest_mismatch"),
427        VerifyError::BodyTooLarge => error(StatusCode::PAYLOAD_TOO_LARGE, "body_too_large"),
428    }
429}
430
431fn method_not_allowed(allow: &'static str) -> Response {
432    let mut response = Response::new(Body::empty());
433    *response.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
434    set(response.headers_mut(), ALLOW, allow);
435    response
436}
437
438fn error(status: StatusCode, code: &'static str) -> Response {
439    let mut response = Response::new(Body::from(format!("{{\"error\":\"{code}\"}}")));
440    *response.status_mut() = status;
441    set(response.headers_mut(), CONTENT_TYPE, "application/json");
442    response
443}