Basic auth in HTTP API
This commit is contained in:
parent
09c9659b88
commit
3e8a39314b
4 changed files with 77 additions and 5 deletions
|
|
@ -55,6 +55,10 @@ impl ApiError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn unathorized() -> Self {
|
||||||
|
Self::new_from_text(StatusCode::UNAUTHORIZED, "unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn status(&self) -> StatusCode {
|
pub fn status(&self) -> StatusCode {
|
||||||
self.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
|
self.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use axum::body::Bytes;
|
use axum::body::Bytes;
|
||||||
use axum::extract::{ConnectInfo, Path, Query, Request, State};
|
use axum::extract::{ConnectInfo, Path, Query, Request, State};
|
||||||
|
use axum::middleware::Next;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
|
use base64::Engine;
|
||||||
use bencode::AsDisplay;
|
use bencode::AsDisplay;
|
||||||
use buffers::ByteBuf;
|
use buffers::ByteBuf;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
|
|
@ -19,7 +21,7 @@ use std::time::Duration;
|
||||||
use tokio::io::AsyncSeekExt;
|
use tokio::io::AsyncSeekExt;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tower_http::trace::{DefaultOnFailure, DefaultOnResponse, OnFailure};
|
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};
|
use axum::{Json, Router};
|
||||||
|
|
||||||
|
|
@ -43,6 +45,35 @@ pub struct HttpApi {
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct HttpApiOptions {
|
pub struct HttpApiOptions {
|
||||||
pub read_only: bool,
|
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 Err(ApiError::unathorized()),
|
||||||
|
};
|
||||||
|
// 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 {
|
impl HttpApi {
|
||||||
|
|
@ -57,7 +88,7 @@ impl HttpApi {
|
||||||
/// If read_only is passed, no state-modifying methods will be exposed.
|
/// If read_only is passed, no state-modifying methods will be exposed.
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
pub fn make_http_api_and_run(
|
pub fn make_http_api_and_run(
|
||||||
self,
|
mut self,
|
||||||
listener: TcpListener,
|
listener: TcpListener,
|
||||||
upnp_router: Option<Router>,
|
upnp_router: Option<Router>,
|
||||||
) -> BoxFuture<'static, anyhow::Result<()>> {
|
) -> BoxFuture<'static, anyhow::Result<()>> {
|
||||||
|
|
@ -615,6 +646,19 @@ impl HttpApi {
|
||||||
|
|
||||||
let mut app = app.with_state(state);
|
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 {
|
if let Some(upnp_router) = upnp_router {
|
||||||
app = app.nest("/upnp", upnp_router);
|
app = app.nest("/upnp", upnp_router);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
let stats_printer = |session: Arc<Session>| async move {
|
||||||
loop {
|
loop {
|
||||||
session.with_torrents(|torrents| {
|
session.with_torrents(|torrents| {
|
||||||
|
|
@ -615,7 +624,13 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()>
|
||||||
Some(log_config.rust_log_reload_tx),
|
Some(log_config.rust_log_reload_tx),
|
||||||
Some(log_config.line_broadcast),
|
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;
|
let http_api_listen_addr = opts.http_api_listen_addr;
|
||||||
|
|
||||||
info!("starting HTTP API at http://{http_api_listen_addr}");
|
info!("starting HTTP API at http://{http_api_listen_addr}");
|
||||||
|
|
@ -735,7 +750,13 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()>
|
||||||
Some(log_config.rust_log_reload_tx),
|
Some(log_config.rust_log_reload_tx),
|
||||||
Some(log_config.line_broadcast),
|
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;
|
let http_api_listen_addr = opts.http_api_listen_addr;
|
||||||
|
|
||||||
info!("starting HTTP API at http://{http_api_listen_addr}");
|
info!("starting HTTP API at http://{http_api_listen_addr}");
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,10 @@ async fn api_from_config(
|
||||||
.with_context(|| format!("error listening on {}", listen_addr))?;
|
.with_context(|| format!("error listening on {}", listen_addr))?;
|
||||||
librqbit::http_api::HttpApi::new(
|
librqbit::http_api::HttpApi::new(
|
||||||
api.clone(),
|
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)
|
.make_http_api_and_run(listener, upnp_router)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue