Compare commits

..

2 commits

Author SHA1 Message Date
ee830fc152
more perf 2021-03-25 22:58:07 -04:00
f775ad72d3
Support CLI args 2021-03-25 21:06:54 -04:00
8 changed files with 310 additions and 103 deletions

148
Cargo.lock generated
View file

@ -606,6 +606,39 @@ dependencies = [
"winapi",
]
[[package]]
name = "clap"
version = "3.0.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim",
"termcolor",
"terminal_size",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "clap_derive"
version = "3.0.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "colored"
version = "1.9.3"
@ -693,9 +726,9 @@ dependencies = [
[[package]]
name = "ctor"
version = "0.1.19"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8f45d9ad417bcef4817d614a501ab55cdd96a6fdb24f49aab89a54acfd66b19"
checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d"
dependencies = [
"quote",
"syn",
@ -984,9 +1017,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d832b01df74254fe364568d6ddc294443f61cbec82816b60904303af87efae78"
checksum = "fc018e188373e2777d0ef2467ebff62a08e66c3f5857b23c8fbec3018210dc00"
dependencies = [
"bytes",
"fnv",
@ -1010,6 +1043,15 @@ dependencies = [
"ahash 0.4.7",
]
[[package]]
name = "heck"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hermit-abi"
version = "0.1.18"
@ -1176,9 +1218,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.90"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4aede83fc3617411dc6993bc8c70919750c1c257c6ca6a502aed6e0e2394ae"
checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7"
[[package]]
name = "libsodium-sys"
@ -1221,7 +1263,7 @@ dependencies = [
[[package]]
name = "mangadex-home"
version = "0.2.0"
version = "0.2.1"
dependencies = [
"actix-web",
"base64 0.13.0",
@ -1229,6 +1271,7 @@ dependencies = [
"bytes",
"cacache",
"chrono",
"clap",
"ctrlc",
"dotenv",
"futures",
@ -1435,6 +1478,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "os_str_bytes"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
[[package]]
name = "parking"
version = "2.0.0"
@ -1474,18 +1523,18 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project"
version = "1.0.5"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96fa8ebb90271c4477f144354485b8068bd8f6b78b428b01ba892ca26caf0b63"
checksum = "bc174859768806e91ae575187ada95c91a29e96a98dc5d2cd9a1fed039501ba6"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.0.5"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758669ae3558c6f74bd2a18b41f7ac0b5a195aea6639d6a9b5e5d1ad5ba24c0b"
checksum = "a490329918e856ed1b083f244e3bfe2d8c4f336407e4ea9e1a9f479ff09049e5"
dependencies = [
"proc-macro2",
"quote",
@ -1529,6 +1578,30 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.19"
@ -2027,6 +2100,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.64"
@ -2052,6 +2131,35 @@ dependencies = [
"winapi",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "textwrap"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
dependencies = [
"terminal_size",
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.24"
@ -2244,6 +2352,18 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
[[package]]
name = "unicode-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]]
name = "unicode-xid"
version = "0.2.1"
@ -2290,6 +2410,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b2f665b594b07095e3ac3f718e13c2197143416fae4c5706cffb7b1af8d7f1"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.3"

View file

@ -1,6 +1,6 @@
[package]
name = "mangadex-home"
version = "0.2.0"
version = "0.2.1"
license = "MIT OR Apache-2.0"
authors = ["Edward Shen <code@eddie.sh>"]
edition = "2018"
@ -15,6 +15,7 @@ bincode = "1"
bytes = "1"
cacache = "8"
chrono = { version = "0.4", features = [ "serde" ] }
clap = { version = "3.0.0-beta.2", features = [ "wrap_help" ] }
ctrlc = "3"
dotenv = "0.15"
futures = "0.3"

View file

@ -1,5 +1,11 @@
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
```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
values to make sure you don't exceed the actual quota.
## Installation
Either build it from source or run `cargo install mangadex-home`.
## Running
This version relies on loading configurations from `env`, or from a file called
`.env`. The config options are below:
Run `mangadex-home`, and make sure the advertised port is open on your firewall.
Do note that some configuration fields are required. See the next section for
details.
```
# 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=
```
## Configuration
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

37
src/config.rs Normal file
View file

@ -0,0 +1,37 @@
use std::num::{NonZeroU16, NonZeroUsize};
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
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)]
pub struct CliArgs {
/// The port to listen on.
#[clap(short, long, default_value = "42069", env = "PORT")]
pub port: NonZeroU16,
/// How large, in bytes, the in-memory cache should be. Note that this does
/// not include runtime memory usage.
#[clap(long, env = "MEM_CACHE_QUOTA_BYTES")]
pub memory_quota: NonZeroUsize,
/// How large, in bytes, the on-disk cache should be. Note that actual
/// values may be larger for metadata information.
#[clap(long, env = "DISK_CACHE_QUOTA_BYTES")]
pub disk_quota: usize,
/// Sets the location of the disk cache.
#[clap(long, default_value = "./cache", env = "DISK_CACHE_PATH")]
pub cache_path: PathBuf,
/// The network speed to advertise to Mangadex@Home control server.
#[clap(long, env = "MAX_NETWORK_SPEED")]
pub network_speed: NonZeroUsize,
/// Whether or not to provide the Server HTTP header to clients. This is
/// useful for debugging, but is generally not recommended for security
/// reasons.
#[clap(long, env = "ENABLE_SERVER_STRING", takes_value = false)]
pub enable_server_string: bool,
}

View file

@ -3,7 +3,7 @@
#![allow(clippy::future_not_send, clippy::module_name_repetitions)]
use std::env::{self, VarError};
use std::path::PathBuf;
use std::process;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::time::Duration;
@ -12,6 +12,8 @@ use std::{num::ParseIntError, sync::atomic::Ordering};
use actix_web::rt::{spawn, time, System};
use actix_web::web::{self, Data};
use actix_web::{App, HttpServer};
use clap::Clap;
use config::CliArgs;
use log::{debug, error, warn, LevelFilter};
use parking_lot::RwLock;
use rustls::{NoClientAuth, ServerConfig};
@ -21,6 +23,7 @@ use stop::send_stop;
use thiserror::Error;
mod cache;
mod config;
mod ping;
mod routes;
mod state;
@ -32,6 +35,7 @@ macro_rules! client_api_version {
"30"
};
}
#[derive(Error, Debug)]
enum ServerError {
#[error("There was a failure parsing config")]
@ -44,18 +48,38 @@ enum ServerError {
async fn main() -> Result<(), std::io::Error> {
// It's ok to fail early here, it would imply we have a invalid config.
dotenv::dotenv().ok();
let cli_args = CliArgs::parse();
let port = cli_args.port;
SimpleLogger::new()
.with_level(LevelFilter::Info)
.init()
.unwrap();
let config = Config::new().unwrap();
let port = config.port;
let server = ServerState::init(&config).await.unwrap();
let client_secret = if let Ok(v) = env::var("CLIENT_SECRET") {
v
} else {
error!("Client secret not found in ENV. Please set CLIENT_SECRET.");
process::exit(1);
};
let client_secret_1 = client_secret.clone();
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
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
let client_secret = config.secret.clone();
ctrlc::set_handler(move || {
let client_secret = client_secret.clone();
System::new().block_on(async move {
@ -65,23 +89,18 @@ async fn main() -> Result<(), std::io::Error> {
})
.expect("Error setting Ctrl-C handler");
let data_0 = Arc::new(RwLockServerState(RwLock::new(server)));
let data_1 = Arc::clone(&data_0);
let data_2 = Arc::clone(&data_0);
// Spawn ping task
spawn(async move {
let mut interval = time::interval(Duration::from_secs(90));
let mut data = Arc::clone(&data_0);
loop {
interval.tick().await;
debug!("Sending ping!");
ping::update_server_state(&config, &mut data).await;
ping::update_server_state(&client_secret_1, &cli_args, &mut data).await;
}
});
let mut tls_config = ServerConfig::new(NoClientAuth::new());
tls_config.cert_resolver = data_2;
// Start HTTPS server
HttpServer::new(move || {
App::new()
.service(routes::token_data)
@ -101,35 +120,3 @@ async fn main() -> Result<(), std::io::Error> {
Ok(())
}
pub struct Config {
secret: String,
port: u16,
memory_quota: usize,
disk_quota: usize,
disk_path: PathBuf,
network_speed: usize,
}
impl Config {
fn new() -> Result<Self, ServerError> {
let secret = env::var("CLIENT_SECRET")?;
let port = env::var("PORT")?.parse()?;
let disk_quota = env::var("DISK_CACHE_QUOTA_BYTES")?.parse()?;
let memory_quota = env::var("MEM_CACHE_QUOTA_BYTES")?.parse()?;
let network_speed = env::var("MAX_NETWORK_SPEED")?.parse()?;
let disk_path = env::var("DISK_CACHE_PATH")
.unwrap_or_else(|_| "./cache".to_string())
.parse()
.unwrap();
Ok(Self {
secret,
port,
disk_quota,
memory_quota,
disk_path,
network_speed,
})
}
}

View file

@ -1,29 +1,38 @@
use std::sync::Arc;
use std::{
num::{NonZeroU16, NonZeroUsize},
sync::atomic::Ordering,
};
use log::{error, info, warn};
use serde::{Deserialize, Serialize};
use sodiumoxide::crypto::box_::PrecomputedKey;
use url::Url;
use crate::config::VALIDATE_TOKENS;
use crate::state::RwLockServerState;
use crate::{client_api_version, Config};
use crate::{client_api_version, config::CliArgs};
pub const CONTROL_CENTER_PING_URL: &str = "https://api.mangadex.network/ping";
#[derive(Serialize, Debug)]
pub struct Request<'a> {
secret: &'a str,
port: u16,
port: NonZeroU16,
disk_space: usize,
network_speed: usize,
network_speed: NonZeroUsize,
build_version: usize,
tls_created_at: Option<String>,
}
impl<'a> Request<'a> {
fn from_config_and_state(config: &'a Config, state: &'a Arc<RwLockServerState>) -> Self {
fn from_config_and_state(
secret: &'a str,
config: &CliArgs,
state: &Arc<RwLockServerState>,
) -> Self {
Self {
secret: &config.secret,
secret,
port: config.port,
disk_space: config.disk_quota,
network_speed: config.network_speed,
@ -34,10 +43,10 @@ impl<'a> Request<'a> {
}
#[allow(clippy::fallible_impl_from)]
impl<'a> From<&'a Config> for Request<'a> {
fn from(config: &'a Config) -> Self {
impl<'a> From<(&'a str, &CliArgs)> for Request<'a> {
fn from((secret, config): (&'a str, &CliArgs)) -> Self {
Self {
secret: &config.secret,
secret,
port: config.port,
disk_space: config.disk_quota,
network_speed: config.network_speed,
@ -67,8 +76,8 @@ pub struct Tls {
pub certificate: String,
}
pub async fn update_server_state(req: &Config, data: &mut Arc<RwLockServerState>) {
let req = Request::from_config_and_state(req, data);
pub async fn update_server_state(secret: &str, req: &CliArgs, data: &mut Arc<RwLockServerState>) {
let req = Request::from_config_and_state(secret, req, data);
let client = reqwest::Client::new();
let resp = client.post(CONTROL_CENTER_PING_URL).json(&req).send().await;
match resp {
@ -89,13 +98,13 @@ pub async fn update_server_state(req: &Config, data: &mut Arc<RwLockServerState>
}
}
if write_guard.force_tokens != resp.force_tokens {
write_guard.force_tokens = resp.force_tokens;
if VALIDATE_TOKENS.load(Ordering::Acquire) != resp.force_tokens {
if resp.force_tokens {
info!("Client received command to enforce token validity.");
} else {
info!("Client received command to no longer enforce token validity");
}
VALIDATE_TOKENS.store(resp.force_tokens, Ordering::Release);
}
if let Some(tls) = resp.tls {

View file

@ -1,4 +1,5 @@
use std::convert::Infallible;
use std::sync::atomic::Ordering;
use actix_web::dev::HttpResponseBuilder;
use actix_web::http::header::{
@ -18,6 +19,7 @@ use thiserror::Error;
use crate::cache::{CacheKey, CachedImage};
use crate::client_api_version;
use crate::config::{SEND_SERVER_VERSION, VALIDATE_TOKENS};
use crate::state::RwLockServerState;
pub const BASE64_CONFIG: base64::Config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
@ -52,7 +54,7 @@ async fn token_data(
path: Path<(String, String, String)>,
) -> impl Responder {
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) {
return ServerResponse::TokenValidationError(e);
}
@ -67,7 +69,7 @@ async fn token_data_saver(
path: Path<(String, String, String)>,
) -> impl Responder {
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) {
return ServerResponse::TokenValidationError(e);
}
@ -159,8 +161,13 @@ fn push_headers(builder: &mut HttpResponseBuilder) -> &mut HttpResponseBuilder {
.insert_header((ACCESS_CONTROL_ALLOW_ORIGIN, "https://mangadex.org"))
.insert_header((ACCESS_CONTROL_EXPOSE_HEADERS, "*"))
.insert_header((CACHE_CONTROL, "public, max-age=1209600"))
.insert_header(("Timing-Allow-Origin", "https://mangadex.org"))
.insert_header(("Server", SERVER_ID_STRING))
.insert_header(("Timing-Allow-Origin", "https://mangadex.org"));
if SEND_SERVER_VERSION.load(Ordering::Acquire) {
builder.insert_header(("Server", SERVER_ID_STRING));
}
builder
}
async fn fetch_image(
@ -175,14 +182,23 @@ async fn fetch_image(
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 {
reqwest::get(format!(
"{}/data-saver/{}/{}",
state.image_server, &key.1, &key.2
state.0.read().image_server,
&key.1,
&key.2
))
} 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;
@ -202,6 +218,7 @@ async fn fetch_image(
if let Some(content_type) = content_type {
resp_builder.insert_header((CONTENT_TYPE, content_type));
}
push_headers(&mut resp_builder);
return ServerResponse::HttpResponse(
@ -226,7 +243,7 @@ async fn fetch_image(
last_modified,
};
let resp = construct_response(&cached);
state.cache.put(key, cached).await;
state.0.write().cache.put(key, cached).await;
return resp;
}
Err(e) => {

View file

@ -1,8 +1,9 @@
use std::{io::BufReader, sync::Arc};
use std::io::BufReader;
use std::sync::{atomic::Ordering, Arc};
use crate::cache::Cache;
use crate::config::{SEND_SERVER_VERSION, VALIDATE_TOKENS};
use crate::ping::{Request, Response, Tls, CONTROL_CENTER_PING_URL};
use crate::Config;
use crate::{cache::Cache, config::CliArgs};
use log::{error, info, warn};
use parking_lot::RwLock;
use rustls::internal::pemfile::{certs, rsa_private_keys};
@ -15,7 +16,6 @@ pub struct ServerState {
pub precomputed_key: PrecomputedKey,
pub image_server: Url,
pub tls_config: Tls,
pub force_tokens: bool,
pub url: String,
pub cache: Cache,
pub log_state: LogState,
@ -26,13 +26,18 @@ pub struct LogState {
}
impl ServerState {
pub async fn init(config: &Config) -> Result<Self, ()> {
pub async fn init(secret: &str, config: &CliArgs) -> Result<Self, ()> {
let resp = reqwest::Client::new()
.post(CONTROL_CENTER_PING_URL)
.json(&Request::from(config))
.json(&Request::from((secret, config)))
.send()
.await;
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 {
Ok(resp) => match resp.json::<Response>().await {
Ok(resp) => {
@ -67,16 +72,17 @@ impl ServerState {
info!("This client will not validate tokens.");
}
VALIDATE_TOKENS.store(resp.force_tokens, Ordering::Release);
Ok(Self {
precomputed_key: key,
image_server: resp.image_server,
tls_config: resp.tls.unwrap(),
force_tokens: resp.force_tokens,
url: resp.url,
cache: Cache::new(
config.memory_quota,
config.memory_quota.get(),
config.disk_quota,
config.disk_path.clone(),
config.cache_path.clone(),
),
log_state: LogState {
was_paused_before: resp.paused,