more perf

This commit is contained in:
Edward Shen 2021-03-25 22:58:07 -04:00
parent f775ad72d3
commit ee830fc152
Signed by: edward
GPG key ID: 19182661E818369F
8 changed files with 100 additions and 56 deletions

2
Cargo.lock generated
View file

@ -1263,7 +1263,7 @@ dependencies = [
[[package]] [[package]]
name = "mangadex-home" name = "mangadex-home"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"base64 0.13.0", "base64 0.13.0",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "mangadex-home" name = "mangadex-home"
version = "0.2.0" version = "0.2.1"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
authors = ["Edward Shen <code@eddie.sh>"] authors = ["Edward Shen <code@eddie.sh>"]
edition = "2018" edition = "2018"

View file

@ -1,5 +1,11 @@
A Rust implementation of a Mangadex @ Home client. A Rust implementation of a Mangadex @ Home client.
This client contains the following features:
- Multi-threaded
- HTTP/2 support
- No support for TLS 1.1 or 1.0
## Building ## Building
```sh ```sh
@ -21,24 +27,42 @@ Note that these quotas are closer to a rough estimate, and is not guaranteed to
be strictly below these values, so it's recommended to under set your config be strictly below these values, so it's recommended to under set your config
values to make sure you don't exceed the actual quota. values to make sure you don't exceed the actual quota.
## Installation
Either build it from source or run `cargo install mangadex-home`.
## Running ## Running
This version relies on loading configurations from `env`, or from a file called Run `mangadex-home`, and make sure the advertised port is open on your firewall.
`.env`. The config options are below: Do note that some configuration fields are required. See the next section for
details.
``` ## Configuration
# Your MD@H client secret
CLIENT_SECRET=
# The port to use
PORT=
# The maximum disk cache size, in bytes
DISK_CACHE_QUOTA_BYTES=
# The path where the on-disk cache should be stored
DISK_CACHE_PATH="./cache" # Optional, default is "./cache"
# The maximum memory cache size, in bytes
MEM_CACHE_QUOTA_BYTES=
# The maximum memory speed, in bytes per second.
MAX_NETWORK_SPEED=
```
After these values have been set, simply run the client. Most configuration options can be either provided on the command line, sourced
from a `.env` file, or sourced directly from the environment. Do not that the
client secret is an exception. You must provide the client secret from the
environment or from the `.env` file, as providing client secrets in a shell is a
operation security risk.
The following options are required:
- Client Secret
- Memory cache quota
- Disk cache quota
- Advertised network speed
The following are optional as a default value will be set for you:
- Port
- Disk cache path
### Advanced configuration
This implementation prefers to act more secure by default. As a result, some
features that the official specification requires are not enabled by default.
If you don't know why these features are disabled by default, then don't enable
these, as they may generally weaken the security stance of the client for more
compatibility.
- Sending Server version string

View file

@ -1,8 +1,15 @@
use std::num::{NonZeroU16, NonZeroUsize}; use std::num::{NonZeroU16, NonZeroUsize};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use clap::Clap; use clap::Clap;
// Validate tokens is an atomic because it's faster than locking on rwlock.
pub static VALIDATE_TOKENS: AtomicBool = AtomicBool::new(false);
// We use an atomic here because it's better for us to not pass the config
// everywhere.
pub static SEND_SERVER_VERSION: AtomicBool = AtomicBool::new(false);
#[derive(Clap)] #[derive(Clap)]
pub struct CliArgs { pub struct CliArgs {
/// The port to listen on. /// The port to listen on.
@ -25,6 +32,6 @@ pub struct CliArgs {
/// Whether or not to provide the Server HTTP header to clients. This is /// Whether or not to provide the Server HTTP header to clients. This is
/// useful for debugging, but is generally not recommended for security /// useful for debugging, but is generally not recommended for security
/// reasons. /// reasons.
#[clap(long, env = "ENABLE_SERVER_STRING")] #[clap(long, env = "ENABLE_SERVER_STRING", takes_value = false)]
pub enable_server_string: bool, pub enable_server_string: bool,
} }

View file

@ -2,13 +2,11 @@
// We're end users, so these is ok // We're end users, so these is ok
#![allow(clippy::future_not_send, clippy::module_name_repetitions)] #![allow(clippy::future_not_send, clippy::module_name_repetitions)]
use std::env::{self, VarError};
use std::process;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use std::{
env::{self, VarError},
process,
};
use std::{num::ParseIntError, sync::atomic::Ordering}; use std::{num::ParseIntError, sync::atomic::Ordering};
use actix_web::rt::{spawn, time, System}; use actix_web::rt::{spawn, time, System};
@ -37,6 +35,7 @@ macro_rules! client_api_version {
"30" "30"
}; };
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]
enum ServerError { enum ServerError {
#[error("There was a failure parsing config")] #[error("There was a failure parsing config")]
@ -50,22 +49,33 @@ async fn main() -> Result<(), std::io::Error> {
// It's ok to fail early here, it would imply we have a invalid config. // It's ok to fail early here, it would imply we have a invalid config.
dotenv::dotenv().ok(); dotenv::dotenv().ok();
let cli_args = CliArgs::parse(); let cli_args = CliArgs::parse();
let port = cli_args.port;
SimpleLogger::new() SimpleLogger::new()
.with_level(LevelFilter::Info) .with_level(LevelFilter::Info)
.init() .init()
.unwrap(); .unwrap();
let port = cli_args.port;
let client_secret = if let Ok(v) = env::var("CLIENT_SECRET") { let client_secret = if let Ok(v) = env::var("CLIENT_SECRET") {
v v
} else { } else {
eprintln!("Client secret not found in ENV. Please set CLIENT_SECRET."); error!("Client secret not found in ENV. Please set CLIENT_SECRET.");
process::exit(1); process::exit(1);
}; };
let client_secret_1 = client_secret.clone(); let client_secret_1 = client_secret.clone();
let server = ServerState::init(&client_secret, &cli_args).await.unwrap(); let server = ServerState::init(&client_secret, &cli_args).await.unwrap();
let data_0 = Arc::new(RwLockServerState(RwLock::new(server)));
let data_1 = Arc::clone(&data_0);
// What's nice is that Rustls only supports TLS 1.2 and 1.3.
let mut tls_config = ServerConfig::new(NoClientAuth::new());
tls_config.cert_resolver = data_0.clone();
//
// At this point, the server is ready to start, and starts the necessary
// threads.
//
// Set ctrl+c to send a stop message // Set ctrl+c to send a stop message
let running = Arc::new(AtomicBool::new(true)); let running = Arc::new(AtomicBool::new(true));
@ -79,10 +89,7 @@ async fn main() -> Result<(), std::io::Error> {
}) })
.expect("Error setting Ctrl-C handler"); .expect("Error setting Ctrl-C handler");
let data_0 = Arc::new(RwLockServerState(RwLock::new(server))); // Spawn ping task
let data_1 = Arc::clone(&data_0);
let data_2 = Arc::clone(&data_0);
spawn(async move { spawn(async move {
let mut interval = time::interval(Duration::from_secs(90)); let mut interval = time::interval(Duration::from_secs(90));
let mut data = Arc::clone(&data_0); let mut data = Arc::clone(&data_0);
@ -93,9 +100,7 @@ async fn main() -> Result<(), std::io::Error> {
} }
}); });
let mut tls_config = ServerConfig::new(NoClientAuth::new()); // Start HTTPS server
tls_config.cert_resolver = data_2;
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.service(routes::token_data) .service(routes::token_data)

