mangadex-home-rs/src/routes.rs

452 lines
14 KiB
Rust
Raw Normal View History

2021-07-22 17:37:43 +00:00
use std::hint::unreachable_unchecked;
2021-04-23 01:46:34 +00:00
use std::sync::atomic::Ordering;
2021-03-18 01:45:16 +00:00
2022-01-02 20:34:00 +00:00
use actix_web::body::BoxBody;
2021-05-23 03:06:05 +00:00
use actix_web::error::ErrorNotFound;
2022-01-02 21:25:00 +00:00
use actix_web::http::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE, LAST_MODIFIED};
use actix_web::web::{Data, Path};
2021-04-18 23:14:36 +00:00
use actix_web::HttpResponseBuilder;
2022-01-02 21:25:00 +00:00
use actix_web::{get, HttpRequest, HttpResponse, Responder};
2021-03-18 01:45:16 +00:00
use base64::DecodeError;
use bytes::Bytes;
use chrono::{DateTime, Utc};
2021-07-10 22:53:28 +00:00
use futures::Stream;
2021-05-23 02:10:03 +00:00
use prometheus::{Encoder, TextEncoder};
2021-03-18 01:45:16 +00:00
use serde::Deserialize;
use sodiumoxide::crypto::box_::{open_precomputed, Nonce, PrecomputedKey, NONCEBYTES};
use thiserror::Error;
2021-07-13 03:23:51 +00:00
use tracing::{debug, error, info, trace};
2021-03-18 01:45:16 +00:00
2021-04-18 21:06:40 +00:00
use crate::cache::{Cache, CacheKey, ImageMetadata, UpstreamError};
2021-07-11 18:23:15 +00:00
use crate::client::{FetchResult, DEFAULT_HEADERS, HTTP_CLIENT};
2021-07-09 21:18:43 +00:00
use crate::config::{OFFLINE_MODE, VALIDATE_TOKENS};
2021-05-23 02:10:03 +00:00
use crate::metrics::{
CACHE_HIT_COUNTER, CACHE_MISS_COUNTER, REQUESTS_DATA_COUNTER, REQUESTS_DATA_SAVER_COUNTER,
REQUESTS_OTHER_COUNTER, REQUESTS_TOTAL_COUNTER,
};
2021-03-22 21:47:56 +00:00
use crate::state::RwLockServerState;
2021-03-18 01:45:16 +00:00
2021-07-09 21:18:43 +00:00
const BASE64_CONFIG: base64::Config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
2021-03-18 01:45:16 +00:00
2021-07-10 22:53:28 +00:00
pub enum ServerResponse {
2021-03-18 01:45:16 +00:00
TokenValidationError(TokenValidationError),
HttpResponse(HttpResponse),
}
impl Responder for ServerResponse {
2022-01-02 20:34:00 +00:00
type Body = BoxBody;
2021-03-23 16:59:49 +00:00
#[inline]
2021-03-18 01:45:16 +00:00
fn respond_to(self, req: &HttpRequest) -> HttpResponse {
match self {
2021-03-18 02:41:48 +00:00
Self::TokenValidationError(e) => e.respond_to(req),
2021-05-23 02:10:03 +00:00
Self::HttpResponse(resp) => {
REQUESTS_TOTAL_COUNTER.inc();
resp.respond_to(req)
}
2021-03-18 01:45:16 +00:00
}
}
}
2021-07-11 17:19:37 +00:00
#[allow(clippy::unused_async)]
2021-05-27 21:05:50 +00:00
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Ok().body(include_str!("index.html"))
}
2021-03-18 01:45:16 +00:00
#[get("/{token}/data/{chapter_hash}/{file_name}")]
async fn token_data(
state: Data<RwLockServerState>,
2021-05-20 03:27:56 +00:00
cache: Data<dyn Cache>,
2021-03-18 01:45:16 +00:00
path: Path<(String, String, String)>,
) -> impl Responder {
2021-05-23 02:10:03 +00:00
REQUESTS_DATA_COUNTER.inc();
2021-03-18 01:45:16 +00:00
let (token, chapter_hash, file_name) = path.into_inner();
2021-03-26 02:58:07 +00:00
if VALIDATE_TOKENS.load(Ordering::Acquire) {
2021-03-18 01:45:16 +00:00
if let Err(e) = validate_token(&state.0.read().precomputed_key, token, &chapter_hash) {
return ServerResponse::TokenValidationError(e);
}
}
2021-03-26 04:07:32 +00:00
fetch_image(state, cache, chapter_hash, file_name, false).await
2021-03-18 01:45:16 +00:00
}
#[get("/{token}/data-saver/{chapter_hash}/{file_name}")]
async fn token_data_saver(
state: Data<RwLockServerState>,
2021-05-20 03:27:56 +00:00
cache: Data<dyn Cache>,
2021-03-18 01:45:16 +00:00
path: Path<(String, String, String)>,
) -> impl Responder {
2021-05-23 02:10:03 +00:00
REQUESTS_DATA_SAVER_COUNTER.inc();
2021-03-18 01:45:16 +00:00
let (token, chapter_hash, file_name) = path.into_inner();
2021-03-26 02:58:07 +00:00
if VALIDATE_TOKENS.load(Ordering::Acquire) {
2021-03-18 01:45:16 +00:00
if let Err(e) = validate_token(&state.0.read().precomputed_key, token, &chapter_hash) {
return ServerResponse::TokenValidationError(e);
}
}
2021-04-19 03:06:18 +00:00
2021-03-26 04:07:32 +00:00
fetch_image(state, cache, chapter_hash, file_name, true).await
2021-03-18 01:45:16 +00:00
}
2021-04-20 02:14:57 +00:00
#[allow(clippy::future_not_send)]
2021-03-22 21:47:56 +00:00
pub async fn default(state: Data<RwLockServerState>, req: HttpRequest) -> impl Responder {
2021-05-23 02:10:03 +00:00
REQUESTS_OTHER_COUNTER.inc();
2021-03-22 21:47:56 +00:00
let path = &format!(
"{}{}",
state.0.read().image_server,
req.path().chars().skip(1).collect::<String>()
);
2021-05-23 03:06:05 +00:00
if OFFLINE_MODE.load(Ordering::Acquire) {
info!("Got unknown path in offline mode, returning 404: {}", path);
return ServerResponse::HttpResponse(
ErrorNotFound("Path is not valid in offline mode").into(),
);
}
2021-05-23 03:10:34 +00:00
info!("Got unknown path, just proxying: {}", path);
2021-07-11 18:23:15 +00:00
let mut resp = match HTTP_CLIENT.inner().get(path).send().await {
2021-04-19 03:06:18 +00:00
Ok(resp) => resp,
Err(e) => {
error!("{}", e);
return ServerResponse::HttpResponse(HttpResponse::BadGateway().finish());
}
};
2021-07-11 18:23:15 +00:00
let content_type = resp.headers_mut().remove(CONTENT_TYPE);
2021-03-22 21:47:56 +00:00
let mut resp_builder = HttpResponseBuilder::new(resp.status());
2021-07-11 18:23:15 +00:00
let mut headers = DEFAULT_HEADERS.clone();
2021-03-22 21:47:56 +00:00
if let Some(content_type) = content_type {
2021-07-11 18:23:15 +00:00
headers.insert(CONTENT_TYPE, content_type);
2021-03-22 21:47:56 +00:00
}
2021-07-11 18:23:15 +00:00
// push_headers(&mut resp_builder);
2021-03-22 21:47:56 +00:00
2021-07-12 03:25:17 +00:00
let mut resp = resp_builder.body(resp.bytes().await.unwrap_or_default());
*resp.headers_mut() = headers;
ServerResponse::HttpResponse(resp)
2021-03-22 21:47:56 +00:00
}
2021-07-17 17:32:43 +00:00
#[allow(clippy::unused_async)]
2021-07-15 19:54:26 +00:00
#[get("/prometheus")]
2021-05-23 02:10:03 +00:00
pub async fn metrics() -> impl Responder {
let metric_families = prometheus::gather();
let mut buffer = Vec::new();
TextEncoder::new()
.encode(&metric_families, &mut buffer)
2021-07-22 17:46:40 +00:00
.expect("Should never have an io error writing to a vec");
String::from_utf8(buffer).expect("Text encoder should render valid utf-8")
2021-05-23 02:10:03 +00:00
}
2021-07-22 17:37:43 +00:00
#[derive(Error, Debug, PartialEq, Eq)]
2021-07-10 22:53:28 +00:00
pub enum TokenValidationError {
2021-03-18 01:45:16 +00:00
#[error("Failed to decode base64 token.")]
DecodeError(#[from] DecodeError),
#[error("Nonce was too short.")]
IncompleteNonce,
#[error("Decryption failed")]
DecryptionFailure,
#[error("The token format was invalid.")]
InvalidToken,
#[error("The token has expired.")]
TokenExpired,
#[error("Invalid chapter hash.")]
InvalidChapterHash,
}
impl Responder for TokenValidationError {
2022-01-02 20:34:00 +00:00
type Body = BoxBody;
2021-03-23 16:59:49 +00:00
#[inline]
2021-03-18 01:45:16 +00:00
fn respond_to(self, _: &HttpRequest) -> HttpResponse {
2021-07-11 18:23:15 +00:00
let mut resp = HttpResponse::Forbidden().finish();
*resp.headers_mut() = DEFAULT_HEADERS.clone();
resp
2021-03-18 01:45:16 +00:00
}
}
fn validate_token(
precomputed_key: &PrecomputedKey,
token: String,
chapter_hash: &str,
) -> Result<(), TokenValidationError> {
2021-03-18 02:41:48 +00:00
#[derive(Deserialize)]
struct Token<'a> {
expires: DateTime<Utc>,
hash: &'a str,
}
2021-03-18 01:45:16 +00:00
let data = base64::decode_config(token, BASE64_CONFIG)?;
if data.len() < NONCEBYTES {
return Err(TokenValidationError::IncompleteNonce);
}
2021-05-24 19:21:45 +00:00
let (nonce, encrypted) = data.split_at(NONCEBYTES);
2021-07-22 17:37:43 +00:00
let nonce = match Nonce::from_slice(nonce) {
Some(nonce) => nonce,
// We split at NONCEBYTES, so this should never happen.
None => unsafe { unreachable_unchecked() },
};
2021-06-24 14:41:42 +00:00
let decrypted = open_precomputed(encrypted, &nonce, precomputed_key)
2021-03-18 01:45:16 +00:00
.map_err(|_| TokenValidationError::DecryptionFailure)?;
let parsed_token: Token =
serde_json::from_slice(&decrypted).map_err(|_| TokenValidationError::InvalidToken)?;
if parsed_token.expires < Utc::now() {
return Err(TokenValidationError::TokenExpired);
}
if parsed_token.hash != chapter_hash {
return Err(TokenValidationError::InvalidChapterHash);
}
2021-04-19 03:06:18 +00:00
debug!("Token validated!");
2021-03-18 01:45:16 +00:00
Ok(())
}
2021-04-20 02:14:57 +00:00
#[allow(clippy::future_not_send)]
2021-03-18 01:45:16 +00:00
async fn fetch_image(
state: Data<RwLockServerState>,
2021-05-20 03:27:56 +00:00
cache: Data<dyn Cache>,
2021-03-18 01:45:16 +00:00
chapter_hash: String,
file_name: String,
is_data_saver: bool,
) -> ServerResponse {
2021-04-23 01:46:34 +00:00
let key = CacheKey(chapter_hash, file_name, is_data_saver);
2021-03-18 01:45:16 +00:00
2021-04-23 01:46:34 +00:00
match cache.get(&key).await {
2021-04-19 04:16:13 +00:00
Some(Ok((image, metadata))) => {
2021-05-23 02:10:03 +00:00
CACHE_HIT_COUNTER.inc();
2021-04-22 16:44:02 +00:00
return construct_response(image, &metadata);
2021-04-19 04:16:13 +00:00
}
Some(Err(_)) => {
return ServerResponse::HttpResponse(HttpResponse::BadGateway().finish());
}
2021-07-10 22:53:28 +00:00
None => (),
2021-03-18 01:45:16 +00:00
}
2021-05-23 02:10:03 +00:00
CACHE_MISS_COUNTER.inc();
2021-05-23 03:06:05 +00:00
// If in offline mode, return early since there's nothing else we can do
if OFFLINE_MODE.load(Ordering::Acquire) {
return ServerResponse::HttpResponse(
ErrorNotFound("Offline mode enabled and image not in cache").into(),
);
}
2021-07-10 22:53:28 +00:00
let url = if is_data_saver {
format!(
"{}/data-saver/{}/{}",
state.0.read().image_server,
&key.0,
&key.1,
)
2021-03-18 01:45:16 +00:00
} else {
2021-07-10 22:53:28 +00:00
format!("{}/data/{}/{}", state.0.read().image_server, &key.0, &key.1)
};
2021-03-22 21:47:56 +00:00
2021-07-10 22:53:28 +00:00
match HTTP_CLIENT.fetch_and_cache(url, key, cache).await {
FetchResult::ServiceUnavailable => {
ServerResponse::HttpResponse(HttpResponse::ServiceUnavailable().finish())
2021-03-18 02:41:48 +00:00
}
2021-07-10 22:53:28 +00:00
FetchResult::InternalServerError => {
ServerResponse::HttpResponse(HttpResponse::InternalServerError().finish())
}
FetchResult::Data(status, headers, data) => {
let mut resp = HttpResponseBuilder::new(status);
let mut resp = resp.body(data);
*resp.headers_mut() = headers;
ServerResponse::HttpResponse(resp)
2021-03-18 01:45:16 +00:00
}
2021-07-10 22:53:28 +00:00
FetchResult::Processing => panic!("Race condition found with fetch result"),
2021-03-18 01:45:16 +00:00
}
}
2021-03-18 02:41:48 +00:00
2021-07-11 18:23:15 +00:00
#[inline]
2021-07-10 22:53:28 +00:00
pub fn construct_response(
2021-04-18 21:06:40 +00:00
data: impl Stream<Item = Result<Bytes, UpstreamError>> + Unpin + 'static,
metadata: &ImageMetadata,
) -> ServerResponse {
2021-05-27 23:24:54 +00:00
trace!("Constructing response");
2021-04-19 03:06:18 +00:00
2021-03-18 02:41:48 +00:00
let mut resp = HttpResponse::Ok();
2021-07-11 18:23:15 +00:00
let mut headers = DEFAULT_HEADERS.clone();
2021-04-18 21:06:40 +00:00
if let Some(content_type) = metadata.content_type {
2021-07-11 18:23:15 +00:00
headers.insert(
CONTENT_TYPE,
HeaderValue::from_str(content_type.as_ref()).unwrap(),
);
2021-03-18 02:41:48 +00:00
}
2021-04-18 21:06:40 +00:00
if let Some(content_length) = metadata.content_length {
2021-07-11 18:23:15 +00:00
headers.insert(CONTENT_LENGTH, HeaderValue::from(content_length));
2021-03-18 02:41:48 +00:00
}
2021-04-18 21:06:40 +00:00
if let Some(last_modified) = metadata.last_modified {
2021-07-11 18:23:15 +00:00
headers.insert(
LAST_MODIFIED,
HeaderValue::from_str(&last_modified.to_rfc2822()).unwrap(),
);
2021-03-18 02:41:48 +00:00
}
2021-07-11 18:23:15 +00:00
let mut ret = resp.streaming(data);
*ret.headers_mut() = headers;
ServerResponse::HttpResponse(ret)
2021-03-18 02:41:48 +00:00
}
2021-07-22 17:37:43 +00:00
#[cfg(test)]
mod token_validation {
2022-03-26 23:28:58 +00:00
use super::{BASE64_CONFIG, DecodeError, PrecomputedKey, TokenValidationError, Utc, validate_token};
2021-07-22 17:37:43 +00:00
use sodiumoxide::crypto::box_::precompute;
use sodiumoxide::crypto::box_::seal_precomputed;
use sodiumoxide::crypto::box_::{gen_keypair, gen_nonce, PRECOMPUTEDKEYBYTES};
#[test]
fn invalid_base64() {
let res = validate_token(
&PrecomputedKey::from_slice(&b"1".repeat(PRECOMPUTEDKEYBYTES))
.expect("valid test token"),
"a".to_string(),
"b",
);
assert_eq!(
res,
Err(TokenValidationError::DecodeError(
DecodeError::InvalidLength
))
);
}
#[test]
fn not_long_enough_for_nonce() {
let res = validate_token(
&PrecomputedKey::from_slice(&b"1".repeat(PRECOMPUTEDKEYBYTES))
.expect("valid test token"),
"aGVsbG8gaW50ZXJuZXR-Cg==".to_string(),
"b",
);
assert_eq!(res, Err(TokenValidationError::IncompleteNonce));
}
#[test]
fn invalid_precomputed_key() {
let precomputed_1 = {
let (pk, sk) = gen_keypair();
precompute(&pk, &sk)
};
let precomputed_2 = {
let (pk, sk) = gen_keypair();
precompute(&pk, &sk)
};
let nonce = gen_nonce();
// Seal with precomputed_2, open with precomputed_1
let data = seal_precomputed(b"hello world", &nonce, &precomputed_2);
2022-03-26 23:28:58 +00:00
let data: Vec<u8> = nonce.as_ref().iter().copied().chain(data).collect();
2021-07-22 17:37:43 +00:00
let data = base64::encode_config(data, BASE64_CONFIG);
let res = validate_token(&precomputed_1, data, "b");
assert_eq!(res, Err(TokenValidationError::DecryptionFailure));
}
#[test]
fn invalid_token_data() {
let precomputed = {
let (pk, sk) = gen_keypair();
precompute(&pk, &sk)
};
let nonce = gen_nonce();
let data = seal_precomputed(b"hello world", &nonce, &precomputed);
2022-03-26 23:28:58 +00:00
let data: Vec<u8> = nonce.as_ref().iter().copied().chain(data).collect();
2021-07-22 17:37:43 +00:00
let data = base64::encode_config(data, BASE64_CONFIG);
let res = validate_token(&precomputed, data, "b");
assert_eq!(res, Err(TokenValidationError::InvalidToken));
}
#[test]
fn token_must_have_valid_expiration() {
let precomputed = {
let (pk, sk) = gen_keypair();
precompute(&pk, &sk)
};
let nonce = gen_nonce();
let time = Utc::now() - chrono::Duration::weeks(1);
let data = seal_precomputed(
serde_json::json!({
"expires": time.to_rfc3339(),
"hash": "b",
})
.to_string()
.as_bytes(),
&nonce,
&precomputed,
);
2022-03-26 23:28:58 +00:00
let data: Vec<u8> = nonce.as_ref().iter().copied().chain(data).collect();
2021-07-22 17:37:43 +00:00
let data = base64::encode_config(data, BASE64_CONFIG);
let res = validate_token(&precomputed, data, "b");
assert_eq!(res, Err(TokenValidationError::TokenExpired));
}
#[test]
fn token_must_have_valid_chapter_hash() {
let precomputed = {
let (pk, sk) = gen_keypair();
precompute(&pk, &sk)
};
let nonce = gen_nonce();
let time = Utc::now() + chrono::Duration::weeks(1);
let data = seal_precomputed(
serde_json::json!({
"expires": time.to_rfc3339(),
"hash": "b",
})
.to_string()
.as_bytes(),
&nonce,
&precomputed,
);
2022-03-26 23:28:58 +00:00
let data: Vec<u8> = nonce.as_ref().iter().copied().chain(data).collect();
2021-07-22 17:37:43 +00:00
let data = base64::encode_config(data, BASE64_CONFIG);
let res = validate_token(&precomputed, data, "");
assert_eq!(res, Err(TokenValidationError::InvalidChapterHash));
}
#[test]
fn valid_token_returns_ok() {
let precomputed = {
let (pk, sk) = gen_keypair();
precompute(&pk, &sk)
};
let nonce = gen_nonce();
let time = Utc::now() + chrono::Duration::weeks(1);
let data = seal_precomputed(
serde_json::json!({
"expires": time.to_rfc3339(),
"hash": "b",
})
.to_string()
.as_bytes(),
&nonce,
&precomputed,
);
2022-03-26 23:28:58 +00:00
let data: Vec<u8> = nonce.as_ref().iter().copied().chain(data).collect();
2021-07-22 17:37:43 +00:00
let data = base64::encode_config(data, BASE64_CONFIG);
let res = validate_token(&precomputed, data, "b");
assert!(res.is_ok());
}
}