minimal viable product

This commit is contained in:
Edward Shen 2021-03-17 22:41:48 -04:00
parent 76d50a9c2c
commit 562b439c25
Signed by: edward
GPG key ID: 19182661E818369F
3 changed files with 107 additions and 62 deletions

View file

@ -1,4 +1,5 @@
#![warn(clippy::pedantic, clippy::nursery)] #![warn(clippy::pedantic, clippy::nursery)]
#![allow(clippy::future_not_send)] // We're end users, so this is ok
use std::env::{self, VarError}; use std::env::{self, VarError};
use std::time::Duration; use std::time::Duration;
@ -12,7 +13,7 @@ use awc::{error::SendRequestError, Client};
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use lru::LruCache; use lru::LruCache;
use parking_lot::RwLock; use parking_lot::RwLock;
use ping::{PingRequest, PingResponse}; use ping::{Request, Response};
use rustls::sign::{CertifiedKey, RSASigningKey}; use rustls::sign::{CertifiedKey, RSASigningKey};
use rustls::PrivateKey; use rustls::PrivateKey;
use rustls::{Certificate, NoClientAuth, ResolvesServerCert, ServerConfig}; use rustls::{Certificate, NoClientAuth, ResolvesServerCert, ServerConfig};
@ -40,24 +41,32 @@ struct ServerState {
tls_config: Tls, tls_config: Tls,
disabled_tokens: bool, disabled_tokens: bool,
url: String, url: String,
cache: LruCache<(String, String, bool), Vec<u8>>, cache: LruCache<(String, String, bool), CachedImage>,
}
struct CachedImage {
data: Vec<u8>,
content_type: Option<Vec<u8>>,
content_length: Option<Vec<u8>>,
last_modified: Option<Vec<u8>>,
} }
impl ServerState { impl ServerState {
async fn init(config: &Config) -> Result<Self, ()> { async fn init(config: &Config) -> Result<Self, ()> {
let resp = Client::new() let resp = Client::new()
.post(CONTROL_CENTER_PING_URL) .post(CONTROL_CENTER_PING_URL)
.send_json(&PingRequest::from(config)) .send_json(&Request::from(config))
.await; .await;
match resp { match resp {
Ok(mut resp) => match resp.json::<PingResponse>().await { Ok(mut resp) => match resp.json::<Response>().await {
Ok(resp) => { Ok(resp) => {
let key = resp let key = resp
.token_key .token_key
.and_then(|key| match PrecomputedKey::from_slice(key.as_bytes()) { .and_then(|key| {
Some(key) => Some(key), if let Some(key) = PrecomputedKey::from_slice(key.as_bytes()) {
None => { Some(key)
} else {
error!("Failed to parse token key: got {}", key); error!("Failed to parse token key: got {}", key);
None None
} }
@ -106,7 +115,7 @@ impl ServerState {
} }
} }
struct RwLockServerState(RwLock<ServerState>); pub struct RwLockServerState(RwLock<ServerState>);
impl ResolvesServerCert for RwLockServerState { impl ResolvesServerCert for RwLockServerState {
fn resolve(&self, _: rustls::ClientHello) -> Option<CertifiedKey> { fn resolve(&self, _: rustls::ClientHello) -> Option<CertifiedKey> {
@ -166,7 +175,7 @@ async fn main() -> Result<(), std::io::Error> {
.await .await
} }
struct Config { pub struct Config {
secret: String, secret: String,
port: u16, port: u16,
disk_quota: usize, disk_quota: usize,

View file

@ -9,7 +9,7 @@ use url::Url;
use crate::{client_api_version, Config, RwLockServerState, CONTROL_CENTER_PING_URL}; use crate::{client_api_version, Config, RwLockServerState, CONTROL_CENTER_PING_URL};
#[derive(Serialize)] #[derive(Serialize)]
pub struct PingRequest<'a> { pub struct Request<'a> {
secret: &'a str, secret: &'a str,
port: u16, port: u16,
disk_space: usize, disk_space: usize,
@ -18,7 +18,7 @@ pub struct PingRequest<'a> {
tls_created_at: Option<String>, tls_created_at: Option<String>,
} }
impl<'a> PingRequest<'a> { impl<'a> Request<'a> {
fn from_config_and_state(config: &'a Config, state: &'a Arc<RwLockServerState>) -> Self { fn from_config_and_state(config: &'a Config, state: &'a Arc<RwLockServerState>) -> Self {
Self { Self {
secret: &config.secret, secret: &config.secret,
@ -31,7 +31,7 @@ impl<'a> PingRequest<'a> {
} }
} }
impl<'a> From<&'a Config> for PingRequest<'a> { impl<'a> From<&'a Config> for Request<'a> {
fn from(config: &'a Config) -> Self { fn from(config: &'a Config) -> Self {
Self { Self {
secret: &config.secret, secret: &config.secret,
@ -45,32 +45,32 @@ impl<'a> From<&'a Config> for PingRequest<'a> {
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub(crate) struct PingResponse { pub struct Response {
pub(crate) image_server: Url, pub image_server: Url,
pub(crate) latest_build: usize, pub latest_build: usize,
pub(crate) url: String, pub url: String,
pub(crate) token_key: Option<String>, pub token_key: Option<String>,
pub(crate) compromised: bool, pub compromised: bool,
pub(crate) paused: bool, pub paused: bool,
pub(crate) disabled_tokens: bool, pub disabled_tokens: bool,
pub(crate) tls: Option<Tls>, pub tls: Option<Tls>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub(crate) struct Tls { pub struct Tls {
pub created_at: String, pub created_at: String,
pub private_key: Vec<u8>, pub private_key: Vec<u8>,
pub certificate: Vec<u8>, pub certificate: Vec<u8>,
} }
pub(crate) async fn update_server_state(req: &Config, data: &mut Arc<RwLockServerState>) { pub async fn update_server_state(req: &Config, data: &mut Arc<RwLockServerState>) {
let req = PingRequest::from_config_and_state(req, data); let req = Request::from_config_and_state(req, data);
let resp = Client::new() let resp = Client::new()
.post(CONTROL_CENTER_PING_URL) .post(CONTROL_CENTER_PING_URL)
.send_json(&req) .send_json(&req)
.await; .await;
match resp { match resp {
Ok(mut resp) => match resp.json::<PingResponse>().await { Ok(mut resp) => match resp.json::<Response>().await {
Ok(resp) => { Ok(resp) => {
let mut write_guard = data.0.write(); let mut write_guard = data.0.write();

View file

@ -1,7 +1,12 @@
use std::convert::Infallible; use std::convert::Infallible;
use actix_web::{dev::HttpResponseBuilder, get, web::Data, HttpRequest, Responder}; use actix_web::dev::HttpResponseBuilder;
use actix_web::{web::Path, HttpResponse}; 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;
use actix_web::{get, web::Data, HttpRequest, HttpResponse, Responder};
use awc::Client; use awc::Client;
use base64::DecodeError; use base64::DecodeError;
use bytes::Bytes; use bytes::Bytes;
@ -12,7 +17,7 @@ use serde::Deserialize;
use sodiumoxide::crypto::box_::{open_precomputed, Nonce, PrecomputedKey, NONCEBYTES}; use sodiumoxide::crypto::box_::{open_precomputed, Nonce, PrecomputedKey, NONCEBYTES};
use thiserror::Error; use thiserror::Error;
use crate::{client_api_version, RwLockServerState}; use crate::{client_api_version, CachedImage, RwLockServerState};
const BASE64_CONFIG: base64::Config = base64::Config::new(base64::CharacterSet::UrlSafe, false); const BASE64_CONFIG: base64::Config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
@ -33,8 +38,8 @@ enum ServerResponse {
impl Responder for ServerResponse { impl Responder for ServerResponse {
fn respond_to(self, req: &HttpRequest) -> HttpResponse { fn respond_to(self, req: &HttpRequest) -> HttpResponse {
match self { match self {
ServerResponse::TokenValidationError(e) => e.respond_to(req), Self::TokenValidationError(e) => e.respond_to(req),
ServerResponse::HttpResponse(resp) => resp.respond_to(req), Self::HttpResponse(resp) => resp.respond_to(req),
} }
} }
} }
@ -115,6 +120,12 @@ fn validate_token(
token: String, token: String,
chapter_hash: &str, chapter_hash: &str,
) -> Result<(), TokenValidationError> { ) -> Result<(), TokenValidationError> {
#[derive(Deserialize)]
struct Token<'a> {
expires: DateTime<Utc>,
hash: &'a str,
}
let data = base64::decode_config(token, BASE64_CONFIG)?; let data = base64::decode_config(token, BASE64_CONFIG)?;
if data.len() < NONCEBYTES { if data.len() < NONCEBYTES {
return Err(TokenValidationError::IncompleteNonce); return Err(TokenValidationError::IncompleteNonce);
@ -124,12 +135,6 @@ fn validate_token(
let decrypted = open_precomputed(&data[NONCEBYTES..], &nonce, precomputed_key) let decrypted = open_precomputed(&data[NONCEBYTES..], &nonce, precomputed_key)
.map_err(|_| TokenValidationError::DecryptionFailure)?; .map_err(|_| TokenValidationError::DecryptionFailure)?;
#[derive(Deserialize)]
struct Token<'a> {
expires: DateTime<Utc>,
hash: &'a str,
}
let parsed_token: Token = let parsed_token: Token =
serde_json::from_slice(&decrypted).map_err(|_| TokenValidationError::InvalidToken)?; serde_json::from_slice(&decrypted).map_err(|_| TokenValidationError::InvalidToken)?;
@ -146,10 +151,10 @@ fn validate_token(
fn push_headers(builder: &mut HttpResponseBuilder) -> &mut HttpResponseBuilder { fn push_headers(builder: &mut HttpResponseBuilder) -> &mut HttpResponseBuilder {
builder builder
.insert_header(("X-Content-Type-Options", "nosniff")) .insert_header((X_CONTENT_TYPE_OPTIONS, "nosniff"))
.insert_header(("Access-Control-Allow-Origin", "https://mangadex.org")) .insert_header((ACCESS_CONTROL_ALLOW_ORIGIN, "https://mangadex.org"))
.insert_header(("Access-Control-Expose-Headers", "*")) .insert_header((ACCESS_CONTROL_EXPOSE_HEADERS, "*"))
.insert_header(("Cache-Control", "public, max-age=1209600")) .insert_header((CACHE_CONTROL, "public, max-age=1209600"))
.insert_header(("Timing-Allow-Origin", "https://mangadex.org")) .insert_header(("Timing-Allow-Origin", "https://mangadex.org"))
.insert_header(("Server", SERVER_ID_STRING)) .insert_header(("Server", SERVER_ID_STRING))
} }
@ -163,12 +168,7 @@ async fn fetch_image(
let key = (chapter_hash, file_name, is_data_saver); let key = (chapter_hash, file_name, is_data_saver);
if let Some(cached) = state.0.write().cache.get(&key) { if let Some(cached) = state.0.write().cache.get(&key) {
let data = cached.to_vec(); return construct_response(cached);
let data: Vec<Result<Bytes, Infallible>> = data
.chunks(1024)
.map(|v| Ok(Bytes::from(v.to_vec())))
.collect();
return ServerResponse::HttpResponse(HttpResponse::Ok().streaming(stream::iter(data)));
} }
let mut state = state.0.write(); let mut state = state.0.write();
@ -184,25 +184,61 @@ async fn fetch_image(
.await; .await;
match resp { match resp {
Ok(mut resp) => match resp.body().await { Ok(mut resp) => {
Ok(bytes) => { let headers = resp.headers();
state.cache.put(key, bytes.to_vec()); let content_type = headers.get(CONTENT_TYPE).map(AsRef::as_ref).map(Vec::from);
let bytes: Vec<Result<Bytes, Infallible>> = bytes let content_length = headers
.chunks(1024) .get(CONTENT_LENGTH)
.map(|v| Ok(Bytes::from(v.to_vec()))) .map(AsRef::as_ref)
.collect(); .map(Vec::from);
return ServerResponse::HttpResponse( let last_modified = headers.get(LAST_MODIFIED).map(AsRef::as_ref).map(Vec::from);
HttpResponse::Ok().streaming(stream::iter(bytes)), let body = resp.body().await;
); match body {
Ok(bytes) => {
let cached = CachedImage {
data: bytes.to_vec(),
content_type,
content_length,
last_modified,
};
let resp = construct_response(&cached);
state.cache.put(key, cached);
return resp;
}
Err(e) => {
warn!("Got payload error from image server: {}", e);
ServerResponse::HttpResponse(
push_headers(&mut HttpResponse::ServiceUnavailable()).finish(),
)
}
} }
Err(e) => { }
warn!("Got payload error from image server: {}", e);
todo!()
}
},
Err(e) => { Err(e) => {
error!("Failed to fetch image from server: {}", e); error!("Failed to fetch image from server: {}", e);
todo!() ServerResponse::HttpResponse(
push_headers(&mut HttpResponse::ServiceUnavailable()).finish(),
)
} }
} }
} }
fn construct_response(cached: &CachedImage) -> ServerResponse {
let data: Vec<Result<Bytes, Infallible>> = cached
.data
.to_vec()
.chunks(1024)
.map(|v| Ok(Bytes::from(v.to_vec())))
.collect();
let mut resp = HttpResponse::Ok();
if let Some(content_type) = &cached.content_type {
resp.append_header((CONTENT_TYPE, &**content_type));
}
if let Some(content_length) = &cached.content_length {
resp.append_header((CONTENT_LENGTH, &**content_length));
}
if let Some(last_modified) = &cached.last_modified {
resp.append_header((LAST_MODIFIED, &**last_modified));
}
return ServerResponse::HttpResponse(push_headers(&mut resp).streaming(stream::iter(data)));
}