use crate::api::api_blacklists::{
api_service_blacklist_delete,
api_service_blacklist_get,
api_service_blacklist_post,
api_service_blacklists_delete,
api_service_blacklists_get,
api_service_blacklists_post
};
use crate::api::api_certificate::{
api_service_certificate_reload,
api_service_certificate_status
};
use crate::api::api_keys::{
api_service_key_delete,
api_service_key_get,
api_service_key_post,
api_service_keys_delete,
api_service_keys_get,
api_service_keys_post
};
use crate::api::api_stats::{
api_service_prom_get,
api_service_stats_get
};
use crate::api::api_torrents::{
api_service_torrent_delete,
api_service_torrent_get,
api_service_torrent_post,
api_service_torrents_delete,
api_service_torrents_get,
api_service_torrents_post
};
use crate::api::api_users::{
api_service_user_delete,
api_service_user_get,
api_service_user_post,
api_service_users_delete,
api_service_users_get,
api_service_users_post
};
use crate::api::api_whitelists::{
api_service_whitelist_delete,
api_service_whitelist_get,
api_service_whitelist_post,
api_service_whitelists_delete,
api_service_whitelists_get,
api_service_whitelists_post
};
use crate::api::structs::api_service_data::ApiServiceData;
use crate::common::common::hex2bin;
use crate::common::structs::custom_error::CustomError;
use crate::config::structs::api_trackers_config::ApiTrackersConfig;
use crate::config::structs::configuration::Configuration;
use crate::security::security::{
constant_time_eq,
validate_remote_ip
};
use crate::ssl::enums::server_identifier::ServerIdentifier;
use crate::ssl::structs::dynamic_certificate_resolver::DynamicCertificateResolver;
use crate::stats::enums::stats_event::StatsEvent;
use crate::tracker::structs::info_hash::InfoHash;
use crate::tracker::structs::torrent_tracker::TorrentTracker;
use actix_cors::Cors;
use actix_web::dev::ServerHandle;
use actix_web::http::header::ContentType;
use actix_web::web::{
BytesMut,
Data,
ServiceConfig
};
use actix_web::{
http,
web,
App,
HttpRequest,
HttpResponse,
HttpServer
};
use futures_util::StreamExt;
use log::{
error,
info
};
use serde_json::json;
use std::future::Future;
use std::net::{
IpAddr,
SocketAddr
};
use std::process::exit;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use utoipa_swagger_ui::{
Config,
SwaggerUi
};
pub fn api_service_cors() -> Cors
{
Cors::default()
.send_wildcard()
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
.allowed_headers(vec![http::header::X_FORWARDED_FOR, http::header::ACCEPT])
.allowed_header(http::header::CONTENT_TYPE)
.max_age(1)
}
pub fn api_service_routes(data: Arc<ApiServiceData>) -> Box<dyn Fn(&mut ServiceConfig) + Send + Sync>
{
Box::new(move |cfg: &mut ServiceConfig| {
cfg.app_data(Data::new(Arc::clone(&data)));
cfg.default_service(web::route().to(api_service_not_found));
cfg.service(web::resource("stats")
.route(web::get().to(api_service_stats_get)));
cfg.service(web::resource("metrics")
.route(web::get().to(api_service_prom_get)));
cfg.service(web::resource("api/torrent/{info_hash}")
.route(web::get().to(api_service_torrent_get))
.route(web::delete().to(api_service_torrent_delete))
);
cfg.service(web::resource("api/torrent/{info_hash}/{completed}")
.route(web::post().to(api_service_torrent_post)));
cfg.service(web::resource("api/torrents")
.route(web::get().to(api_service_torrents_get))
.route(web::post().to(api_service_torrents_post))
.route(web::delete().to(api_service_torrents_delete))
);
cfg.service(web::resource("api/whitelist/{info_hash}")
.route(web::get().to(api_service_whitelist_get))
.route(web::post().to(api_service_whitelist_post))
.route(web::delete().to(api_service_whitelist_delete))
);
cfg.service(web::resource("api/whitelists")
.route(web::get().to(api_service_whitelists_get))
.route(web::post().to(api_service_whitelists_post))
.route(web::delete().to(api_service_whitelists_delete))
);
cfg.service(web::resource("api/blacklist/{info_hash}")
.route(web::get().to(api_service_blacklist_get))
.route(web::post().to(api_service_blacklist_post))
.route(web::delete().to(api_service_blacklist_delete))
);
cfg.service(web::resource("api/blacklists")
.route(web::get().to(api_service_blacklists_get))
.route(web::post().to(api_service_blacklists_post))
.route(web::delete().to(api_service_blacklists_delete))
);
cfg.service(web::resource("api/key/{key_hash}")
.route(web::get().to(api_service_key_get))
.route(web::delete().to(api_service_key_delete))
);
cfg.service(web::resource("api/key/{key_hash}/{timeout}")
.route(web::post().to(api_service_key_post))
);
cfg.service(web::resource("api/keys")
.route(web::get().to(api_service_keys_get))
.route(web::post().to(api_service_keys_post))
.route(web::delete().to(api_service_keys_delete))
);
cfg.service(web::resource("api/user/{id}")
.route(web::get().to(api_service_user_get))
.route(web::delete().to(api_service_user_delete))
);
cfg.service(web::resource("api/user/{id}/{key}/{uploaded}/{downloaded}/{completed}/{updated}/{active}")
.route(web::post().to(api_service_user_post))
);
cfg.service(web::resource("api/users")
.route(web::get().to(api_service_users_get))
.route(web::post().to(api_service_users_post))
.route(web::delete().to(api_service_users_delete))
);
cfg.service(web::resource("api/certificate/reload")
.route(web::post().to(api_service_certificate_reload))
);
cfg.service(web::resource("api/certificate/status")
.route(web::get().to(api_service_certificate_status))
);
if data.torrent_tracker.config.tracker_config.swagger {
cfg.service(SwaggerUi::new("/swagger-ui/{_:.*}")
.config(Config::new(["/api/openapi.json"])));
cfg.service(web::resource("/api/openapi.json")
.route(web::get().to(api_service_openapi_json))
);
}
})
}
pub async fn api_service(
addr: SocketAddr,
data: Arc<TorrentTracker>,
api_server_object: ApiTrackersConfig
) -> (ServerHandle, impl Future<Output=Result<(), std::io::Error>>)
{
let keep_alive = api_server_object.keep_alive;
let request_timeout = api_server_object.request_timeout;
let disconnect_timeout = api_server_object.disconnect_timeout;
let worker_threads = api_server_object.threads as usize;
let api_service_data = Arc::new(ApiServiceData {
torrent_tracker: Arc::clone(&data),
api_trackers_config: Arc::new(api_server_object.clone()),
});
let app_factory = move || {
let cors = api_service_cors();
let sentry_wrap = sentry_actix::Sentry::new();
App::new()
.wrap(cors)
.wrap(sentry_wrap)
.configure(api_service_routes(Arc::clone(&api_service_data)))
};
if api_server_object.ssl {
info!("[APIS] Starting server listener with SSL on {addr}");
if api_server_object.ssl_key.is_empty() || api_server_object.ssl_cert.is_empty() {
error!("[APIS] No SSL key or SSL certificate given, exiting...");
exit(1);
}
let server_id = ServerIdentifier::ApiServer(addr.to_string());
if let Err(e) = data.certificate_store.load_certificate(
server_id.clone(),
&api_server_object.ssl_cert,
&api_server_object.ssl_key,
) {
panic!("[APIS] Failed to load SSL certificate: {e}");
}
let resolver = match DynamicCertificateResolver::new(
Arc::clone(&data.certificate_store),
server_id,
) {
Ok(resolver) => Arc::new(resolver),
Err(e) => panic!("[APIS] Failed to create certificate resolver: {e}"),
};
let tls_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_cert_resolver(resolver);
let server = HttpServer::new(app_factory)
.keep_alive(Duration::from_secs(keep_alive))
.client_request_timeout(Duration::from_secs(request_timeout))
.client_disconnect_timeout(Duration::from_secs(disconnect_timeout))
.workers(worker_threads)
.bind_rustls_0_23((addr.ip(), addr.port()), tls_config)
.unwrap()
.disable_signals()
.run();
return (server.handle(), server);
}
info!("[API] Starting server listener on {addr}");
let server = HttpServer::new(app_factory)
.keep_alive(Duration::from_secs(keep_alive))
.client_request_timeout(Duration::from_secs(request_timeout))
.client_disconnect_timeout(Duration::from_secs(disconnect_timeout))
.workers(worker_threads)
.bind((addr.ip(), addr.port()))
.unwrap()
.disable_signals()
.run();
(server.handle(), server)
}
pub async fn api_service_stats_log(ip: IpAddr, tracker: Arc<TorrentTracker>)
{
let event = if ip.is_ipv4() {
StatsEvent::Tcp4ConnectionsHandled
} else {
StatsEvent::Tcp6ConnectionsHandled
};
tracker.update_stats(event, 1);
}
pub async fn api_service_token(token: Option<String>, config: Arc<Configuration>) -> Option<HttpResponse>
{
let token_code = match token {
Some(token) => token,
None => {
return Some(HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({
"status": "missing token"
})));
}
};
if !constant_time_eq(&token_code, &config.tracker_config.api_key) {
return Some(HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({
"status": "invalid token"
})));
}
None
}
pub async fn api_service_retrieve_remote_ip(request: &HttpRequest, data: Arc<ApiTrackersConfig>) -> Result<IpAddr, ()>
{
let origin_ip = request.peer_addr().map(|addr| addr.ip()).ok_or(())?;
if !data.trusted_proxies {
return Ok(origin_ip);
}
request.headers()
.get(&data.real_ip)
.and_then(|header| header.to_str().ok())
.and_then(|ip_str| {
validate_remote_ip(ip_str, data.trusted_proxies).ok()?;
IpAddr::from_str(ip_str).ok()
})
.map_or(Ok(origin_ip), Ok)
}
pub async fn api_validate_ip(request: &HttpRequest, data: Data<Arc<ApiServiceData>>) -> Result<IpAddr, HttpResponse>
{
match api_service_retrieve_remote_ip(request, Arc::clone(&data.api_trackers_config)).await {
Ok(ip) => {
api_service_stats_log(ip, Arc::clone(&data.torrent_tracker)).await;
Ok(ip)
}
Err(()) => {
Err(HttpResponse::Ok().content_type(ContentType::json()).json(json!({
"status": "invalid ip"
})))
}
}
}
pub async fn api_service_not_found(request: HttpRequest, data: Data<Arc<ApiServiceData>>) -> HttpResponse
{
if let Some(error_return) = api_validation(&request, &data).await {
return error_return;
}
HttpResponse::NotFound().content_type(ContentType::json()).json(json!({
"status": "not found"
}))
}
pub fn api_stat_update(ip: IpAddr, data: Arc<TorrentTracker>, stats_ipv4: StatsEvent, stat_ipv6: StatsEvent, count: i64)
{
let event = if ip.is_ipv4() {
stats_ipv4
} else {
stat_ipv6
};
data.update_stats(event, count);
}
pub async fn api_validation(request: &HttpRequest, data: &Data<Arc<ApiServiceData>>) -> Option<HttpResponse>
{
match api_validate_ip(request, data.clone()).await {
Ok(ip) => {
api_stat_update(
ip,
Arc::clone(&data.torrent_tracker),
StatsEvent::Tcp4ApiHandled,
StatsEvent::Tcp6ApiHandled,
1
);
None
}
Err(result) => Some(result),
}
}
pub async fn api_service_openapi_json() -> HttpResponse
{
let openapi_file = include_str!("../openapi.json");
HttpResponse::Ok().content_type(ContentType::json()).body(openapi_file)
}
pub async fn api_parse_body(mut payload: web::Payload) -> Result<BytesMut, CustomError>
{
let mut body = BytesMut::new();
while let Some(chunk) = payload.next().await {
let chunk = chunk.map_err(|_| CustomError::new("chunk error"))?;
if body.len() + chunk.len() > 1_048_576 {
return Err(CustomError::new("chunk size exceeded"));
}
body.extend_from_slice(&chunk);
}
Ok(body)
}
pub fn parse_info_hash(info: &str) -> Result<InfoHash, HttpResponse>
{
if info.len() != 40 {
return Err(HttpResponse::BadRequest()
.content_type(ContentType::json())
.json(json!({"status": "bad info_hash"})));
}
match hex2bin(info.to_string()) {
Ok(hash) => Ok(InfoHash(hash)),
Err(_) => Err(HttpResponse::BadRequest()
.content_type(ContentType::json())
.json(json!({"status": "invalid info_hash"}))),
}
}