Files
asd-backend/src/main.rs
Milky0217 338871796a feat: 添加 /health 健康检查端点
- 新增 GET /health 端点,公开无需认证
- 检查 PostgreSQL 数据库连接状态
- 健康时返回: {"status": "healthy", "database": "connected"}
- 不健康时返回 HTTP 503 + {"status": "unhealthy", "database": "disconnected", "error": "..."}
- 失败时记录错误到 JSON 日志
- 用于负载均衡器、Kubernetes、1Panel 健康探针
- 更新 IMPROVEMENTS.md 标记 6.3 完成
2026-04-15 10:49:15 +08:00

882 lines
29 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use actix_web::middleware::from_fn;
use actix_web::{App, HttpResponse, HttpServer, Responder, delete, get, post, put, web};
use include_dir::{Dir, include_dir};
use tracing::{debug, error, info, warn};
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
use reqwest::Client;
use serde::Deserialize;
use sqlx::postgres::PgPool;
use std::pin::Pin;
mod auth;
mod db;
mod models;
use auth::{generate_token, jwt_middleware};
use db::{create_pool, insert_weather_data};
use models::{ErrorResponse, WeChatApiResponse, WeChatLoginRequest, WeatherData};
use crate::models::{Claims, TokenResponse, UpdatePaymentRequest};
// 编译时嵌入整个 static 目录
static STATIC_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static");
static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
#[derive(Clone)]
struct AppState {
jwt_secret: String,
wechat_appid: String,
wechat_secret: String,
}
impl AppState {
fn load() -> Result<Self, String> {
Ok(Self {
jwt_secret: std::env::var("JWT_SECRET")
.map_err(|_| "环境变量JWT_SECRET未设置".to_string())?,
wechat_appid: std::env::var("WECHAT_APPID")
.map_err(|_| "环境变量WECHAT_APPID未设置".to_string())?,
wechat_secret: std::env::var("WECHAT_SECRET")
.map_err(|_| "环境变量WECHAT_SECRET未设置".to_string())?,
})
}
}
// 加载TLS证书和私钥
fn create_ssl_acceptor() -> Result<SslAcceptorBuilder, Box<dyn std::error::Error>> {
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
// 从环境变量获取证书和私钥路径
let key_path =
std::env::var("SSL_KEY_PATH").unwrap_or_else(|_| "path/to/private.key".to_string());
let cert_path =
std::env::var("SSL_CERT_PATH").unwrap_or_else(|_| "path/to/certificate.crt".to_string());
builder.set_private_key_file(&key_path, SslFiletype::PEM)?;
builder.set_certificate_chain_file(&cert_path)?;
// 返回构建器而不是构建后的接受器
Ok(builder)
}
#[post("/api/login")]
async fn login(
pool: web::Data<PgPool>,
req: web::Json<WeChatLoginRequest>,
http_client: web::Data<Client>,
app_state: web::Data<AppState>,
) -> impl Responder {
let url = format!(
"https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code",
app_state.wechat_appid, app_state.wechat_secret, req.code
);
debug!("准备调用微信API: {}", url.replace(&app_state.wechat_secret, "***"));
let wechat_response = match http_client.get(&url).send().await {
Ok(response) => {
debug!("微信API请求成功状态码: {}", response.status());
response
}
Err(e) => {
error!("微信API请求失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "请求微信服务失败".to_string(),
errcode: None,
errmsg: None,
});
}
};
let wechat_data: WeChatApiResponse = match wechat_response.json().await {
Ok(data) => {
debug!("微信API响应解析成功");
data
}
Err(e) => {
error!("微信API响应解析失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "解析微信响应失败".to_string(),
errcode: None,
errmsg: None,
});
}
};
if let Some(errcode) = wechat_data.errcode {
warn!(
"微信登录验证失败 - 错误码: {}, 错误信息: {:?}",
errcode, wechat_data.errmsg
);
return HttpResponse::BadRequest().json(ErrorResponse {
error: "微信登录失败".to_string(),
errcode: Some(errcode),
errmsg: wechat_data.errmsg,
});
}
let openid = match wechat_data.openid {
Some(id) => {
let masked_openid = format!("{}***{}", &id[0..4], &id[id.len() - 4..]);
debug!("获取到用户openid: {}", masked_openid);
id
}
None => {
error!("微信API响应中未包含openid");
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "微信API未返回openid".to_string(),
errcode: None,
errmsg: None,
});
}
};
let upsert_query = r#"
INSERT INTO users (openid, name, type)
VALUES ($1, left($1, 8), 2)
ON CONFLICT (openid) DO UPDATE SET id = users.id
RETURNING id
"#;
let user_id = match sqlx::query_as::<_, (i32,)>(upsert_query)
.bind(&openid)
.fetch_one(pool.get_ref())
.await
{
Ok((id,)) => {
info!("用户登录/注册成功用户ID: {}", id);
id
}
Err(e) => {
error!("用户信息处理失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "用户信息处理失败".to_string(),
errcode: Some(500),
errmsg: Some(e.to_string()),
});
}
};
let token = match generate_token(user_id, &openid, 2, &app_state.jwt_secret) {
Ok(t) => {
debug!("JWT生成成功: {}", &t[0..8]);
t
}
Err(e) => {
error!("JWT生成失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "生成身份令牌失败".to_string(),
errcode: Some(500),
errmsg: Some(e.to_string()),
});
}
};
let token_response = TokenResponse::new(token);
HttpResponse::Ok().json(token_response)
}
#[post("/api/post-weather-data")]
async fn post_weather_data(
data: web::Json<WeatherData>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> impl Responder {
info!("Received weather data, preparing to insert into DB...");
let user_id = claims.user_id;
// 调用 db.rs 中的函数来处理数据库逻辑
match insert_weather_data(pool.get_ref(), &data, user_id).await {
Ok(inserted_id) => {
info!(
"Successfully inserted weather data with id: {}",
inserted_id
);
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"message": "Weather data inserted successfully",
"inserted_id": inserted_id,
"received_assignment": data.assignment_number
}))
}
Err(error_msg) => {
error!("Database operation failed: {}", error_msg);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": error_msg
}))
}
}
}
#[post("/api/generate-temp-token/{resource_id}")]
async fn generate_temp_token_handler(
path: web::Path<i32>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
app_state: web::Data<AppState>,
) -> impl Responder {
let resource_id = path.into_inner();
let openid: &String = &claims.openid;
match db::get_weather_details(pool.get_ref(), resource_id).await {
Ok(weather_data) => {
if weather_data.openid != *openid {
return HttpResponse::Forbidden().json(ErrorResponse {
error: "无权限为该资源生成临时token".to_string(),
errcode: Some(403),
errmsg: None,
});
}
}
Err(e) => {
return HttpResponse::BadRequest().json(ErrorResponse {
error: format!("资源不存在: {}", e),
errcode: Some(404),
errmsg: None,
});
}
}
let temp_token = match auth::generate_temp_token(openid, resource_id, &app_state.jwt_secret, 10) {
Ok(token) => token,
Err(e) => {
return HttpResponse::InternalServerError().json(ErrorResponse {
error: format!("生成临时token失败: {}", e),
errcode: Some(500),
errmsg: None,
});
}
};
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"temp_token": temp_token,
"expire_minutes": 10,
}))
}
#[get("/weather/details")]
async fn get_weather_details(
pool: web::Data<PgPool>,
claims: Option<web::ReqData<Claims>>,
query: web::Query<serde_json::Value>,
app_state: web::Data<AppState>,
) -> impl Responder {
let mut is_temp_token = false;
let (openid, weather_id) =
if let Some(temp_token) = query.get("temp_token").and_then(|v| v.as_str()) {
is_temp_token = true;
let temp_claims = match auth::verify_temp_token(temp_token, &app_state.jwt_secret) {
Ok(c) => c,
Err(e) => {
return HttpResponse::Unauthorized().json(ErrorResponse {
error: format!("临时token无效: {}", e),
errcode: Some(401),
errmsg: None,
});
}
};
(temp_claims.openid, temp_claims.resource_id)
} else if let Some(claims) = claims {
let weather_id = match query.get("id").and_then(|v| v.as_i64()) {
Some(id) => id as i32,
None => {
return HttpResponse::BadRequest().json(ErrorResponse {
error: "缺少资源ID参数id".to_string(),
errcode: Some(400),
errmsg: None,
});
}
};
(claims.openid.clone(), weather_id)
} else {
return HttpResponse::Unauthorized().json(ErrorResponse {
error: "缺少token需提供JWT或临时token".to_string(),
errcode: Some(401),
errmsg: None,
});
};
let weather_data = match db::get_weather_details(pool.get_ref(), weather_id).await {
Ok(data) => {
if data.openid != openid {
return HttpResponse::Forbidden().json(ErrorResponse {
error: "无权限访问该数据".to_string(),
errcode: Some(403),
errmsg: None,
});
}
data
}
Err(error_msg) => {
error!("获取天气数据详情失败: {}", error_msg);
let errcode = if error_msg.starts_with("未找到") {
404
} else {
500
};
return HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": errcode,
"errmsg": error_msg
}));
}
};
if is_temp_token {
let index_html = match TEMPLATES_DIR.get_file("index.html") {
Some(file) => file.contents_utf8().unwrap_or_default(),
None => {
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "无法找到模板文件".to_string(),
errcode: Some(500),
errmsg: None,
});
}
};
let weather_data_json = serde_json::to_string(&weather_data).unwrap_or_default();
let data_script = format!(
r#"
<script>
window.weatherData = {};
</script>
"#,
weather_data_json
);
let rendered_html = index_html
.replace("<!-- 详细信息将在这里显示 -->", &data_script)
.replace(
"style=\"display: none;\"",
"",
);
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(rendered_html)
} else {
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"data": weather_data
}))
}
}
#[get("/weather")]
async fn get_weather_brief(
query: web::Query<serde_json::Value>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> impl Responder {
let page = match query.get("page") {
Some(v) => {
let raw = if let Some(num) = v.as_i64() {
num as i32
} else if let Some(s) = v.as_str() {
s.parse().unwrap_or(1)
} else {
1
};
raw.max(1)
}
None => 1,
};
let limit = match query.get("limit") {
Some(v) => {
let raw = if let Some(num) = v.as_i64() {
num as i32
} else if let Some(s) = v.as_str() {
s.parse().unwrap_or(10)
} else {
10
};
raw.clamp(1, 100)
}
None => 10,
};
debug!("获取天气数据列表, 页码: {}, 每页条数: {}", page, limit);
match db::get_weather_list(pool.get_ref(), claims.user_id, page, limit).await {
Ok(response) => {
debug!(
"获取到的天气列表: {:?}, 总条数: {}",
response.list, response.total
);
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"data": response.list,
"page": page,
"limit": limit,
"total": response.total
}))
}
Err(error_msg) => {
error!("获取天气数据列表失败: {}", error_msg);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": error_msg
}))
}
}
}
// 删除天气数据
#[delete("/weather/delete/{id}")]
async fn delete_weather(
path: web::Path<i32>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> impl Responder {
let weather_id = path.into_inner();
info!("删除天气数据, ID: {}", weather_id);
match db::delete_weather_data(pool.get_ref(), weather_id, claims.user_id).await {
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
"success": true,
"message": format!("天气数据 {} 已成功删除", weather_id)
})),
Err(error_msg) => {
error!("删除天气数据失败: {}", error_msg);
let errcode = if error_msg.starts_with("未找到") {
404
} else {
500
};
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": errcode,
"errmsg": error_msg
}))
}
}
}
// 管理员获取用户信息
#[get("/api/admin/users/{id}")]
async fn admin_get_user(
path: web::Path<i32>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> impl Responder {
let target_user_id = path.into_inner();
info!("管理员获取用户信息, 目标用户ID: {}", target_user_id);
// 验证当前用户是否为管理员
let current_user = match db::get_user_by_id(pool.get_ref(), claims.user_id).await {
Ok(user) => user,
Err(e) => {
error!("获取当前用户信息失败: {}", e);
return HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": "获取用户信息失败"
}));
}
};
if !current_user.is_admin {
return HttpResponse::Forbidden().json(serde_json::json!({
"success": false,
"errcode": 403,
"errmsg": "无权限执行此操作"
}));
}
// 获取目标用户信息
match db::get_user_by_id(pool.get_ref(), target_user_id).await {
Ok(user) => HttpResponse::Ok().json(serde_json::json!({
"success": true,
"data": user
})),
Err(e) => {
error!("获取目标用户信息失败: {}", e);
let errcode = if e.starts_with("未找到") { 404 } else { 500 };
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": errcode,
"errmsg": e
}))
}
}
}
// 管理员更新用户付费状态
#[put("/api/admin/users/{id}/payment")]
async fn admin_update_user_payment(
path: web::Path<i32>,
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
body: web::Json<UpdatePaymentRequest>,
) -> impl Responder {
let target_user_id = path.into_inner();
info!("管理员更新用户付费状态, 目标用户ID: {}", target_user_id);
// 验证当前用户是否为管理员
let current_user = match db::get_user_by_id(pool.get_ref(), claims.user_id).await {
Ok(user) => user,
Err(e) => {
error!("获取当前用户信息失败: {}", e);
return HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": "获取用户信息失败"
}));
}
};
if !current_user.is_admin {
return HttpResponse::Forbidden().json(serde_json::json!({
"success": false,
"errcode": 403,
"errmsg": "无权限执行此操作"
}));
}
// 解析 paid_expires_at
let paid_expires_at = match &body.paid_expires_at {
Some(date_str) => match chrono::DateTime::parse_from_rfc3339(date_str) {
Ok(dt) => Some(dt.with_timezone(&chrono::Utc)),
Err(e) => {
return HttpResponse::BadRequest().json(serde_json::json!({
"success": false,
"errcode": 400,
"errmsg": format!("日期格式错误: {}", e)
}));
}
},
None => None,
};
// 更新用户付费状态
match db::update_user_payment_status(pool.get_ref(), target_user_id, body.is_paid, paid_expires_at).await {
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
"success": true,
"message": "用户付费状态已更新"
})),
Err(e) => {
error!("更新用户付费状态失败: {}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": e
}))
}
}
}
// 在 main.rs 中添加以下函数,用于处理静态文件请求
async fn serve_static_files(path: web::Path<String>) -> impl Responder {
// 获取请求的文件路径(例如 "css/style.css"
let file_path = path.into_inner();
// 【调试日志 1】打印服务器收到的文件路径
debug!("请求静态文件: {}", file_path);
// 从嵌入的 STATIC_DIR 中查找文件
match STATIC_DIR.get_file(&file_path) {
Some(file) => {
// 根据文件扩展名设置 Content-Type
let content_type = match file_path.split('.').next_back() {
Some("ico") => "image/x-icon",
Some("css") => "text/css",
Some("js") => "application/javascript",
Some("html") => "text/html",
Some("ttf") => "font/ttf",
Some("woff") => "font/woff",
Some("woff2") => "font/woff2",
_ => "application/octet-stream", // 默认类型
};
debug!(
"成功找到文件: {}, Content-Type: {}",
file_path, content_type
);
HttpResponse::Ok()
.content_type(content_type)
.body(file.contents())
}
None => {
// 文件不存在时返回 404
// 【调试日志 3】打印文件未找到的信息
error!("文件未找到: {}", file_path);
HttpResponse::NotFound().body("静态文件不存在")
}
}
}
// 获取当前用户信息(普通用户可用)
#[get("/api/user/profile")]
async fn get_current_user_profile(
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
) -> impl Responder {
info!("获取当前用户信息, 用户ID: {}", claims.user_id);
match db::get_user_by_id(pool.get_ref(), claims.user_id).await {
Ok(user) => {
let is_paid_active = user.is_paid &&
(user.paid_expires_at.is_none() || user.paid_expires_at.unwrap() > chrono::Utc::now());
HttpResponse::Ok().json(serde_json::json!({
"success": true,
"data": {
"id": user.id,
"name": user.name,
"nickname": user.nickname,
"avatarUrl": user.avatar_url,
"is_paid": user.is_paid,
"is_paid_active": is_paid_active,
"is_admin": user.is_admin,
"paid_expires_at": user.paid_expires_at
}
}))
},
Err(e) => {
error!("获取用户信息失败: {}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": e
}))
}
}
}
// 保存用户信息请求体
#[derive(Debug, Deserialize)]
pub struct SaveUserProfileRequest {
pub nickname: Option<String>,
pub avatar_url: Option<String>,
}
// 保存用户信息(普通用户可用)
#[put("/api/user/profile")]
async fn save_user_profile(
pool: web::Data<PgPool>,
claims: web::ReqData<Claims>,
body: web::Json<SaveUserProfileRequest>,
) -> impl Responder {
info!("保存用户信息, 用户ID: {}", claims.user_id);
match db::update_user_profile(pool.get_ref(), claims.user_id, &body.nickname, &body.avatar_url).await {
Ok(_) => {
HttpResponse::Ok().json(serde_json::json!({
"success": true
}))
},
Err(e) => {
error!("保存用户信息失败: {}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": e
}))
}
}
}
// 健康检查端点
#[get("/health")]
async fn health_check(pool: web::Data<PgPool>) -> impl Responder {
// 检查数据库连接
match sqlx::query("SELECT 1").fetch_one(pool.get_ref()).await {
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
"status": "healthy",
"database": "connected"
})),
Err(e) => {
tracing::error!("健康检查失败: {}", e);
HttpResponse::ServiceUnavailable().json(serde_json::json!({
"status": "unhealthy",
"database": "disconnected",
"error": e.to_string()
}))
}
}
}
fn create_server_config(
pool: PgPool,
http_client: Client,
app_state: AppState,
) -> App<
impl actix_web::dev::ServiceFactory<
actix_web::dev::ServiceRequest,
Config = (),
Response = actix_web::dev::ServiceResponse,
Error = actix_web::Error,
InitError = (),
>,
> {
App::new()
.app_data(web::Data::new(pool))
.app_data(web::Data::new(http_client))
.app_data(web::Data::new(app_state))
.service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files)))
.service(health_check)
.service(login)
.service(get_weather_details)
.service(
web::scope("")
.wrap(from_fn(jwt_middleware))
.service(post_weather_data)
.service(get_weather_brief)
.service(generate_temp_token_handler)
.service(delete_weather)
.service(get_current_user_profile)
.service(save_user_profile)
.service(admin_get_user)
.service(admin_update_user_payment),
)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
if let Err(e) = dotenvy::dotenv() {
tracing::warn!("加载.env文件失败使用系统环境变量: {}", e);
}
// 初始化文件日志JSON 格式,带轮转)
let log_dir = std::path::Path::new("./logs");
std::fs::create_dir_all(log_dir).ok(); // 确保日志目录存在
let file_appender = tracing_appender::rolling::Builder::new()
.rotation(tracing_appender::rolling::Rotation::DAILY)
.filename_prefix("rust-backend")
.filename_suffix("log")
.build(log_dir)
.expect("无法创建日志文件");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
// 保持文件 guard 存活(使用 Box 泄漏)
std::mem::forget(_guard);
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("rust_backend=info".parse().unwrap())
.add_directive("actix_web=info".parse().unwrap())
.add_directive("sqlx=warn".parse().unwrap()) // SQLx 日志太多,降级
)
.with_target(true)
.with_thread_ids(false) // 生产环境可开启
.with_file(true)
.with_line_number(true)
.with_writer(non_blocking)
.with_ansi(false) // 文件中不使用 ANSI 颜色
.json() // JSON 格式便于 ELK 解析
.init();
info!("日志系统初始化成功JSON格式输出到 ./logs/");
let app_state = match AppState::load() {
Ok(state) => state,
Err(e) => {
error!("环境变量配置错误: {}", e);
std::process::exit(1);
}
};
let pool = match create_pool().await {
Ok(pool) => pool,
Err(e) => {
error!("Failed to create database pool: {}", e);
error!("Please check your database connection configuration in .env file");
std::process::exit(1);
}
};
let http_client = Client::new();
info!("Attempting to start server...");
let ports = vec![4433, 8443, 8080, 3000, 8000, 8888];
let mut server: Option<
Pin<Box<dyn std::future::Future<Output = std::io::Result<()>> + Unpin>>,
> = None;
let mut bound_port = 0;
for port in &ports {
let addr = format!("127.0.0.1:{}", port);
info!("Trying to bind to {}", addr);
let pool_clone = pool.clone();
let http_client_clone = http_client.clone();
let app_state_clone = app_state.clone();
if *port == 443 || *port == 8443 {
let ssl_builder = match create_ssl_acceptor() {
Ok(builder) => builder,
Err(e) => {
error!("Failed to create SSL acceptor: {}", e);
continue;
}
};
match HttpServer::new(move || {
create_server_config(pool_clone.clone(), http_client_clone.clone(), app_state_clone.clone())
})
.bind_openssl(&addr, ssl_builder)
{
Ok(s) => {
info!("Successfully bound to {} with HTTPS", addr);
let server_future = s.run();
server = Some(Box::pin(server_future));
bound_port = *port;
break;
}
Err(e) => {
error!("Failed to bind to {} with HTTPS: {}", addr, e);
if e.kind() == std::io::ErrorKind::PermissionDenied {
error!(" -> Permission denied. Try running with sudo or use a port > 1024");
}
continue;
}
}
} else {
match HttpServer::new(move || {
create_server_config(pool_clone.clone(), http_client_clone.clone(), app_state_clone.clone())
})
.bind(&addr)
{
Ok(s) => {
info!("Successfully bound to {} with HTTP", addr);
let server_future = s.run();
server = Some(Box::pin(server_future));
bound_port = *port;
break;
}
Err(e) => {
error!("Failed to bind to {} with HTTP: {}", addr, e);
if e.kind() == std::io::ErrorKind::PermissionDenied {
error!(
" -> Permission denied. Try running with sudo or use a port > 1024"
);
}
continue;
}
}
}
}
match server {
Some(s) => {
if bound_port == 443 || bound_port == 8443 {
info!("Server started successfully with HTTPS");
} else {
info!("Server started successfully with HTTP");
}
s.await
}
None => {
error!("Failed to bind to any port. Please check your system configuration.");
std::process::exit(1);
}
}
}