use hotaru::prelude::*;
use hotaru::http::*;
use hotaru::TcpOutbound;
use hotaru::hotaru_http::protocol::HttpError;
use htmstd::session::CSessionRW;
use super::user::*;
use super::Server;
pub async fn send_http_request(
host: impl Into<String>,
mut request: HttpRequest,
safety: HttpSafety,
) -> Result<HttpResponse, HttpError> {
let host_str = host.into();
let (is_https, without_scheme) = if let Some(rest) = host_str.strip_prefix("https://") {
(true, rest.to_string())
} else if let Some(rest) = host_str.strip_prefix("http://") {
(false, rest.to_string())
} else {
(false, host_str.clone())
};
let (host_part, port, explicit_port) = match without_scheme.rfind(':') {
Some(colon) => {
let candidate = &without_scheme[colon + 1..];
if !candidate.is_empty()
&& candidate.len() <= 5
&& candidate.chars().all(|c| c.is_ascii_digit())
{
if let Ok(p) = candidate.parse::<u16>() {
(without_scheme[..colon].to_string(), p, true)
} else {
(without_scheme.clone(), if is_https { 443 } else { 80 }, false)
}
} else {
(without_scheme.clone(), if is_https { 443 } else { 80 }, false)
}
}
None => (without_scheme.clone(), if is_https { 443 } else { 80 }, false),
};
if request.meta.get_host().is_none() {
let host_header = if explicit_port {
format!("{}:{}", host_part, port)
} else {
host_part.clone()
};
request.meta.set_host(Some(host_header));
}
if is_https {
return Err(HttpError::Io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"HTTPS outbound is not enabled in this build (requires the hotaru `https` feature)",
)));
}
let outbound = TcpOutbound::build(format!("{}:{}", host_part, port))
.await
.map_err(HttpError::Io)?;
send_request(&outbound, request, safety).await
}
pub fn set_auth_token(req: &mut HttpReqCtx, token: &str) {
tracing::info!(%token, "Setting auth token in session");
req.params
.get_mut::<CSessionRW>()
.unwrap()
.insert("auth_token".into(), token.into());
}
pub fn get_auth_token(req: &HttpReqCtx) -> Option<String> {
req.params
.get::<CSessionRW>()
.and_then(|session| session.get("auth_token"))
.map(|token| token.string())
}
pub fn set_host(req: &mut HttpReqCtx, host: &str) {
tracing::info!(%host, "Setting host in session");
req.params
.get_mut::<CSessionRW>()
.unwrap()
.insert("host".into(), host.into());
}
pub fn get_host(req: &HttpReqCtx) -> Server {
req.params
.get::<CSessionRW>()
.and_then(|session| session.get("host"))
.map(|token| token.string())
.map(|s| Server::from_string(&s))
.unwrap_or(Server::Local)
}
pub async fn fetch_user_info(host: Server, auth: String) -> Option<User> {
println!("fetch_user_info: sending request to {}, token: {}", host.get_address(), auth);
let request = request_with_auth_token(get_request("/users/me"), Some(auth));
let response = send_http_request(
host.get_address(),
request,
HttpSafety::default(),
)
.await;
if response.is_err() {
println!("fetch_user_info: request failed: {:?}", response);
return None;
}
let response = response.unwrap();
println!("fetch_user_info: response body type: {:?}", std::mem::discriminant(&response.body));
let body = response.body.parse_buffer(&HttpSafety::new());
println!("fetch_user_info: parsed body: {:?}", body);
if let HttpBody::Json(json) = body {
if json.get("success").boolean() {
let mut user_value = json.get("user").clone();
user_value.set("server", host.clone());
println!("fetch_user_info: returning user: {:?}", user_value);
Some(user_value.into())
} else {
println!("fetch_user_info: success=false in response");
None
}
} else {
println!("fetch_user_info: unexpected response body: {:?}", body);
None
}
}
pub async fn refresh_user_token(req: &mut HttpReqCtx) -> Value {
let auth_token = match get_auth_token(req) {
Some(t) => t,
None => {
return object!({
success: false,
message: "No authentication token available"
});
}
};
let host = get_host(req);
match get_new_token(host, auth_token).await {
Ok(new_token) => {
tracing::info!(%new_token, "Refreshed auth token successfully");
set_auth_token(req, &new_token);
object!({
success: true,
access_token: new_token
})
}
Err(err_value) => {
tracing::error!("Token refresh failed: {:?}", err_value);
err_value
}
}
}
async fn get_new_token(host: Server, token: String) -> Result<String, Value> {
tracing::info!(%token, "Requesting new token from auth server");
let request = get_request("/auth/refresh")
.add_header("Authorization", format!("Bearer {}", token));
let response = send_http_request(
host.get_address(),
request,
HttpSafety::default(),
)
.await
.unwrap();
if let HttpBody::Json(json) = response.body.parse_buffer(&HttpSafety::new()) {
if json.get("success").boolean() {
Ok(json.get("access_token").string())
} else {
Err(json)
}
} else {
Err(object!({
success: false,
message: "Invalid response from server or no response"
}))
}
}
pub fn cache_user_info(req: &mut HttpReqCtx, user: User) {
tracing::info!(user = ?user, "Caching user info in session");
req.params
.get_mut::<CSessionRW>()
.unwrap()
.insert("user_info_cache".into(), user.into());
}
pub async fn auth_server_health(host: Server) -> bool {
let response = send_http_request(
host.get_address(),
get_request("/health"),
HttpSafety::default(),
)
.await
.unwrap();
if let HttpBody::Json(json) = response.body.parse_buffer(&HttpSafety::new()) {
json.get("status").string() == "ok"
} else {
false
}
}
pub async fn logout(req: &mut HttpReqCtx) -> HttpResponse {
tracing::info!("Clearing session and redirecting to login-refresh");
let params = req.params.get_mut::<CSessionRW>().unwrap();
params.remove("user_info_cache");
params.remove("auth_token");
params.remove("host");
redirect_response("/user/refresh?redirect=/user/login")
}
pub fn redirect_refresh(req: &mut HttpReqCtx) {
let redirect_url = req.request.meta.url();
let encoded = hotaru_lib::url_encoding::encode_url_owned(&redirect_url);
req.response = redirect_response(&format!(
"/user/refresh?redirect={}",
encoded
));
}
pub async fn disable_token(host: Server, token: String) -> Value {
let request = get_request("/auth/logout")
.add_header("Authorization", format!("Bearer {}", token));
let response = send_http_request(
host.get_address(),
request,
HttpSafety::default(),
)
.await
.unwrap();
if let HttpBody::Json(json) = response.body.parse_buffer(&HttpSafety::new()) {
json
} else {
object!({
success: false,
message: "Invalid response from server or no response"
})
}
}
pub fn request_with_auth_token(mut req: HttpRequest, token: Option<String>) -> HttpRequest {
if let Some(tok) = token {
req = req.add_header("Authorization", format!("Bearer {}", tok));
}
req
}
pub async fn get_user(req: &mut HttpReqCtx) -> User {
req.params
.get::<User>()
.map(|u| u.clone())
.unwrap_or_else(|| User::guest(get_host(req)))
}
pub async fn get_user_id(req: &mut HttpReqCtx) -> UserID {
get_user(req).await.into()
}