(
State(state): State<AppState>,
headers: HeaderMap,
Json(mut request): Json<RegisterServerRequest>,
)
| 203 | path = "/v1/servers/register", |
| 204 | request_body = RegisterServerRequest, |
| 205 | responses( |
| 206 | (status = 200, description = "Registered server row", body = DirectoryServerRecord) |
| 207 | ) |
| 208 | )] |
| 209 | async fn register_server( |
| 210 | State(state): State<AppState>, |
| 211 | headers: HeaderMap, |
| 212 | connect_info: Option<ConnectInfo<SocketAddr>>, |
| 213 | Json(mut request): Json<RegisterServerRequest>, |
| 214 | ) -> Result<Json<DirectoryServerRecord>, (StatusCode, String)> { |
| 215 | if let Some(ip) = publish_client_ip(&state, &headers, connect_info.as_ref().map(|info| info.0)) |
| 216 | { |
| 217 | request.server_id = format!("{ip}:{}", request.hostport); |
| 218 | request.address = ip; |
| 219 | } |
| 220 | let server_id = request.server_id.clone(); |
| 221 | |
| 222 | let existing = state |
| 223 | .service |
| 224 | .get(&server_id) |
| 225 | .await |
| 226 | .map_err(|error| internal_error(&error))?; |
| 227 | let stored_hash = state |
| 228 | .service |
| 229 | .server_token_hash(&server_id) |
| 230 | .await |
| 231 | .map_err(|error| internal_error(&error))?; |
| 232 | let provided_token = bearer_token(&headers); |
| 233 | let token_ok = matches!( |
| 234 | (&provided_token, &stored_hash), |
| 235 | (Some(token), Some(hash)) if &service::token_hash(token) == hash |
| 236 | ); |
| 237 | |
| 238 | // Protect an actively-heartbeating, token-held row from takeover by a wrong/missing |
| 239 | // token (only when enforcement is on). A stale row (silent owner) may be reclaimed, |
| 240 | // which is the crash-recovery path. |
| 241 | if state.require_server_token && !token_ok && stored_hash.is_some() { |
| 242 | let fresh = existing.is_some_and(|row| { |
| 243 | current_time_millis() - row.last_seen_unix_ms < SERVER_TOKEN_RECOVERY_MS |
| 244 | }); |
| 245 | if fresh { |
| 246 | return Err(( |
| 247 | StatusCode::UNAUTHORIZED, |
| 248 | "missing or invalid server token".to_string(), |
| 249 | )); |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | let mut record = state |
| 254 | .service |
| 255 | .register(request, current_time_millis()) |
| 256 | .await |
| 257 | .map_err(|error| internal_error(&error))?; |
| 258 | |
| 259 | // Issue a token on first registration, and rotate it on an (authorized) takeover; a |
| 260 | // valid-token heartbeat keeps the existing token. |
| 261 | if !token_ok { |
| 262 | let (token, hash) = service::issue_token().map_err(|error| internal_error(&error))?; |
nothing calls this directly
no test coverage detected