cufflink_native/
socket.rs1use 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
26pub struct SocketServer {
28 listener: UnixListener,
29 socket: SocketFile,
30 dispatcher: Arc<Dispatcher>,
31 token: Option<Arc<str>>,
32}
33
34struct 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 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 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
128fn 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}