initial proof of concept
This commit is contained in:
commit
76d50a9c2c
8 changed files with 2182 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"actix"
|
||||||
|
]
|
||||||
|
}
|
1637
Cargo.lock
generated
Normal file
1637
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "mangadex-home-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Edward Shen <code@eddie.sh>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-web = { version = "4.0.0-beta.4", features = [ "rustls" ] }
|
||||||
|
awc = "3.0.0-beta.3"
|
||||||
|
parking_lot = "0.11"
|
||||||
|
base64 = "0.13"
|
||||||
|
sodiumoxide = "0.2"
|
||||||
|
thiserror = "1"
|
||||||
|
chrono = { version = "0.4", features = [ "serde" ] }
|
||||||
|
serde = "1"
|
||||||
|
serde_json = "1"
|
||||||
|
url = { version = "2", features = [ "serde" ] }
|
||||||
|
dotenv = "0.15"
|
||||||
|
log = "0.4"
|
||||||
|
rustls = "0.19"
|
||||||
|
simple_logger = "1"
|
||||||
|
lru = "0.6"
|
||||||
|
futures = "0.3"
|
||||||
|
bytes = "1"
|
190
src/main.rs
Normal file
190
src/main.rs
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
#![warn(clippy::pedantic, clippy::nursery)]
|
||||||
|
|
||||||
|
use std::env::{self, VarError};
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::{num::ParseIntError, sync::Arc};
|
||||||
|
|
||||||
|
use crate::ping::Tls;
|
||||||
|
use actix_web::rt::{spawn, time};
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use actix_web::{App, HttpServer};
|
||||||
|
use awc::{error::SendRequestError, Client};
|
||||||
|
use log::{debug, error, info, warn};
|
||||||
|
use lru::LruCache;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use ping::{PingRequest, PingResponse};
|
||||||
|
use rustls::sign::{CertifiedKey, RSASigningKey};
|
||||||
|
use rustls::PrivateKey;
|
||||||
|
use rustls::{Certificate, NoClientAuth, ResolvesServerCert, ServerConfig};
|
||||||
|
use simple_logger::SimpleLogger;
|
||||||
|
use sodiumoxide::crypto::box_::PrecomputedKey;
|
||||||
|
use thiserror::Error;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
mod ping;
|
||||||
|
mod routes;
|
||||||
|
mod stop;
|
||||||
|
|
||||||
|
const CONTROL_CENTER_PING_URL: &str = "https://api.mangadex.network/ping";
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! client_api_version {
|
||||||
|
() => {
|
||||||
|
"30"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ServerState {
|
||||||
|
precomputed_key: PrecomputedKey,
|
||||||
|
image_server: Url,
|
||||||
|
tls_config: Tls,
|
||||||
|
disabled_tokens: bool,
|
||||||
|
url: String,
|
||||||
|
cache: LruCache<(String, String, bool), Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerState {
|
||||||
|
async fn init(config: &Config) -> Result<Self, ()> {
|
||||||
|
let resp = Client::new()
|
||||||
|
.post(CONTROL_CENTER_PING_URL)
|
||||||
|
.send_json(&PingRequest::from(config))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match resp {
|
||||||
|
Ok(mut resp) => match resp.json::<PingResponse>().await {
|
||||||
|
Ok(resp) => {
|
||||||
|
let key = resp
|
||||||
|
.token_key
|
||||||
|
.and_then(|key| match PrecomputedKey::from_slice(key.as_bytes()) {
|
||||||
|
Some(key) => Some(key),
|
||||||
|
None => {
|
||||||
|
error!("Failed to parse token key: got {}", key);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if resp.compromised {
|
||||||
|
warn!("Got compromised response from control center!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.paused {
|
||||||
|
debug!("Got paused response from control center.");
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("This client's URL has been set to {}", resp.url);
|
||||||
|
|
||||||
|
if resp.disabled_tokens {
|
||||||
|
info!("This client will not validated tokens");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
precomputed_key: key,
|
||||||
|
image_server: resp.image_server,
|
||||||
|
tls_config: resp.tls.unwrap(),
|
||||||
|
disabled_tokens: resp.disabled_tokens,
|
||||||
|
url: resp.url,
|
||||||
|
cache: LruCache::new(1000),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Got malformed response: {}", e);
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => match e {
|
||||||
|
SendRequestError::Timeout => {
|
||||||
|
error!("Response timed out to control server. Is MangaDex down?");
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
e => {
|
||||||
|
warn!("Failed to send request: {}", e);
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RwLockServerState(RwLock<ServerState>);
|
||||||
|
|
||||||
|
impl ResolvesServerCert for RwLockServerState {
|
||||||
|
fn resolve(&self, _: rustls::ClientHello) -> Option<CertifiedKey> {
|
||||||
|
let read_guard = self.0.read();
|
||||||
|
Some(CertifiedKey {
|
||||||
|
cert: vec![Certificate(read_guard.tls_config.certificate.clone())],
|
||||||
|
key: Arc::new(Box::new(
|
||||||
|
RSASigningKey::new(&PrivateKey(read_guard.tls_config.private_key.clone())).unwrap(),
|
||||||
|
)),
|
||||||
|
ocsp: None,
|
||||||
|
sct_list: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
enum ServerError {
|
||||||
|
#[error("There was a failure parsing config")]
|
||||||
|
Config(#[from] VarError),
|
||||||
|
#[error("Failed to parse an int")]
|
||||||
|
ParseInt(#[from] ParseIntError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> Result<(), std::io::Error> {
|
||||||
|
dotenv::dotenv().ok();
|
||||||
|
SimpleLogger::new().init().unwrap();
|
||||||
|
|
||||||
|
let config = Config::new().unwrap();
|
||||||
|
let port = config.port;
|
||||||
|
|
||||||
|
let server = ServerState::init(&config).await.unwrap();
|
||||||
|
let data_0 = Arc::new(RwLockServerState(RwLock::new(server)));
|
||||||
|
let data_1 = Arc::clone(&data_0);
|
||||||
|
let data_2 = Arc::clone(&data_0);
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
let mut interval = time::interval(Duration::from_secs(90));
|
||||||
|
let mut data = Arc::clone(&data_0);
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
ping::update_server_state(&config, &mut data).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut tls_config = ServerConfig::new(NoClientAuth::new());
|
||||||
|
tls_config.cert_resolver = data_2;
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.service(routes::token_data)
|
||||||
|
.app_data(Data::from(Arc::clone(&data_1)))
|
||||||
|
})
|
||||||
|
.shutdown_timeout(60)
|
||||||
|
.bind_rustls(format!("0.0.0.0:{}", port), tls_config)?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
secret: String,
|
||||||
|
port: u16,
|
||||||
|
disk_quota: usize,
|
||||||
|
network_speed: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
fn new() -> Result<Self, ServerError> {
|
||||||
|
let secret = env::var("CLIENT_SECRET")?;
|
||||||
|
let port = env::var("PORT")?.parse::<u16>()?;
|
||||||
|
let disk_quota = env::var("MAX_STORAGE_BYTES")?.parse::<usize>()?;
|
||||||
|
let network_speed = env::var("MAX_NETWORK_SPEED")?.parse::<usize>()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
secret,
|
||||||
|
port,
|
||||||
|
disk_quota,
|
||||||
|
network_speed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
113
src/ping.rs
Normal file
113
src/ping.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use awc::{error::SendRequestError, Client};
|
||||||
|
use log::{debug, error, info, warn};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sodiumoxide::crypto::box_::PrecomputedKey;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{client_api_version, Config, RwLockServerState, CONTROL_CENTER_PING_URL};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct PingRequest<'a> {
|
||||||
|
secret: &'a str,
|
||||||
|
port: u16,
|
||||||
|
disk_space: usize,
|
||||||
|
network_speed: usize,
|
||||||
|
build_version: usize,
|
||||||
|
tls_created_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PingRequest<'a> {
|
||||||
|
fn from_config_and_state(config: &'a Config, state: &'a Arc<RwLockServerState>) -> Self {
|
||||||
|
Self {
|
||||||
|
secret: &config.secret,
|
||||||
|
port: config.port,
|
||||||
|
disk_space: config.disk_quota,
|
||||||
|
network_speed: config.network_speed,
|
||||||
|
build_version: client_api_version!().parse().unwrap(),
|
||||||
|
tls_created_at: Some(state.0.read().tls_config.created_at.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Config> for PingRequest<'a> {
|
||||||
|
fn from(config: &'a Config) -> Self {
|
||||||
|
Self {
|
||||||
|
secret: &config.secret,
|
||||||
|
port: config.port,
|
||||||
|
disk_space: config.disk_quota,
|
||||||
|
network_speed: config.network_speed,
|
||||||
|
build_version: client_api_version!().parse().unwrap(),
|
||||||
|
tls_created_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub(crate) struct PingResponse {
|
||||||
|
pub(crate) image_server: Url,
|
||||||
|
pub(crate) latest_build: usize,
|
||||||
|
pub(crate) url: String,
|
||||||
|
pub(crate) token_key: Option<String>,
|
||||||
|
pub(crate) compromised: bool,
|
||||||
|
pub(crate) paused: bool,
|
||||||
|
pub(crate) disabled_tokens: bool,
|
||||||
|
pub(crate) tls: Option<Tls>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub(crate) struct Tls {
|
||||||
|
pub created_at: String,
|
||||||
|
pub private_key: Vec<u8>,
|
||||||
|
pub certificate: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn update_server_state(req: &Config, data: &mut Arc<RwLockServerState>) {
|
||||||
|
let req = PingRequest::from_config_and_state(req, data);
|
||||||
|
let resp = Client::new()
|
||||||
|
.post(CONTROL_CENTER_PING_URL)
|
||||||
|
.send_json(&req)
|
||||||
|
.await;
|
||||||
|
match resp {
|
||||||
|
Ok(mut resp) => match resp.json::<PingResponse>().await {
|
||||||
|
Ok(resp) => {
|
||||||
|
let mut write_guard = data.0.write();
|
||||||
|
|
||||||
|
write_guard.image_server = resp.image_server;
|
||||||
|
|
||||||
|
if let Some(key) = resp.token_key {
|
||||||
|
match PrecomputedKey::from_slice(key.as_bytes()) {
|
||||||
|
Some(key) => write_guard.precomputed_key = key,
|
||||||
|
None => error!("Failed to parse token key: got {}", key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_guard.disabled_tokens = resp.disabled_tokens;
|
||||||
|
|
||||||
|
if let Some(tls) = resp.tls {
|
||||||
|
write_guard.tls_config = tls;
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.compromised {
|
||||||
|
warn!("Got compromised response from control center!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.paused {
|
||||||
|
debug!("Got paused response from control center.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.url != write_guard.url {
|
||||||
|
info!("This client's URL has been updated to {}", resp.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Got malformed response: {}", e),
|
||||||
|
},
|
||||||
|
Err(e) => match e {
|
||||||
|
SendRequestError::Timeout => {
|
||||||
|
error!("Response timed out to control server. Is MangaDex down?")
|
||||||
|
}
|
||||||
|
e => warn!("Failed to send request: {}", e),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
208
src/routes.rs
Normal file
208
src/routes.rs
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
use actix_web::{dev::HttpResponseBuilder, get, web::Data, HttpRequest, Responder};
|
||||||
|
use actix_web::{web::Path, HttpResponse};
|
||||||
|
use awc::Client;
|
||||||
|
use base64::DecodeError;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use futures::stream;
|
||||||
|
use log::{error, warn};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sodiumoxide::crypto::box_::{open_precomputed, Nonce, PrecomputedKey, NONCEBYTES};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::{client_api_version, RwLockServerState};
|
||||||
|
|
||||||
|
const BASE64_CONFIG: base64::Config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
|
||||||
|
|
||||||
|
const SERVER_ID_STRING: &str = concat!(
|
||||||
|
env!("CARGO_CRATE_NAME"),
|
||||||
|
" ",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
" (",
|
||||||
|
client_api_version!(),
|
||||||
|
")",
|
||||||
|
);
|
||||||
|
|
||||||
|
enum ServerResponse {
|
||||||
|
TokenValidationError(TokenValidationError),
|
||||||
|
HttpResponse(HttpResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Responder for ServerResponse {
|
||||||
|
fn respond_to(self, req: &HttpRequest) -> HttpResponse {
|
||||||
|
match self {
|
||||||
|
ServerResponse::TokenValidationError(e) => e.respond_to(req),
|
||||||
|
ServerResponse::HttpResponse(resp) => resp.respond_to(req),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/{token}/data/{chapter_hash}/{file_name}")]
|
||||||
|
async fn token_data(
|
||||||
|
state: Data<RwLockServerState>,
|
||||||
|
path: Path<(String, String, String)>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let (token, chapter_hash, file_name) = path.into_inner();
|
||||||
|
if !state.0.read().disabled_tokens {
|
||||||
|
if let Err(e) = validate_token(&state.0.read().precomputed_key, token, &chapter_hash) {
|
||||||
|
return ServerResponse::TokenValidationError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_image(state, chapter_hash, file_name, false).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/{token}/data-saver/{chapter_hash}/{file_name}")]
|
||||||
|
async fn token_data_saver(
|
||||||
|
state: Data<RwLockServerState>,
|
||||||
|
path: Path<(String, String, String)>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let (token, chapter_hash, file_name) = path.into_inner();
|
||||||
|
if !state.0.read().disabled_tokens {
|
||||||
|
if let Err(e) = validate_token(&state.0.read().precomputed_key, token, &chapter_hash) {
|
||||||
|
return ServerResponse::TokenValidationError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetch_image(state, chapter_hash, file_name, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/data/{chapter_hash}/{file_name}")]
|
||||||
|
async fn no_token_data(
|
||||||
|
state: Data<RwLockServerState>,
|
||||||
|
path: Path<(String, String)>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let (chapter_hash, file_name) = path.into_inner();
|
||||||
|
fetch_image(state, chapter_hash, file_name, false).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/data-saver/{chapter_hash}/{file_name}")]
|
||||||
|
async fn no_token_data_saver(
|
||||||
|
state: Data<RwLockServerState>,
|
||||||
|
path: Path<(String, String)>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let (chapter_hash, file_name) = path.into_inner();
|
||||||
|
fetch_image(state, chapter_hash, file_name, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
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> {
|
||||||
|
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)?;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Token<'a> {
|
||||||
|
expires: DateTime<Utc>,
|
||||||
|
hash: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_headers(builder: &mut HttpResponseBuilder) -> &mut HttpResponseBuilder {
|
||||||
|
builder
|
||||||
|
.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"))
|
||||||
|
.insert_header(("Timing-Allow-Origin", "https://mangadex.org"))
|
||||||
|
.insert_header(("Server", SERVER_ID_STRING))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_image(
|
||||||
|
state: Data<RwLockServerState>,
|
||||||
|
chapter_hash: String,
|
||||||
|
file_name: String,
|
||||||
|
is_data_saver: bool,
|
||||||
|
) -> ServerResponse {
|
||||||
|
let key = (chapter_hash, file_name, is_data_saver);
|
||||||
|
|
||||||
|
if let Some(cached) = state.0.write().cache.get(&key) {
|
||||||
|
let data = cached.to_vec();
|
||||||
|
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 resp = if is_data_saver {
|
||||||
|
Client::new().get(format!(
|
||||||
|
"{}/data-saver/{}/{}",
|
||||||
|
state.image_server, &key.1, &key.2
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Client::new().get(format!("{}/data/{}/{}", state.image_server, &key.1, &key.2))
|
||||||
|
}
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match resp {
|
||||||
|
Ok(mut resp) => match resp.body().await {
|
||||||
|
Ok(bytes) => {
|
||||||
|
state.cache.put(key, bytes.to_vec());
|
||||||
|
let bytes: Vec<Result<Bytes, Infallible>> = bytes
|
||||||
|
.chunks(1024)
|
||||||
|
.map(|v| Ok(Bytes::from(v.to_vec())))
|
||||||
|
.collect();
|
||||||
|
return ServerResponse::HttpResponse(
|
||||||
|
HttpResponse::Ok().streaming(stream::iter(bytes)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Got payload error from image server: {}", e);
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to fetch image from server: {}", e);
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
src/stop.rs
Normal file
3
src/stop.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
struct StopRequest {
|
||||||
|
secret: String,
|
||||||
|
}
|
Loading…
Reference in a new issue