2021-03-25 19:58:07 -07:00
|
|
|
use std::sync::atomic::Ordering;
|
2021-03-17 18:45:16 -07:00
|
|
|
|
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-04-18 14:06:40 -07:00
|
|
|
use futures::{Stream, TryStreamExt};
|
2021-04-18 20:06:18 -07:00
|
|
|
use log::{debug, error, info, warn};
|
2021-03-25 21:07:32 -07:00
|
|
|
use parking_lot::Mutex;
|
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-03-25 19:58:07 -07:00
|
|
|
use crate::config::{SEND_SERVER_VERSION, VALIDATE_TOKENS};
|
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
|
|
|
|
|
|
|
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),
|
|
|
|
Self::HttpResponse(resp) => resp.respond_to(req),
|
2021-03-17 18:45:16 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/{token}/data/{chapter_hash}/{file_name}")]
|
|
|
|
async fn token_data(
|
|
|
|
state: Data<RwLockServerState>,
|
2021-04-14 20:44:13 -07:00
|
|
|
cache: Data<Mutex<Box<dyn Cache>>>,
|
2021-03-17 18:45:16 -07:00
|
|
|
path: Path<(String, String, String)>,
|
|
|
|
) -> impl Responder {
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/{token}/data-saver/{chapter_hash}/{file_name}")]
|
|
|
|
async fn token_data_saver(
|
|
|
|
state: Data<RwLockServerState>,
|
2021-04-14 20:44:13 -07:00
|
|
|
cache: Data<Mutex<Box<dyn Cache>>>,
|
2021-03-17 18:45:16 -07:00
|
|
|
path: Path<(String, String, String)>,
|
|
|
|
) -> impl Responder {
|
|
|
|
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-03-22 14:47:56 -07:00
|
|
|
pub async fn default(state: Data<RwLockServerState>, req: HttpRequest) -> impl Responder {
|
|
|
|
let path = &format!(
|
|
|
|
"{}{}",
|
|
|
|
state.0.read().image_server,
|
|
|
|
req.path().chars().skip(1).collect::<String>()
|
|
|
|
);
|
|
|
|
info!("Got unknown path, just proxying: {}", path);
|
2021-04-18 20:06:18 -07:00
|
|
|
let resp = match reqwest::get(path).await {
|
|
|
|
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-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);
|
|
|
|
}
|
|
|
|
|
|
|
|
let nonce = Nonce::from_slice(&data[..NONCEBYTES]).ok_or(TokenValidationError::InvalidNonce)?;
|
|
|
|
let decrypted = open_precomputed(&data[NONCEBYTES..], &nonce, precomputed_key)
|
|
|
|
.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
|
|
|
}
|
|
|
|
|
|
|
|
async fn fetch_image(
|
|
|
|
state: Data<RwLockServerState>,
|
2021-04-14 20:44:13 -07:00
|
|
|
cache: Data<Mutex<Box<dyn Cache>>>,
|
2021-03-17 18:45:16 -07:00
|
|
|
chapter_hash: String,
|
|
|
|
file_name: String,
|
|
|
|
is_data_saver: bool,
|
|
|
|
) -> ServerResponse {
|
2021-03-22 14:47:56 -07:00
|
|
|
let key = CacheKey(chapter_hash, file_name, is_data_saver);
|
2021-03-17 18:45:16 -07:00
|
|
|
|
2021-04-18 14:06:40 -07:00
|
|
|
match cache.lock().get(&key).await {
|
|
|
|
Some(Ok((image, metadata))) => return construct_response(image, metadata),
|
|
|
|
Some(Err(_)) => return ServerResponse::HttpResponse(HttpResponse::BadGateway().finish()),
|
|
|
|
_ => (),
|
2021-03-17 18:45:16 -07:00
|
|
|
}
|
|
|
|
|
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-03-22 14:47:56 -07:00
|
|
|
reqwest::get(format!(
|
2021-03-17 18:45:16 -07:00
|
|
|
"{}/data-saver/{}/{}",
|
2021-03-25 19:58:07 -07:00
|
|
|
state.0.read().image_server,
|
2021-04-18 20:06:18 -07:00
|
|
|
&key.0,
|
|
|
|
&key.1
|
2021-03-17 18:45:16 -07:00
|
|
|
))
|
|
|
|
} else {
|
2021-03-25 19:58:07 -07:00
|
|
|
reqwest::get(format!(
|
|
|
|
"{}/data/{}/{}",
|
|
|
|
state.0.read().image_server,
|
2021-04-18 20:06:18 -07:00
|
|
|
&key.0,
|
|
|
|
&key.1
|
2021-03-25 19:58:07 -07:00
|
|
|
))
|
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-03-22 14:47:56 -07:00
|
|
|
if resp.status() != 200 || !is_image {
|
|
|
|
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();
|
|
|
|
let (stream, metadata) = {
|
|
|
|
match cache.lock().put(key, Box::new(body), metadata).await {
|
|
|
|
Ok((stream, metadata)) => (stream, *metadata),
|
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-04-18 14:06:40 -07:00
|
|
|
return 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-04-18 20:06:18 -07:00
|
|
|
debug!("Constructing response");
|
|
|
|
|
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
|
|
|
}
|