2023-11-24 14:08:02 +00:00
use anyhow ::Context ;
2023-11-20 19:52:48 +00:00
use axum ::body ::Bytes ;
2022-12-08 11:06:29 +00:00
use axum ::extract ::{ Path , Query , State } ;
2022-12-08 23:56:14 +00:00
use axum ::response ::IntoResponse ;
2023-11-24 14:19:39 +00:00
use axum ::routing ::{ get , post } ;
2024-02-27 08:14:39 +00:00
use futures ::future ::BoxFuture ;
use futures ::{ FutureExt , TryStreamExt } ;
2023-11-22 17:19:35 +00:00
use itertools ::Itertools ;
2023-12-02 15:19:05 +00:00
2021-10-10 11:39:25 +01:00
use serde ::{ Deserialize , Serialize } ;
2024-04-24 19:10:17 +01:00
use std ::io ::SeekFrom ;
2021-10-10 09:57:21 +01:00
use std ::net ::SocketAddr ;
2023-12-01 09:30:23 +00:00
use std ::str ::FromStr ;
2023-11-30 16:05:48 +00:00
use std ::time ::Duration ;
2024-04-24 19:10:17 +01:00
use tokio ::io ::AsyncSeekExt ;
2023-12-17 10:25:56 +00:00
use tracing ::{ debug , info } ;
2022-12-04 12:53:55 +00:00
2022-12-08 23:56:14 +00:00
use axum ::Router ;
2021-06-30 10:14:33 +01:00
2023-12-02 15:19:05 +00:00
use crate ::api ::Api ;
2023-11-30 16:05:48 +00:00
use crate ::peer_connection ::PeerConnectionOptions ;
2023-12-08 19:47:48 +00:00
use crate ::session ::{ AddTorrent , AddTorrentOptions , SUPPORTED_SCHEMES } ;
2023-12-02 15:19:05 +00:00
use crate ::torrent_state ::peer ::stats ::snapshot ::PeerStatsFilter ;
2023-12-08 19:47:48 +00:00
type ApiState = Api ;
2023-12-02 15:19:05 +00:00
use crate ::api ::Result ;
2021-07-08 23:03:58 +01:00
2023-12-03 12:14:50 +00:00
/// An HTTP server for the API.
2022-12-08 15:40:29 +00:00
pub struct HttpApi {
2023-12-02 15:19:05 +00:00
inner : ApiState ,
2023-12-08 19:47:48 +00:00
opts : HttpApiOptions ,
}
#[ derive(Debug, Default) ]
pub struct HttpApiOptions {
pub read_only : bool ,
2022-12-08 09:28:01 +00:00
}
2022-12-08 15:40:29 +00:00
impl HttpApi {
2023-12-08 19:47:48 +00:00
pub fn new ( api : Api , opts : Option < HttpApiOptions > ) -> Self {
2022-12-08 09:28:01 +00:00
Self {
2023-12-08 19:47:48 +00:00
inner : api ,
opts : opts . unwrap_or_default ( ) ,
2022-12-08 09:28:01 +00:00
}
}
2022-12-08 11:06:29 +00:00
2023-12-03 12:14:50 +00:00
/// Run the HTTP server forever on the given address.
/// If read_only is passed, no state-modifying methods will be exposed.
2024-02-26 23:08:47 +00:00
#[ inline(never) ]
2024-02-27 08:14:39 +00:00
pub fn make_http_api_and_run ( self , addr : SocketAddr ) -> BoxFuture < 'static , anyhow ::Result < ( ) > > {
2022-12-08 15:40:29 +00:00
let state = self . inner ;
2022-12-08 23:56:14 +00:00
async fn api_root ( ) -> impl IntoResponse {
axum ::Json ( serde_json ::json! ( {
" apis " : {
" GET / " : " list all available APIs " ,
" GET /dht/stats " : " DHT stats " ,
" GET /dht/table " : " DHT routing table " ,
" GET /torrents " : " List torrents (default torrent is 0) " ,
" GET /torrents/{index} " : " Torrent details " ,
" GET /torrents/{index}/haves " : " The bitfield of have pieces " ,
2023-11-25 01:24:57 +00:00
" GET /torrents/{index}/stats/v1 " : " Torrent stats " ,
2023-11-20 13:55:42 +00:00
" GET /torrents/{index}/peer_stats " : " Per peer stats " ,
2023-11-25 01:24:57 +00:00
" POST /torrents/{index}/pause " : " Pause torrent " ,
" POST /torrents/{index}/start " : " Resume torrent " ,
" POST /torrents/{index}/forget " : " Forget about the torrent, keep the files " ,
" POST /torrents/{index}/delete " : " Forget about the torrent, remove the files " ,
2024-03-30 20:36:56 +00:00
" POST /torrents/{index}/update_only_files " : " Change the selection of files to download. You need to POST json of the following form { \" only_files \" : [0, 1, 2]} " ,
2023-11-21 12:56:07 +00:00
" POST /torrents " : " Add a torrent here. magnet: or http:// or a local file. " ,
2023-11-25 01:24:57 +00:00
" POST /rust_log " : " Set RUST_LOG to this post launch (for debugging) " ,
2023-11-21 12:56:07 +00:00
" GET /web/ " : " Web UI " ,
2022-12-08 23:56:14 +00:00
} ,
" server " : " rqbit " ,
2023-12-07 12:19:35 +00:00
" version " : env ! ( " CARGO_PKG_VERSION " ) ,
2022-12-08 23:56:14 +00:00
} ) )
}
async fn dht_stats ( State ( state ) : State < ApiState > ) -> Result < impl IntoResponse > {
state . api_dht_stats ( ) . map ( axum ::Json )
}
async fn dht_table ( State ( state ) : State < ApiState > ) -> Result < impl IntoResponse > {
state . api_dht_table ( ) . map ( axum ::Json )
}
async fn torrents_list ( State ( state ) : State < ApiState > ) -> impl IntoResponse {
axum ::Json ( state . api_torrent_list ( ) )
}
async fn torrents_post (
State ( state ) : State < ApiState > ,
Query ( params ) : Query < TorrentAddQueryParams > ,
2023-11-20 19:52:48 +00:00
data : Bytes ,
2022-12-08 23:56:14 +00:00
) -> Result < impl IntoResponse > {
2023-12-01 11:28:35 +00:00
let is_url = params . is_url ;
2022-12-08 23:56:14 +00:00
let opts = params . into_add_torrent_options ( ) ;
2023-12-01 11:28:35 +00:00
let data = data . to_vec ( ) ;
let add = match is_url {
Some ( true ) = > AddTorrent ::Url (
String ::from_utf8 ( data )
. context ( " invalid utf-8 for passed URL " ) ?
. into ( ) ,
) ,
Some ( false ) = > AddTorrent ::TorrentFileBytes ( data . into ( ) ) ,
// Guess the format.
None if SUPPORTED_SCHEMES
. iter ( )
. any ( | s | data . starts_with ( s . as_bytes ( ) ) ) = >
{
AddTorrent ::Url (
String ::from_utf8 ( data )
. context ( " invalid utf-8 for passed URL " ) ?
. into ( ) ,
)
}
_ = > AddTorrent ::TorrentFileBytes ( data . into ( ) ) ,
2023-11-20 19:52:48 +00:00
} ;
state . api_add_torrent ( add , Some ( opts ) ) . await . map ( axum ::Json )
2022-12-08 23:56:14 +00:00
}
async fn torrent_details (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
state . api_torrent_details ( idx ) . map ( axum ::Json )
}
async fn torrent_haves (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
state . api_dump_haves ( idx )
}
2023-11-24 15:04:36 +00:00
async fn torrent_stats_v0 (
2022-12-08 23:56:14 +00:00
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
2023-11-24 15:04:36 +00:00
state . api_stats_v0 ( idx ) . map ( axum ::Json )
}
async fn torrent_stats_v1 (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
state . api_stats_v1 ( idx ) . map ( axum ::Json )
2022-12-08 23:56:14 +00:00
}
2022-12-08 09:28:01 +00:00
2023-11-20 13:55:42 +00:00
async fn peer_stats (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
Query ( filter ) : Query < PeerStatsFilter > ,
) -> Result < impl IntoResponse > {
state . api_peer_stats ( idx , filter ) . map ( axum ::Json )
}
2024-04-24 19:10:17 +01:00
async fn torrent_stream_file (
State ( state ) : State < ApiState > ,
Path ( ( idx , file_id ) ) : Path < ( usize , usize ) > ,
headers : http ::HeaderMap ,
) -> Result < impl IntoResponse > {
let mut stream = state . api_stream ( idx , file_id ) ? ;
if let Some ( range ) = headers . get ( http ::header ::RANGE ) {
let offset : Option < u64 > = range
. to_str ( )
. ok ( )
. and_then ( | s | s . strip_prefix ( " bytes= " ) )
. and_then ( | s | s . strip_suffix ( '-' ) )
. and_then ( | s | s . parse ( ) . ok ( ) ) ;
if let Some ( offset ) = offset {
stream
. seek ( SeekFrom ::Start ( offset ) )
. await
. context ( " error seeking " ) ? ;
}
}
let s = tokio_util ::io ::ReaderStream ::new ( stream ) ;
Ok ( axum ::body ::Body ::from_stream ( s ) )
}
2023-11-24 14:19:39 +00:00
async fn torrent_action_pause (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
2023-11-24 18:28:46 +00:00
state . api_torrent_action_pause ( idx ) . map ( axum ::Json )
2023-11-24 14:19:39 +00:00
}
async fn torrent_action_start (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
2023-11-24 18:28:46 +00:00
state . api_torrent_action_start ( idx ) . map ( axum ::Json )
}
async fn torrent_action_forget (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
state . api_torrent_action_forget ( idx ) . map ( axum ::Json )
}
async fn torrent_action_delete (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
) -> Result < impl IntoResponse > {
state . api_torrent_action_delete ( idx ) . map ( axum ::Json )
2023-11-24 14:19:39 +00:00
}
2024-03-30 17:55:43 +00:00
#[ derive(Deserialize) ]
struct UpdateOnlyFilesRequest {
only_files : Vec < usize > ,
}
async fn torrent_action_update_only_files (
State ( state ) : State < ApiState > ,
Path ( idx ) : Path < usize > ,
axum ::Json ( req ) : axum ::Json < UpdateOnlyFilesRequest > ,
) -> Result < impl IntoResponse > {
state
. api_torrent_action_update_only_files ( idx , & req . only_files . into_iter ( ) . collect ( ) )
. map ( axum ::Json )
}
2023-11-25 01:24:57 +00:00
async fn set_rust_log (
State ( state ) : State < ApiState > ,
new_value : String ,
) -> Result < impl IntoResponse > {
state . api_set_rust_log ( new_value ) . map ( axum ::Json )
}
2023-12-08 19:47:48 +00:00
async fn stream_logs ( State ( state ) : State < ApiState > ) -> Result < impl IntoResponse > {
2023-12-09 14:03:42 +00:00
let s = state . api_log_lines_stream ( ) ? . map_err ( | e | {
debug! ( error = % e , " stream_logs " ) ;
e
} ) ;
2023-12-08 19:47:48 +00:00
Ok ( axum ::body ::Body ::from_stream ( s ) )
}
2023-11-20 20:15:40 +00:00
let mut app = Router ::new ( )
2022-12-08 23:56:14 +00:00
. route ( " / " , get ( api_root ) )
2023-12-08 19:47:48 +00:00
. route ( " /stream_logs " , get ( stream_logs ) )
2023-11-25 11:21:45 +00:00
. route ( " /rust_log " , post ( set_rust_log ) )
2022-12-08 23:56:14 +00:00
. route ( " /dht/stats " , get ( dht_stats ) )
. route ( " /dht/table " , get ( dht_table ) )
2023-11-25 11:21:45 +00:00
. route ( " /torrents " , get ( torrents_list ) )
2022-12-08 23:56:14 +00:00
. route ( " /torrents/:id " , get ( torrent_details ) )
. route ( " /torrents/:id/haves " , get ( torrent_haves ) )
2023-11-24 15:04:36 +00:00
. route ( " /torrents/:id/stats " , get ( torrent_stats_v0 ) )
. route ( " /torrents/:id/stats/v1 " , get ( torrent_stats_v1 ) )
2024-04-24 19:10:17 +01:00
. route ( " /torrents/:id/peer_stats " , get ( peer_stats ) )
. route ( " /torrents/:id/stream/:file_id " , get ( torrent_stream_file ) ) ;
2023-11-25 11:21:45 +00:00
2023-12-08 19:47:48 +00:00
if ! self . opts . read_only {
2023-11-25 11:21:45 +00:00
app = app
. route ( " /torrents " , post ( torrents_post ) )
. route ( " /torrents/:id/pause " , post ( torrent_action_pause ) )
. route ( " /torrents/:id/start " , post ( torrent_action_start ) )
. route ( " /torrents/:id/forget " , post ( torrent_action_forget ) )
2024-03-30 17:55:43 +00:00
. route ( " /torrents/:id/delete " , post ( torrent_action_delete ) )
. route (
" /torrents/:id/update_only_files " ,
post ( torrent_action_update_only_files ) ,
) ;
2023-11-25 11:21:45 +00:00
}
2023-11-20 20:15:40 +00:00
#[ cfg(feature = " webui " ) ]
{
let webui_router = Router ::new ( )
. route (
" / " ,
get ( | | async {
(
[ ( " Content-Type " , " text/html " ) ] ,
2023-11-21 03:43:32 +00:00
include_str! ( " ../webui/dist/index.html " ) ,
2023-11-20 20:15:40 +00:00
)
} ) ,
)
. route (
2023-11-27 17:21:45 +00:00
" /assets/index.js " ,
2023-11-20 20:15:40 +00:00
get ( | | async {
(
[ ( " Content-Type " , " application/javascript " ) ] ,
2023-11-27 17:21:45 +00:00
include_str! ( " ../webui/dist/assets/index.js " ) ,
)
} ) ,
)
2023-12-14 10:37:29 +00:00
. route (
" /assets/index.css " ,
get ( | | async {
(
[ ( " Content-Type " , " text/css " ) ] ,
include_str! ( " ../webui/dist/assets/index.css " ) ,
)
} ) ,
)
2023-11-27 17:21:45 +00:00
. route (
" /assets/logo.svg " ,
get ( | | async {
(
[ ( " Content-Type " , " image/svg+xml " ) ] ,
include_str! ( " ../webui/dist/assets/logo.svg " ) ,
2023-11-20 20:15:40 +00:00
)
} ) ,
) ;
2023-12-08 19:47:48 +00:00
app = app . nest ( " /web/ " , webui_router ) ;
}
2023-11-20 22:10:01 +00:00
2023-12-17 10:25:56 +00:00
let cors_layer = {
2023-12-08 19:47:48 +00:00
use tower_http ::cors ::{ AllowHeaders , AllowOrigin } ;
2023-12-17 10:25:56 +00:00
const ALLOWED_ORIGINS : [ & [ u8 ] ; 4 ] = [
// Webui-dev
b " http://localhost:3031 " ,
b " http://127.0.0.1:3031 " ,
// Tauri dev
b " http://localhost:1420 " ,
// Tauri prod
b " tauri://localhost " ,
] ;
2023-12-08 19:47:48 +00:00
tower_http ::cors ::CorsLayer ::default ( )
2023-12-17 10:25:56 +00:00
. allow_origin ( AllowOrigin ::predicate ( | v , _ | {
ALLOWED_ORIGINS . contains ( & v . as_bytes ( ) )
} ) )
2023-12-08 19:47:48 +00:00
. allow_headers ( AllowHeaders ::any ( ) )
} ;
2023-11-20 20:15:40 +00:00
let app = app
2023-12-09 00:26:14 +00:00
. layer ( cors_layer )
2023-11-20 19:52:48 +00:00
. layer ( tower_http ::trace ::TraceLayer ::new_for_http ( ) )
2023-11-20 20:15:40 +00:00
. with_state ( state )
. into_make_service ( ) ;
2022-12-08 09:28:01 +00:00
2023-12-09 00:26:14 +00:00
info! ( % addr , " starting HTTP server " ) ;
2023-12-01 11:56:07 +00:00
use tokio ::net ::TcpListener ;
2024-02-26 22:52:53 +00:00
async move {
let listener = TcpListener ::bind ( & addr )
. await
. with_context ( | | format! ( " error binding to {addr} " ) ) ? ;
axum ::serve ( listener , app ) . await ? ;
Ok ( ( ) )
}
. boxed ( )
2022-12-08 09:28:01 +00:00
}
}
2023-12-03 12:14:50 +00:00
pub ( crate ) struct OnlyFiles ( Vec < usize > ) ;
pub ( crate ) struct InitialPeers ( pub Vec < SocketAddr > ) ;
2023-12-02 12:48:03 +00:00
#[ derive(Serialize, Deserialize, Default) ]
2023-12-03 12:14:50 +00:00
pub ( crate ) struct TorrentAddQueryParams {
2023-12-02 12:48:03 +00:00
pub overwrite : Option < bool > ,
pub output_folder : Option < String > ,
pub sub_folder : Option < String > ,
pub only_files_regex : Option < String > ,
pub only_files : Option < OnlyFiles > ,
pub peer_connect_timeout : Option < u64 > ,
pub peer_read_write_timeout : Option < u64 > ,
pub initial_peers : Option < InitialPeers > ,
// Will force interpreting the content as a URL.
pub is_url : Option < bool > ,
pub list_only : Option < bool > ,
}
2023-11-22 17:19:35 +00:00
impl Serialize for OnlyFiles {
fn serialize < S > ( & self , serializer : S ) -> core ::result ::Result < S ::Ok , S ::Error >
where
S : serde ::Serializer ,
{
let s = self . 0. iter ( ) . map ( | id | id . to_string ( ) ) . join ( " , " ) ;
s . serialize ( serializer )
}
}
impl < ' de > Deserialize < ' de > for OnlyFiles {
fn deserialize < D > ( deserializer : D ) -> core ::result ::Result < Self , D ::Error >
where
D : serde ::Deserializer < ' de > ,
{
use serde ::de ::Error ;
let s = String ::deserialize ( deserializer ) ? ;
let list = s
. split ( ',' )
. try_fold ( Vec ::< usize > ::new ( ) , | mut acc , c | match c . parse ( ) {
Ok ( i ) = > {
acc . push ( i ) ;
Ok ( acc )
}
Err ( _ ) = > Err ( D ::Error ::custom ( format! (
" only_files: failed to parse {:?} as integer " ,
c
) ) ) ,
} ) ? ;
if list . is_empty ( ) {
return Err ( D ::Error ::custom (
" only_files: should contain at least one file id " ,
) ) ;
}
Ok ( OnlyFiles ( list ) )
2023-11-22 15:26:24 +00:00
}
}
2023-12-01 09:30:23 +00:00
impl < ' de > Deserialize < ' de > for InitialPeers {
fn deserialize < D > ( deserializer : D ) -> std ::prelude ::v1 ::Result < Self , D ::Error >
where
D : serde ::Deserializer < ' de > ,
{
use serde ::de ::Error ;
let string = String ::deserialize ( deserializer ) ? ;
let mut addrs = Vec ::new ( ) ;
2023-12-01 10:28:20 +00:00
for addr_str in string . split ( ',' ) . filter ( | s | ! s . is_empty ( ) ) {
2023-12-01 09:30:23 +00:00
addrs . push ( SocketAddr ::from_str ( addr_str ) . map_err ( D ::Error ::custom ) ? ) ;
}
Ok ( InitialPeers ( addrs ) )
}
}
impl Serialize for InitialPeers {
fn serialize < S > ( & self , serializer : S ) -> std ::prelude ::v1 ::Result < S ::Ok , S ::Error >
where
S : serde ::Serializer ,
{
self . 0
. iter ( )
. map ( | s | s . to_string ( ) )
. join ( " , " )
. serialize ( serializer )
}
}
2022-12-08 15:40:29 +00:00
impl TorrentAddQueryParams {
2023-12-02 12:48:03 +00:00
pub fn into_add_torrent_options ( self ) -> AddTorrentOptions {
2022-12-08 15:40:29 +00:00
AddTorrentOptions {
overwrite : self . overwrite . unwrap_or ( false ) ,
only_files_regex : self . only_files_regex ,
2023-11-22 17:19:35 +00:00
only_files : self . only_files . map ( | o | o . 0 ) ,
2022-12-08 15:40:29 +00:00
output_folder : self . output_folder ,
sub_folder : self . sub_folder ,
list_only : self . list_only . unwrap_or ( false ) ,
2023-12-01 09:30:23 +00:00
initial_peers : self . initial_peers . map ( | i | i . 0 ) ,
2023-11-30 16:05:48 +00:00
peer_opts : Some ( PeerConnectionOptions {
connect_timeout : self . peer_connect_timeout . map ( Duration ::from_secs ) ,
read_write_timeout : self . peer_read_write_timeout . map ( Duration ::from_secs ) ,
.. Default ::default ( )
} ) ,
2022-12-08 15:40:29 +00:00
.. Default ::default ( )
}
}
}