haystack_server/ops/
about.rs1use axum::extract::State;
11use axum::http::{HeaderMap, StatusCode};
12use axum::response::{IntoResponse, Response};
13
14use haystack_core::auth::{AuthHeader, parse_auth_header};
15use haystack_core::data::{HCol, HDict, HGrid};
16use haystack_core::kinds::Kind;
17
18use crate::content;
19use crate::error::error_grid;
20use crate::state::SharedState;
21
22fn haystack_response(body: Vec<u8>, content_type: &str) -> Response {
24 (
25 [(axum::http::header::CONTENT_TYPE, content_type.to_string())],
26 body,
27 )
28 .into_response()
29}
30
31pub async fn handle(State(state): State<SharedState>, headers: HeaderMap) -> Response {
33 let accept = headers
34 .get("Accept")
35 .and_then(|v| v.to_str().ok())
36 .unwrap_or("");
37
38 if !state.auth.is_enabled() {
40 return respond_about_grid(accept);
41 }
42
43 let auth_header = headers.get("Authorization").and_then(|v| v.to_str().ok());
44
45 match auth_header {
46 None => {
47 (
49 StatusCode::UNAUTHORIZED,
50 [("WWW-Authenticate", "HELLO")],
51 "Authentication required",
52 )
53 .into_response()
54 }
55 Some(header) => match parse_auth_header(header) {
56 Ok(AuthHeader::Hello { username, data }) => {
57 match state.auth.handle_hello(&username, data.as_deref()) {
58 Ok(www_auth) => (
59 StatusCode::UNAUTHORIZED,
60 [("WWW-Authenticate", www_auth.as_str())],
61 "",
62 )
63 .into_response(),
64 Err(e) => {
65 log::warn!("HELLO failed for {username}: {e}");
66 let grid = error_grid(&format!("authentication failed: {e}"));
67 respond_error_grid(&grid, accept, StatusCode::FORBIDDEN)
68 }
69 }
70 }
71 Ok(AuthHeader::Scram {
72 handshake_token,
73 data,
74 }) => match state.auth.handle_scram(&handshake_token, &data) {
75 Ok((_auth_token, auth_info)) => (
76 StatusCode::OK,
77 [("Authentication-Info", auth_info.as_str())],
78 "",
79 )
80 .into_response(),
81 Err(e) => {
82 log::warn!("SCRAM verification failed: {e}");
83 let grid = error_grid("authentication failed");
84 respond_error_grid(&grid, accept, StatusCode::FORBIDDEN)
85 }
86 },
87 Ok(AuthHeader::Bearer { auth_token }) => match state.auth.validate_token(&auth_token) {
88 Some(_user) => respond_about_grid(accept),
89 None => {
90 let grid = error_grid("invalid or expired auth token");
91 respond_error_grid(&grid, accept, StatusCode::UNAUTHORIZED)
92 }
93 },
94 Err(e) => {
95 log::warn!("Invalid Authorization header: {e}");
96 (
97 StatusCode::BAD_REQUEST,
98 format!("Invalid Authorization header: {e}"),
99 )
100 .into_response()
101 }
102 },
103 }
104}
105
106pub async fn handle_close(State(state): State<SharedState>, headers: HeaderMap) -> Response {
108 let accept = headers
109 .get("Accept")
110 .and_then(|v| v.to_str().ok())
111 .unwrap_or("");
112
113 if let Some(auth_header) = headers.get("Authorization").and_then(|v| v.to_str().ok())
115 && let Ok(AuthHeader::Bearer { auth_token }) = parse_auth_header(auth_header)
116 {
117 state.auth.revoke_token(&auth_token);
118 log::info!("User logged out");
119 }
120
121 let grid = HGrid::new();
123 match content::encode_response_grid(&grid, accept) {
124 Ok((body, ct)) => haystack_response(body, ct),
125 Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "encoding error").into_response(),
126 }
127}
128
129fn respond_about_grid(accept: &str) -> Response {
131 let mut row = HDict::new();
132 row.set("haystackVersion", Kind::Str("4.0".to_string()));
133 row.set("serverName", Kind::Str("rusty-haystack".to_string()));
134 row.set("serverVersion", Kind::Str("0.7.2".to_string()));
135 row.set("productName", Kind::Str("rusty-haystack".to_string()));
136 row.set(
137 "productUri",
138 Kind::Uri(haystack_core::kinds::Uri::new(
139 "https://github.com/jscott3201/rusty-haystack",
140 )),
141 );
142 row.set("moduleName", Kind::Str("haystack-server".to_string()));
143 row.set("moduleVersion", Kind::Str("0.7.2".to_string()));
144
145 let cols = vec![
146 HCol::new("haystackVersion"),
147 HCol::new("serverName"),
148 HCol::new("serverVersion"),
149 HCol::new("productName"),
150 HCol::new("productUri"),
151 HCol::new("moduleName"),
152 HCol::new("moduleVersion"),
153 ];
154
155 let grid = HGrid::from_parts(HDict::new(), cols, vec![row]);
156 match content::encode_response_grid(&grid, accept) {
157 Ok((body, ct)) => haystack_response(body, ct),
158 Err(e) => {
159 log::error!("Failed to encode about grid: {e}");
160 (StatusCode::INTERNAL_SERVER_ERROR, "encoding error").into_response()
161 }
162 }
163}
164
165fn respond_error_grid(grid: &HGrid, accept: &str, status: StatusCode) -> Response {
167 match content::encode_response_grid(grid, accept) {
168 Ok((body, ct)) => (
169 status,
170 [(axum::http::header::CONTENT_TYPE, ct.to_string())],
171 body,
172 )
173 .into_response(),
174 Err(_) => (status, "error").into_response(),
175 }
176}