mangadex-home-rs/src/routes.rs

375 lines
12 KiB
Rust
Raw Normal View History

2021-04-22 18:46:34 -07:00
use std::sync::atomic::Ordering;
use std::time::Duration;
2021-03-17 18:45:16 -07:00
2021-05-22 20:06:05 -07:00
use actix_web::error::ErrorNotFound;
2021-03-17 19:41:48 -07:00
use actix_web::http::header::{
ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, CACHE_CONTROL, CONTENT_LENGTH,
CONTENT_TYPE, LAST_MODIFIED, X_CONTENT_TYPE_OPTIONS,
};
use actix_web::web::Path;
2021-04-18 16:14:36 -07:00
use actix_web::HttpResponseBuilder;
2021-03-17 19:41:48 -07:00
use actix_web::{get, web::Data, HttpRequest, HttpResponse, Responder};
2021-03-17 18:45:16 -07:00
use base64::DecodeError;
use bytes::Bytes;
use chrono::{DateTime, Utc};
2021-05-27 13:22:15 -07:00
use futures::{Stream, TryStreamExt};
2021-05-27 16:24:54 -07:00
use log::{debug, error, info, trace, warn};
2021-04-23 19:23:24 -07:00
use once_cell::sync::Lazy;
2021-05-22 19:10:03 -07:00
use prometheus::{Encoder, TextEncoder};
2021-05-11 18:01:01 -07:00
use reqwest::{Client, StatusCode};
2021-03-17 18:45:16 -07:00
use serde::Deserialize;
use sodiumoxide::crypto::box_::{open_precomputed, Nonce, PrecomputedKey, NONCEBYTES};
use thiserror::Error;
2021-04-18 14:06:40 -07:00
use crate::cache::{Cache, CacheKey, ImageMetadata, UpstreamError};
2021-03-22 14:47:56 -07:00
use crate::client_api_version;
2021-05-22 20:06:05 -07:00
use crate::config::{OFFLINE_MODE, SEND_SERVER_VERSION, VALIDATE_TOKENS};
2021-05-22 19:10:03 -07: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 14:47:56 -07:00
use crate::state::RwLockServerState;
2021-03-17 18:45:16 -07:00
2021-03-22 14:47:56 -07:00
pub const BASE64_CONFIG: base64::Config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
2021-03-17 18:45:16 -07:00
static HTTP_CLIENT: Lazy<Client> = Lazy::new(|| {
Client::builder()
.pool_idle_timeout(Duration::from_secs(180))
.https_only(true)
.http2_prior_knowledge()
.build()
.expect("Client initialization to work")
});
2021-04-23 19:23:24 -07:00
2021-03-17 18:45:16 -07:00
const SERVER_ID_STRING: &str = concat!(
env!("CARGO_CRATE_NAME"),
" ",
env!("CARGO_PKG_VERSION"),
" (",
client_api_version!(),
2021-03-22 20:04:54 -07:00
") - Conforming to spec revision b82043289",
2021-03-17 18:45:16 -07:00
);
enum ServerResponse {
TokenValidationError(TokenValidationError),
HttpResponse(HttpResponse),
}
impl Responder for ServerResponse {
2021-03-23 09:59:49 -07:00
#[inline]
2021-03-17 18:45:16 -07:00
fn respond_to(self, req: &HttpRequest) -> HttpResponse {
match self {
2021-03-17 19:41:48 -07:00
Self::TokenValidationError(e) => e.respond_to(req),
2021-05-22 19:10:03 -07:00
Self::HttpResponse(resp) => {
REQUESTS_TOTAL_COUNTER.inc();
resp.respond_to(req)
}
2021-03-17 18:45:16 -07:00
}
}
}
2021-05-27 14:05:50 -07:00
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Ok().body(include_str!("index.html"))
}
2021-04-19 19:14:57 -07:00
#[allow(clippy::future_not_send)]
2021-03-17 18:45:16 -07:00
#[get("/{token}/data/{chapter_hash}/{file_name}")]
async fn token_data(
state: Data<RwLockServerState>,
2021-05-19 20:27:56 -07:00
cache: Data<dyn Cache>,
2021-03-17 18:45:16 -07:00
path: Path<(String, String, String)>,
) -> impl Responder {
2021-05-22 19:10:03 -07:00
REQUESTS_DATA_COUNTER.inc();
2021-03-17 18:45:16 -07:00
let (token, chapter_hash, file_name) = path.into_inner();
2021-03-25 19:58:07 -07:00
if VALIDATE_TOKENS.load(Ordering::Acquire) {
2021-03-17 18:45:16 -07:00
if let Err(e) = validate_token(&state.0.read().precomputed_key, token, &chapter_hash) {
return ServerResponse::TokenValidationError(e);
}
}
2021-03-25 21:07:32 -07:00
fetch_image(state, cache, chapter_hash, file_name, false).await
2021-03-17 18:45:16 -07:00
}
2021-04-19 19:14:57 -07:00
#[allow(clippy::future_not_send)]
2021-03-17 18:45:16 -07:00
#[get("/{token}/data-saver/{chapter_hash}/{file_name}")]
async fn token_data_saver(
state: Data<RwLockServerState>,
2021-05-19 20:27:56 -07:00
cache: Data<dyn Cache>,
2021-03-17 18:45:16 -07:00
path: Path<(String, String, String)>,
) -> impl Responder {
2021-05-22 19:10:03 -07:00
REQUESTS_DATA_SAVER_COUNTER.inc();
2021-03-17 18:45:16 -07:00
let (token, chapter_hash, file_name) = path.into_inner();
2021-03-25 19:58:07 -07:00
if VALIDATE_TOKENS.load(Ordering::Acquire) {
2021-03-17 18:45:16 -07:00
if let Err(e) = validate_token(&state.0.read().precomputed_key, token, &chapter_hash) {
return ServerResponse::TokenValidationError(e);
}
}
2021-04-18 20:06:18 -07:00
2021-03-25 21:07:32 -07:00
fetch_image(state, cache, chapter_hash, file_name, true).await
2021-03-17 18:45:16 -07:00
}
2021-04-19 19:14:57 -07:00
#[allow(clippy::future_not_send)]
2021-03-22 14:47:56 -07:00
pub async fn default(state: Data<RwLockServerState>, req: HttpRequest) -> impl Responder {
2021-05-22 19:10:03 -07:00
REQUESTS_OTHER_COUNTER.inc();
2021-03-22 14:47:56 -07:00
let path = &format!(
"{}{}",
state.0.read().image_server,
req.path().chars().skip(1).collect::<String>()
);
2021-05-22 20:06:05 -07: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-22 20:10:34 -07:00
info!("Got unknown path, just proxying: {}", path);
2021-04-23 19:23:24 -07:00
let resp = match HTTP_CLIENT.get(path).send().await {
2021-04-18 20:06:18 -07:00
Ok(resp) => resp,
Err(e) => {
error!("{}", e);
return ServerResponse::HttpResponse(HttpResponse::BadGateway().finish());
}
};
2021-03-22 14:47:56 -07:00
let content_type = resp.headers().get(CONTENT_TYPE);
let mut resp_builder = HttpResponseBuilder::new(resp.status());
if let Some(content_type) = content_type {
resp_builder.insert_header((CONTENT_TYPE, content_type));
}
push_headers(&mut resp_builder);
ServerResponse::HttpResponse(resp_builder.body(resp.bytes().await.unwrap_or_default()))
}
2021-05-22 19:10:03 -07:00
#[allow(clippy::future_not_send)]
#[get("/metrics")]
pub async fn metrics() -> impl Responder {
let metric_families = prometheus::gather();
let mut buffer = Vec::new();
TextEncoder::new()
.encode(&metric_families, &mut buffer)
.unwrap();
String::from_utf8(buffer).unwrap()
}
2021-03-17 18:45:16 -07:00
#[derive(Error, Debug)]
enum TokenValidationError {
#[error("Failed to decode base64 token.")]
DecodeError(#[from] DecodeError),
#[error("Nonce was too short.")]
IncompleteNonce,
#[error("Invalid nonce.")]
InvalidNonce,
#[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 {
2021-03-23 09:59:49 -07:00
#[inline]
2021-03-17 18:45:16 -07:00
fn respond_to(self, _: &HttpRequest) -> HttpResponse {
push_headers(&mut HttpResponse::Forbidden()).finish()
}
}
fn validate_token(
precomputed_key: &PrecomputedKey,
token: String,
chapter_hash: &str,
) -> Result<(), TokenValidationError> {
2021-03-17 19:41:48 -07:00
#[derive(Deserialize)]
struct Token<'a> {
expires: DateTime<Utc>,
hash: &'a str,
}
2021-03-17 18:45:16 -07:00
let data = base64::decode_config(token, BASE64_CONFIG)?;
if data.len() < NONCEBYTES {
return Err(TokenValidationError::IncompleteNonce);
}
2021-05-24 12:21:45 -07:00
let (nonce, encrypted) = data.split_at(NONCEBYTES);
let nonce = Nonce::from_slice(&nonce).ok_or(TokenValidationError::InvalidNonce)?;
let decrypted = open_precomputed(&encrypted, &nonce, precomputed_key)
2021-03-17 18:45:16 -07: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-18 20:06:18 -07:00
debug!("Token validated!");
2021-03-17 18:45:16 -07:00
Ok(())
}
2021-03-23 09:59:49 -07:00
#[inline]
2021-03-17 18:45:16 -07:00
fn push_headers(builder: &mut HttpResponseBuilder) -> &mut HttpResponseBuilder {
builder
2021-03-17 19:41:48 -07:00
.insert_header((X_CONTENT_TYPE_OPTIONS, "nosniff"))
.insert_header((ACCESS_CONTROL_ALLOW_ORIGIN, "https://mangadex.org"))
.insert_header((ACCESS_CONTROL_EXPOSE_HEADERS, "*"))
.insert_header((CACHE_CONTROL, "public, max-age=1209600"))
2021-03-25 18:06:54 -07:00
.insert_header(("Timing-Allow-Origin", "https://mangadex.org"));
if SEND_SERVER_VERSION.load(Ordering::Acquire) {
builder.insert_header(("Server", SERVER_ID_STRING));
}
builder
2021-03-17 18:45:16 -07:00
}
2021-04-19 19:14:57 -07:00
#[allow(clippy::future_not_send)]
2021-03-17 18:45:16 -07:00
async fn fetch_image(
state: Data<RwLockServerState>,
2021-05-19 20:27:56 -07:00
cache: Data<dyn Cache>,
2021-03-17 18:45:16 -07:00
chapter_hash: String,
file_name: String,
is_data_saver: bool,
) -> ServerResponse {
2021-04-22 18:46:34 -07:00
let key = CacheKey(chapter_hash, file_name, is_data_saver);
2021-03-17 18:45:16 -07:00
2021-04-22 18:46:34 -07:00
match cache.get(&key).await {
2021-04-18 21:16:13 -07:00
Some(Ok((image, metadata))) => {
2021-05-22 19:10:03 -07:00
CACHE_HIT_COUNTER.inc();
2021-04-22 09:44:02 -07:00
return construct_response(image, &metadata);
2021-04-18 21:16:13 -07:00
}
Some(Err(_)) => {
return ServerResponse::HttpResponse(HttpResponse::BadGateway().finish());
}
2021-04-18 14:06:40 -07:00
_ => (),
2021-03-17 18:45:16 -07:00
}
2021-05-22 19:10:03 -07:00
CACHE_MISS_COUNTER.inc();
2021-05-22 20:06:05 -07: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-03-25 19:58:07 -07:00
// It's important to not get a write lock before this request, else we're
// holding the read lock until the await resolves.
2021-03-25 18:06:54 -07:00
2021-03-17 18:45:16 -07:00
let resp = if is_data_saver {
2021-04-23 19:23:24 -07:00
HTTP_CLIENT
.get(format!(
"{}/data-saver/{}/{}",
state.0.read().image_server,
&key.0,
&key.1
))
.send()
2021-03-17 18:45:16 -07:00
} else {
2021-04-23 19:23:24 -07:00
HTTP_CLIENT
.get(format!(
"{}/data/{}/{}",
state.0.read().image_server,
&key.0,
&key.1
))
.send()
2021-03-17 18:45:16 -07:00
}
.await;
match resp {
2021-04-14 19:11:00 -07:00
Ok(mut resp) => {
2021-03-22 14:47:56 -07:00
let content_type = resp.headers().get(CONTENT_TYPE);
let is_image = content_type
.map(|v| String::from_utf8_lossy(v.as_ref()).contains("image/"))
.unwrap_or_default();
2021-04-18 20:06:18 -07:00
2021-05-11 18:01:01 -07:00
if resp.status() != StatusCode::OK || !is_image {
2021-03-22 14:47:56 -07:00
warn!(
"Got non-OK or non-image response code from upstream, proxying and not caching result.",
);
let mut resp_builder = HttpResponseBuilder::new(resp.status());
if let Some(content_type) = content_type {
resp_builder.insert_header((CONTENT_TYPE, content_type));
}
2021-03-25 18:06:54 -07:00
2021-03-22 14:47:56 -07:00
push_headers(&mut resp_builder);
return ServerResponse::HttpResponse(
resp_builder.body(resp.bytes().await.unwrap_or_default()),
);
}
2021-04-14 19:11:00 -07:00
let (content_type, length, last_mod) = {
let headers = resp.headers_mut();
(
headers.remove(CONTENT_TYPE),
headers.remove(CONTENT_LENGTH),
headers.remove(LAST_MODIFIED),
)
};
2021-04-18 14:06:40 -07:00
let body = resp.bytes_stream().map_err(|e| e.into());
2021-04-18 20:06:18 -07:00
debug!("Inserting into cache");
2021-04-18 14:06:40 -07:00
let metadata = ImageMetadata::new(content_type, length, last_mod).unwrap();
2021-04-22 09:44:02 -07:00
let stream = {
2021-04-22 18:55:26 -07:00
match cache.put(key, Box::new(body), metadata).await {
2021-04-22 09:44:02 -07:00
Ok(stream) => stream,
2021-04-18 14:11:30 -07:00
Err(e) => {
warn!("Failed to insert into cache: {}", e);
return ServerResponse::HttpResponse(
HttpResponse::InternalServerError().finish(),
);
}
2021-03-17 19:41:48 -07:00
}
2021-04-18 14:06:40 -07:00
};
2021-04-18 20:06:18 -07:00
debug!("Done putting into cache");
2021-05-11 18:01:01 -07:00
construct_response(stream, &metadata)
2021-03-17 19:41:48 -07:00
}
2021-03-17 18:45:16 -07:00
Err(e) => {
error!("Failed to fetch image from server: {}", e);
2021-03-17 19:41:48 -07:00
ServerResponse::HttpResponse(
push_headers(&mut HttpResponse::ServiceUnavailable()).finish(),
)
2021-03-17 18:45:16 -07:00
}
}
}
2021-03-17 19:41:48 -07:00
2021-04-18 14:06:40 -07:00
fn construct_response(
data: impl Stream<Item = Result<Bytes, UpstreamError>> + Unpin + 'static,
metadata: &ImageMetadata,
) -> ServerResponse {
2021-05-27 16:24:54 -07:00
trace!("Constructing response");
2021-04-18 20:06:18 -07:00
2021-03-17 19:41:48 -07:00
let mut resp = HttpResponse::Ok();
2021-04-18 14:06:40 -07:00
if let Some(content_type) = metadata.content_type {
2021-04-14 19:52:54 -07:00
resp.append_header((CONTENT_TYPE, content_type.as_ref()));
2021-03-17 19:41:48 -07:00
}
2021-04-18 14:06:40 -07:00
if let Some(content_length) = metadata.content_length {
resp.append_header((CONTENT_LENGTH, content_length));
2021-03-17 19:41:48 -07:00
}
2021-04-18 14:06:40 -07:00
if let Some(last_modified) = metadata.last_modified {
2021-04-14 19:11:00 -07:00
resp.append_header((LAST_MODIFIED, last_modified.to_rfc2822()));
2021-03-17 19:41:48 -07:00
}
2021-04-18 14:06:40 -07:00
ServerResponse::HttpResponse(push_headers(&mut resp).streaming(data))
2021-03-17 19:41:48 -07:00
}