View file

@ -1,6 +1,7 @@
use std::sync::Arc;
use std::{ use std::{
num::{NonZeroU16, NonZeroUsize}, num::{NonZeroU16, NonZeroUsize},
sync::Arc, sync::atomic::Ordering,
}; };
use log::{error, info, warn}; use log::{error, info, warn};
@ -8,6 +9,7 @@ use serde::{Deserialize, Serialize};
use sodiumoxide::crypto::box_::PrecomputedKey; use sodiumoxide::crypto::box_::PrecomputedKey;
use url::Url; use url::Url;
use crate::config::VALIDATE_TOKENS;
use crate::state::RwLockServerState; use crate::state::RwLockServerState;
use crate::{client_api_version, config::CliArgs}; use crate::{client_api_version, config::CliArgs};
@ -96,13 +98,13 @@ pub async fn update_server_state(secret: &str, req: &CliArgs, data: &mut Arc<RwL
} }
} }
if write_guard.force_tokens != resp.force_tokens { if VALIDATE_TOKENS.load(Ordering::Acquire) != resp.force_tokens {
write_guard.force_tokens = resp.force_tokens;
if resp.force_tokens { if resp.force_tokens {
info!("Client received command to enforce token validity."); info!("Client received command to enforce token validity.");
} else { } else {
info!("Client received command to no longer enforce token validity"); info!("Client received command to no longer enforce token validity");
} }
VALIDATE_TOKENS.store(resp.force_tokens, Ordering::Release);
} }
if let Some(tls) = resp.tls { if let Some(tls) = resp.tls {

View file

@ -1,7 +1,5 @@
use std::{ use std::convert::Infallible;
convert::Infallible, use std::sync::atomic::Ordering;
sync::atomic::{AtomicBool, Ordering},
};
use actix_web::dev::HttpResponseBuilder; use actix_web::dev::HttpResponseBuilder;
use actix_web::http::header::{ use actix_web::http::header::{
@ -21,12 +19,11 @@ use thiserror::Error;
use crate::cache::{CacheKey, CachedImage}; use crate::cache::{CacheKey, CachedImage};
use crate::client_api_version; use crate::client_api_version;
use crate::config::{SEND_SERVER_VERSION, VALIDATE_TOKENS};
use crate::state::RwLockServerState; use crate::state::RwLockServerState;
pub const BASE64_CONFIG: base64::Config = base64::Config::new(base64::CharacterSet::UrlSafe, false); pub const BASE64_CONFIG: base64::Config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
pub static SEND_SERVER_VERSION: AtomicBool = AtomicBool::new(false);
const SERVER_ID_STRING: &str = concat!( const SERVER_ID_STRING: &str = concat!(
env!("CARGO_CRATE_NAME"), env!("CARGO_CRATE_NAME"),
" ", " ",
@ -57,7 +54,7 @@ async fn token_data(
path: Path<(String, String, String)>, path: Path<(String, String, String)>,
) -> impl Responder { ) -> impl Responder {
let (token, chapter_hash, file_name) = path.into_inner(); let (token, chapter_hash, file_name) = path.into_inner();
if state.0.read().force_tokens { if VALIDATE_TOKENS.load(Ordering::Acquire) {
if let Err(e) = validate_token(&state.0.read().precomputed_key, token, &chapter_hash) { if let Err(e) = validate_token(&state.0.read().precomputed_key, token, &chapter_hash) {
return ServerResponse::TokenValidationError(e); return ServerResponse::TokenValidationError(e);
} }
@ -72,7 +69,7 @@ async fn token_data_saver(
path: Path<(String, String, String)>, path: Path<(String, String, String)>,
) -> impl Responder { ) -> impl Responder {
let (token, chapter_hash, file_name) = path.into_inner(); let (token, chapter_hash, file_name) = path.into_inner();
if state.0.read().force_tokens { if VALIDATE_TOKENS.load(Ordering::Acquire) {
if let Err(e) = validate_token(&state.0.read().precomputed_key, token, &chapter_hash) { if let Err(e) = validate_token(&state.0.read().precomputed_key, token, &chapter_hash) {
return ServerResponse::TokenValidationError(e); return ServerResponse::TokenValidationError(e);
} }
@ -185,15 +182,23 @@ async fn fetch_image(
return construct_response(cached); return construct_response(cached);
} }
let mut state = state.0.write(); // It's important to not get a write lock before this request, else we're
// holding the read lock until the await resolves.
let resp = if is_data_saver { let resp = if is_data_saver {
reqwest::get(format!( reqwest::get(format!(
"{}/data-saver/{}/{}", "{}/data-saver/{}/{}",
state.image_server, &key.1, &key.2 state.0.read().image_server,
&key.1,
&key.2
)) ))
} else { } else {
reqwest::get(format!("{}/data/{}/{}", state.image_server, &key.1, &key.2)) reqwest::get(format!(
"{}/data/{}/{}",
state.0.read().image_server,
&key.1,
&key.2
))
} }
.await; .await;
@ -238,7 +243,7 @@ async fn fetch_image(
last_modified, last_modified,
}; };
let resp = construct_response(&cached); let resp = construct_response(&cached);
state.cache.put(key, cached).await; state.0.write().cache.put(key, cached).await;
return resp; return resp;
} }
Err(e) => { Err(e) => {

View file

@ -1,10 +1,8 @@
use std::{ use std::io::BufReader;
io::BufReader, use std::sync::{atomic::Ordering, Arc};
sync::{atomic::Ordering, Arc},
};
use crate::config::{SEND_SERVER_VERSION, VALIDATE_TOKENS};
use crate::ping::{Request, Response, Tls, CONTROL_CENTER_PING_URL}; use crate::ping::{Request, Response, Tls, CONTROL_CENTER_PING_URL};
use crate::routes::SEND_SERVER_VERSION;
use crate::{cache::Cache, config::CliArgs}; use crate::{cache::Cache, config::CliArgs};
use log::{error, info, warn}; use log::{error, info, warn};
use parking_lot::RwLock; use parking_lot::RwLock;
@ -18,7 +16,6 @@ pub struct ServerState {
pub precomputed_key: PrecomputedKey, pub precomputed_key: PrecomputedKey,
pub image_server: Url, pub image_server: Url,
pub tls_config: Tls, pub tls_config: Tls,
pub force_tokens: bool,
pub url: String, pub url: String,
pub cache: Cache, pub cache: Cache,
pub log_state: LogState, pub log_state: LogState,
@ -36,7 +33,10 @@ impl ServerState {
.send() .send()
.await; .await;
SEND_SERVER_VERSION.store(config.enable_server_string, Ordering::Release); if config.enable_server_string {
warn!("Client will send Server header in responses. This is not recommended!");
SEND_SERVER_VERSION.store(true, Ordering::Release);
}
match resp { match resp {
Ok(resp) => match resp.json::<Response>().await { Ok(resp) => match resp.json::<Response>().await {
@ -72,11 +72,12 @@ impl ServerState {
info!("This client will not validate tokens."); info!("This client will not validate tokens.");
} }
VALIDATE_TOKENS.store(resp.force_tokens, Ordering::Release);
Ok(Self { Ok(Self {
precomputed_key: key, precomputed_key: key,
image_server: resp.image_server, image_server: resp.image_server,
tls_config: resp.tls.unwrap(), tls_config: resp.tls.unwrap(),
force_tokens: resp.force_tokens,
url: resp.url, url: resp.url,
cache: Cache::new( cache: Cache::new(
config.memory_quota.get(), config.memory_quota.get(),