Skip to content

Commit

Permalink
Merge pull request #279 from ikatson/http-api-auth
Browse files Browse the repository at this point in the history
Basic auth in HTTP API
  • Loading branch information
ikatson authored Nov 21, 2024
2 parents 09c9659 + 6213481 commit 3378ecb
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 5 deletions.
11 changes: 11 additions & 0 deletions crates/librqbit/src/api_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ impl ApiError {
}
}

pub const fn unathorized() -> Self {
Self {
status: Some(StatusCode::UNAUTHORIZED),
kind: ApiErrorKind::Unauthorized,
plaintext: true,
}
}

pub fn status(&self) -> StatusCode {
self.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
}
Expand All @@ -80,6 +88,7 @@ impl ApiError {
enum ApiErrorKind {
TorrentNotFound(TorrentIdOrHash),
DhtDisabled,
Unauthorized,
Text(&'static str),
Other(anyhow::Error),
}
Expand All @@ -102,6 +111,7 @@ impl Serialize for ApiError {
error_kind: match self.kind {
ApiErrorKind::TorrentNotFound(_) => "torrent_not_found",
ApiErrorKind::DhtDisabled => "dht_disabled",
ApiErrorKind::Unauthorized => "unathorized",
ApiErrorKind::Other(_) => "internal_error",
ApiErrorKind::Text(_) => "internal_error",
},
Expand Down Expand Up @@ -142,6 +152,7 @@ impl std::fmt::Display for ApiError {
match &self.kind {
ApiErrorKind::TorrentNotFound(idx) => write!(f, "torrent {idx} not found"),
ApiErrorKind::Other(err) => write!(f, "{err:?}"),
ApiErrorKind::Unauthorized => write!(f, "unathorized"),
ApiErrorKind::DhtDisabled => write!(f, "DHT is disabled"),
ApiErrorKind::Text(t) => write!(f, "{t}"),
}
Expand Down
54 changes: 52 additions & 2 deletions crates/librqbit/src/http_api.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use anyhow::Context;
use axum::body::Bytes;
use axum::extract::{ConnectInfo, Path, Query, Request, State};
use axum::middleware::Next;
use axum::response::IntoResponse;
use axum::routing::{get, post};
use base64::Engine;
use bencode::AsDisplay;
use buffers::ByteBuf;
use futures::future::BoxFuture;
Expand All @@ -19,7 +21,7 @@ use std::time::Duration;
use tokio::io::AsyncSeekExt;
use tokio::net::TcpListener;
use tower_http::trace::{DefaultOnFailure, DefaultOnResponse, OnFailure};
use tracing::{debug, error_span, trace, Span};
use tracing::{debug, error_span, info, trace, Span};

use axum::{Json, Router};

Expand All @@ -43,6 +45,41 @@ pub struct HttpApi {
#[derive(Debug, Default)]
pub struct HttpApiOptions {
pub read_only: bool,
pub basic_auth: Option<(String, String)>,
}

async fn simple_basic_auth(
expected_username: Option<&str>,
expected_password: Option<&str>,
headers: HeaderMap,
request: axum::extract::Request,
next: Next,
) -> Result<axum::response::Response> {
let (expected_user, expected_pass) = match (expected_username, expected_password) {
(Some(u), Some(p)) => (u, p),
_ => return Ok(next.run(request).await),
};
let user_pass = headers
.get("Authorization")
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix("Basic "))
.and_then(|v| base64::engine::general_purpose::STANDARD.decode(v).ok())
.and_then(|v| String::from_utf8(v).ok());
let user_pass = match user_pass {
Some(user_pass) => user_pass,
None => {
return Ok((
StatusCode::UNAUTHORIZED,
[("WWW-Authenticate", "Basic realm=\"API\"")],
)
.into_response())
}
};
// TODO: constant time compare
match user_pass.split_once(':') {
Some((u, p)) if u == expected_user && p == expected_pass => Ok(next.run(request).await),
_ => Err(ApiError::unathorized()),
}
}

impl HttpApi {
Expand All @@ -57,7 +94,7 @@ impl HttpApi {
/// If read_only is passed, no state-modifying methods will be exposed.
#[inline(never)]
pub fn make_http_api_and_run(
self,
mut self,
listener: TcpListener,
upnp_router: Option<Router>,
) -> BoxFuture<'static, anyhow::Result<()>> {
Expand Down Expand Up @@ -615,6 +652,19 @@ impl HttpApi {

let mut app = app.with_state(state);

// Simple one-user basic auth
if let Some((user, pass)) = self.opts.basic_auth.take() {
info!("Enabling simple basic authentication in HTTP API");
app =
app.route_layer(axum::middleware::from_fn(move |headers, request, next| {
let user = user.clone();
let pass = pass.clone();
async move {
simple_basic_auth(Some(&user), Some(&pass), headers, request, next).await
}
}));
}

if let Some(upnp_router) = upnp_router {
app = app.nest("/upnp", upnp_router);
}
Expand Down
25 changes: 23 additions & 2 deletions crates/rqbit/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,15 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()>
},
};

let http_api_basic_auth = if let Ok(up) = std::env::var("RQBIT_HTTP_BASIC_AUTH_USERPASS") {
let (u, p) = up
.split_once(":")
.context("basic auth credentials should be in format username:password")?;
Some((u.to_owned(), p.to_owned()))
} else {
None
};

let stats_printer = |session: Arc<Session>| async move {
loop {
session.with_torrents(|torrents| {
Expand Down Expand Up @@ -615,7 +624,13 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()>
Some(log_config.rust_log_reload_tx),
Some(log_config.line_broadcast),
);
let http_api = HttpApi::new(api, Some(HttpApiOptions { read_only: false }));
let http_api = HttpApi::new(
api,
Some(HttpApiOptions {
read_only: false,
basic_auth: http_api_basic_auth,
}),
);
let http_api_listen_addr = opts.http_api_listen_addr;

info!("starting HTTP API at http://{http_api_listen_addr}");
Expand Down Expand Up @@ -735,7 +750,13 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()>
Some(log_config.rust_log_reload_tx),
Some(log_config.line_broadcast),
);
let http_api = HttpApi::new(api, Some(HttpApiOptions { read_only: true }));
let http_api = HttpApi::new(
api,
Some(HttpApiOptions {
read_only: true,
basic_auth: http_api_basic_auth,
}),
);
let http_api_listen_addr = opts.http_api_listen_addr;

info!("starting HTTP API at http://{http_api_listen_addr}");
Expand Down
5 changes: 4 additions & 1 deletion desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,10 @@ async fn api_from_config(
.with_context(|| format!("error listening on {}", listen_addr))?;
librqbit::http_api::HttpApi::new(
api.clone(),
Some(librqbit::http_api::HttpApiOptions { read_only }),
Some(librqbit::http_api::HttpApiOptions {
read_only,
basic_auth: None,
}),
)
.make_http_api_and_run(listener, upnp_router)
.await
Expand Down

0 comments on commit 3378ecb

Please sign in to comment.