Skip to main content

cufflink_native/
socket.rs

1//! The Unix-socket transport the extension runner relays calls over:
2//! `GET /ready` and `POST /call`, HTTP/1.1, each with the runner's token
3//! once one is required.
4
5use crate::deadline::CallDeadline;
6use crate::dispatch::{decode, Dispatcher};
7use axum::body::Bytes;
8use axum::extract::{DefaultBodyLimit, Request, State};
9use axum::http::header::AUTHORIZATION;
10use axum::http::{HeaderMap, StatusCode};
11use axum::middleware::{self, Next};
12use axum::response::{IntoResponse, Response};
13use axum::routing::{get, post};
14use axum::{Json, Router};
15use cufflink_types::ext::{ExtReply, Ready};
16use eyre::WrapErr;
17use std::fs::{self, Permissions};
18use std::future::Future;
19use std::io::ErrorKind;
20use std::os::unix::fs::{FileTypeExt, PermissionsExt};
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23use subtle::ConstantTimeEq;
24use tokio::net::UnixListener;
25
26/// A service bound to its socket, ready to [`serve`](Self::serve).
27pub struct SocketServer {
28    listener: UnixListener,
29    socket: SocketFile,
30    dispatcher: Arc<Dispatcher>,
31    token: Option<Arc<str>>,
32}
33
34/// The socket file, removed once nothing listens on it.
35struct SocketFile(PathBuf);
36
37impl Drop for SocketFile {
38    fn drop(&mut self) {
39        if let Err(e) = fs::remove_file(&self.0) {
40            if e.kind() != ErrorKind::NotFound {
41                tracing::warn!(socket = %self.0.display(), error = %e, "cannot remove the socket");
42            }
43        }
44    }
45}
46
47fn remove_stale(path: &Path) -> eyre::Result<()> {
48    match fs::symlink_metadata(path) {
49        Ok(meta) if meta.file_type().is_socket() => fs::remove_file(path)
50            .wrap_err_with(|| format!("cannot remove the stale socket {}", path.display())),
51        Ok(_) => eyre::bail!("{} exists and is not a socket", path.display()),
52        Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
53        Err(e) => Err(e).wrap_err_with(|| format!("cannot inspect {}", path.display())),
54    }
55}
56
57impl SocketServer {
58    pub(crate) fn bind(path: &Path, dispatcher: Dispatcher) -> eyre::Result<Self> {
59        remove_stale(path)?;
60        let listener = UnixListener::bind(path)
61            .wrap_err_with(|| format!("cannot listen on {}", path.display()))?;
62        let socket = SocketFile(path.to_path_buf());
63        fs::set_permissions(path, Permissions::from_mode(0o600))
64            .wrap_err_with(|| format!("cannot restrict {} to its owner", path.display()))?;
65        Ok(Self {
66            listener,
67            socket,
68            dispatcher: Arc::new(dispatcher),
69            token: None,
70        })
71    }
72
73    pub fn path(&self) -> &Path {
74        &self.socket.0
75    }
76
77    /// Answer only requests bearing `token` (`Authorization: Bearer
78    /// <token>`), anything else with 401: another process allowed to
79    /// connect to the socket cannot call as the runner.
80    pub fn require_token(self, token: impl Into<String>) -> Self {
81        Self {
82            token: Some(Arc::from(token.into())),
83            ..self
84        }
85    }
86
87    /// Serve until `shutdown` completes, then stop accepting, finish the
88    /// calls in flight and remove the socket.
89    pub async fn serve(
90        self,
91        shutdown: impl Future<Output = ()> + Send + 'static,
92    ) -> eyre::Result<()> {
93        let Self {
94            listener,
95            socket,
96            dispatcher,
97            token,
98        } = self;
99        tracing::info!(
100            socket = %socket.0.display(),
101            extensions = ?dispatcher.ready().extensions,
102            "serving"
103        );
104        axum::serve(listener, router(dispatcher, token))
105            .with_graceful_shutdown(shutdown)
106            .await
107            .wrap_err("the socket server failed")?;
108        drop(socket);
109        tracing::info!("stopped");
110        Ok(())
111    }
112}
113
114fn router(dispatcher: Arc<Dispatcher>, token: Option<Arc<str>>) -> Router {
115    let routes = Router::new()
116        .route("/ready", get(ready))
117        .route("/call", post(call))
118        .fallback(not_found)
119        .method_not_allowed_fallback(not_found)
120        .layer(DefaultBodyLimit::disable())
121        .with_state(dispatcher);
122    match token {
123        Some(token) => routes.layer(middleware::from_fn_with_state(token, authorized)),
124        None => routes,
125    }
126}
127
128/// Whether `headers` carry `Authorization: Bearer <token>` (the scheme in
129/// any case), the token compared in constant time.
130fn bears(headers: &HeaderMap, token: &str) -> bool {
131    let presented = headers
132        .get(AUTHORIZATION)
133        .and_then(|value| value.to_str().ok())
134        .and_then(|value| value.split_once(' '))
135        .filter(|(scheme, _)| scheme.eq_ignore_ascii_case("bearer"))
136        .map_or("", |(_, credentials)| credentials.trim());
137    bool::from(presented.as_bytes().ct_eq(token.as_bytes()))
138}
139
140async fn authorized(State(token): State<Arc<str>>, request: Request, next: Next) -> Response {
141    match bears(request.headers(), &token) {
142        true => next.run(request).await,
143        false => StatusCode::UNAUTHORIZED.into_response(),
144    }
145}
146
147async fn ready(State(dispatcher): State<Arc<Dispatcher>>) -> Json<Ready> {
148    Json(dispatcher.ready())
149}
150
151async fn call(
152    State(dispatcher): State<Arc<Dispatcher>>,
153    body: Bytes,
154) -> (StatusCode, Json<ExtReply>) {
155    match decode(&body) {
156        Ok(request) => {
157            let deadline = request.context.deadline();
158            (
159                StatusCode::OK,
160                Json(dispatcher.dispatch(request, deadline).await),
161            )
162        }
163        Err(refused) => (StatusCode::BAD_REQUEST, Json(refused)),
164    }
165}
166
167async fn not_found() -> StatusCode {
168    StatusCode::NOT_FOUND
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn only_the_whole_token_as_a_bearer_is_borne() {
177        let headers = |value: &str| {
178            let mut headers = HeaderMap::new();
179            headers.insert(AUTHORIZATION, value.parse().expect("header"));
180            headers
181        };
182        assert!(bears(&headers("Bearer abc"), "abc"));
183        assert!(bears(&headers("bearer abc"), "abc"));
184        for refused in ["Bearer", "Bearer ab", "Bearer abcd", "Basic abc", "abc"] {
185            assert!(!bears(&headers(refused), "abc"), "{refused}");
186        }
187        assert!(!bears(&HeaderMap::new(), "abc"));
188    }
189}