HTTP API: moved everything back to functions

This commit is contained in:
Igor Katson 2022-12-08 23:56:14 +00:00
parent 1875599706
commit b52824df6f
No known key found for this signature in database
GPG key ID: B4EC22B66D61A3F5
2 changed files with 91 additions and 76 deletions

View file

@ -1,5 +1,7 @@
use anyhow::Context;
use axum::extract::{Path, Query, State};
use axum::response::IntoResponse;
use axum::routing::get;
use buffers::ByteString;
use dht::{Dht, DhtStats};
use http::StatusCode;
@ -12,9 +14,9 @@ use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use axum::{routing, Router};
use axum::Router;
use crate::http_api_error::{ApiError, WithErrorStatus};
use crate::http_api_error::{ApiError, ApiErrorExt};
use crate::session::{AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session};
use crate::torrent_manager::TorrentManagerHandle;
use crate::torrent_state::StatsSnapshot;
@ -37,80 +39,75 @@ impl HttpApi {
pub async fn make_http_api_and_run(self, addr: SocketAddr) -> anyhow::Result<()> {
let state = self.inner;
let api_description_body = 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",
"GET /torrents/{index}/stats": "Torrent stats",
// This is kind of not secure as it just reads any local file that it has access to,
// or any URL, but whatever, ok for our purposes / threat model.
"POST /torrents": "Add a torrent here. magnet: or http:// or a local file."
},
"server": "rqbit",
});
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",
"GET /torrents/{index}/stats": "Torrent stats",
// This is kind of not secure as it just reads any local file that it has access to,
// or any URL, but whatever, ok for our purposes / threat model.
"POST /torrents": "Add a torrent here. magnet: or http:// or a local file."
},
"server": "rqbit",
}))
}
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>,
url: String,
) -> Result<impl IntoResponse> {
let opts = params.into_add_torrent_options();
state.api_add_torrent(url, Some(opts)).await.map(axum::Json)
}
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)
}
async fn torrent_stats(
State(state): State<ApiState>,
Path(idx): Path<usize>,
) -> Result<impl IntoResponse> {
state.api_stats(idx).map(axum::Json)
}
let app = Router::new()
.route(
"/",
routing::get(move || async move { axum::Json(api_description_body) }),
)
.route(
"/dht/stats",
routing::get(|State(state): State<ApiState>| async move {
state.api_dht_stats().map(axum::Json)
}),
)
.route(
"/dht/table",
routing::get(|State(state): State<ApiState>| async move {
state.api_dht_table().map(axum::Json)
}),
)
.route(
"/torrents",
routing::get(|State(state): State<ApiState>| async move {
axum::Json(state.api_torrent_list())
}),
)
.route(
"/torrents",
routing::post(
|State(state): State<ApiState>,
Query(params): Query<TorrentAddQueryParams>,
url: String| async move {
let opts = params.into_add_torrent_options();
state.api_add_torrent(url, Some(opts)).await.map(axum::Json)
},
),
)
.route(
"/torrents/:id",
routing::get(
|State(state): State<ApiState>, Path(idx): Path<usize>| async move {
state.api_torrent_details(idx).map(axum::Json)
},
),
)
.route(
"/torrents/:id/haves",
routing::get(
|State(state): State<ApiState>, Path(idx): Path<usize>| async move {
state.api_dump_haves(idx)
},
),
)
.route(
"/torrents/:id/stats",
routing::get(
|State(state): State<ApiState>, Path(idx): Path<usize>| async move {
state.api_stats(idx).map(axum::Json)
},
),
)
.route("/", get(api_root))
.route("/dht/stats", get(dht_stats))
.route("/dht/table", get(dht_table))
.route("/torrents", get(torrents_list).post(torrents_post))
.route("/torrents/:id", get(torrent_details))
.route("/torrents/:id/haves", get(torrent_haves))
.route("/torrents/:id/stats", get(torrent_stats))
.with_state(state);
log::info!("starting HTTP server on {}", addr);

View file

@ -7,6 +7,7 @@ use serde::{Serialize, Serializer};
pub struct ApiError {
status: Option<StatusCode>,
kind: ApiErrorKind,
plaintext: bool,
}
impl ApiError {
@ -14,6 +15,7 @@ impl ApiError {
Self {
status: Some(StatusCode::NOT_FOUND),
kind: ApiErrorKind::TorrentNotFound(torrent_id),
plaintext: false,
}
}
@ -21,6 +23,7 @@ impl ApiError {
Self {
status: Some(StatusCode::NOT_FOUND),
kind: ApiErrorKind::DhtDisabled,
plaintext: false,
}
}
@ -32,6 +35,15 @@ impl ApiError {
Self {
status: Some(status),
kind: self.kind,
plaintext: self.plaintext,
}
}
pub fn with_plaintext_error(self, value: bool) -> Self {
Self {
status: self.status,
kind: self.kind,
plaintext: value,
}
}
}
@ -79,6 +91,7 @@ impl From<anyhow::Error> for ApiError {
Self {
status,
kind: ApiErrorKind::Other(value),
plaintext: false,
}
}
}
@ -110,15 +123,20 @@ impl IntoResponse for ApiError {
}
}
pub trait WithErrorStatus<T> {
pub trait ApiErrorExt<T> {
fn with_error_status_code(self, s: StatusCode) -> Result<T, ApiError>;
fn with_plaintext_error(self, value: bool) -> Result<T, ApiError>;
}
impl<T, E> WithErrorStatus<T> for std::result::Result<T, E>
impl<T, E> ApiErrorExt<T> for std::result::Result<T, E>
where
E: Into<ApiError>,
{
fn with_error_status_code(self, s: StatusCode) -> Result<T, ApiError> {
self.map_err(|e| e.into().with_status(s))
}
fn with_plaintext_error(self, value: bool) -> Result<T, ApiError> {
self.map_err(|e| e.into().with_plaintext_error(value))
}
}