forked from neri/datatrash
add rate limiting for download
This commit is contained in:
parent
96eadb1723
commit
4496335f50
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
|
@ -7,26 +7,28 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = { version = "4.1.0", default-features = false, features = [
|
actix-web = { version = "4.2.1", default-features = false, features = [
|
||||||
"macros",
|
"macros",
|
||||||
"compress-gzip",
|
"compress-gzip",
|
||||||
"compress-zstd",
|
"compress-zstd",
|
||||||
] }
|
] }
|
||||||
sqlx = { version = "0.6.0", default-features = false, features = [
|
sqlx = { version = "0.6.2", default-features = false, features = [
|
||||||
"runtime-tokio-rustls",
|
"runtime-tokio-rustls",
|
||||||
"postgres",
|
"postgres",
|
||||||
"time",
|
"time",
|
||||||
] }
|
] }
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.9.1"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
actix-files = "0.6.1"
|
actix-files = "0.6.2"
|
||||||
tokio = { version = "1.19.2", features = ["rt", "macros", "sync"] }
|
tokio = { version = "1.21.2", features = ["rt", "macros", "sync"] }
|
||||||
actix-multipart = "0.4.0"
|
actix-multipart = "0.4.0"
|
||||||
futures-util = "0.3.21"
|
futures-util = "0.3.24"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
time = "0.3.11"
|
time = "0.3.14"
|
||||||
htmlescape = "0.3.1"
|
htmlescape = "0.3.1"
|
||||||
urlencoding = "2.1.0"
|
urlencoding = "2.1.2"
|
||||||
tree_magic_mini = { version = "3.0.3", features = ["with-gpl-data"] }
|
tree_magic_mini = { version = "3.0.3", features = ["with-gpl-data"] }
|
||||||
mime = "0.3.16"
|
mime = "0.3.16"
|
||||||
url = "2.2.2"
|
url = "2.3.1"
|
||||||
|
actix-governor = "0.3.2"
|
||||||
|
governor = "0.4.2"
|
||||||
|
|
16
README.md
16
README.md
|
@ -18,12 +18,16 @@ To run the software directly, use the compiling instructions below.
|
||||||
|
|
||||||
### General configuration
|
### General configuration
|
||||||
|
|
||||||
| environment variable | default value |
|
| environment variable | default value | description |
|
||||||
| -------------------- | -------------- |
|
| --------------------- | -------------- | ---------------------------------------------- |
|
||||||
| STATIC_DIR | ./static |
|
| STATIC_DIR | ./static | directory to generate "static" files into |
|
||||||
| FILES_DIR | ./files |
|
| FILES_DIR | ./files | directory to save uploaded files into |
|
||||||
| UPLOAD_MAX_BYTES | 8388608 (8MiB) |
|
| UPLOAD_MAX_BYTES | 8388608 (8MiB) | maximum size for uploaded files |
|
||||||
| BIND_ADDRESS | 0.0.0.0:8000 |
|
| BIND_ADDRESS | 0.0.0.0:8000 | address to bind the server to |
|
||||||
|
| RATE_LIMIT | true | whether download rate should be limited |
|
||||||
|
| RATE_LIMIT_PROXIED | false | whether rate limit should read x-forwarded-for |
|
||||||
|
| RATE_LIMIT_PER_SECOND | 60 | seconds to wait between requests |
|
||||||
|
| RATE_LIMIT_BURST | 1440 | allowed request burst |
|
||||||
|
|
||||||
### Database configuration
|
### Database configuration
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,10 @@ pub struct Config {
|
||||||
pub files_dir: PathBuf,
|
pub files_dir: PathBuf,
|
||||||
pub max_file_size: Option<u64>,
|
pub max_file_size: Option<u64>,
|
||||||
pub no_auth_limits: Option<NoAuthLimits>,
|
pub no_auth_limits: Option<NoAuthLimits>,
|
||||||
|
pub enable_rate_limit: bool,
|
||||||
|
pub proxied: bool,
|
||||||
|
pub rate_limit_per_second: u64,
|
||||||
|
pub rate_limit_burst: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -36,11 +40,26 @@ pub async fn get_config() -> Config {
|
||||||
|
|
||||||
let no_auth_limits = get_no_auth_limits();
|
let no_auth_limits = get_no_auth_limits();
|
||||||
|
|
||||||
|
let enable_rate_limit = matches!(env::var("RATE_LIMIT").as_deref(), Ok("true") | Err(_));
|
||||||
|
let proxied = env::var("PROXIED").as_deref() == Ok("true");
|
||||||
|
let rate_limit_per_second = env::var("RATE_LIMIT_PER_SECOND")
|
||||||
|
.ok()
|
||||||
|
.and_then(|rate_limit| rate_limit.parse().ok())
|
||||||
|
.unwrap_or(60);
|
||||||
|
let rate_limit_burst = env::var("RATE_LIMIT_BURST")
|
||||||
|
.ok()
|
||||||
|
.and_then(|rate_limit| rate_limit.parse().ok())
|
||||||
|
.unwrap_or(1440);
|
||||||
|
|
||||||
Config {
|
Config {
|
||||||
static_dir,
|
static_dir,
|
||||||
files_dir,
|
files_dir,
|
||||||
max_file_size,
|
max_file_size,
|
||||||
no_auth_limits,
|
no_auth_limits,
|
||||||
|
enable_rate_limit,
|
||||||
|
proxied,
|
||||||
|
rate_limit_per_second,
|
||||||
|
rate_limit_burst,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
31
src/main.rs
31
src/main.rs
|
@ -3,10 +3,13 @@ mod db;
|
||||||
mod deleter;
|
mod deleter;
|
||||||
mod download;
|
mod download;
|
||||||
mod multipart;
|
mod multipart;
|
||||||
|
mod rate_limit;
|
||||||
mod template;
|
mod template;
|
||||||
mod upload;
|
mod upload;
|
||||||
|
|
||||||
|
use crate::rate_limit::ForwardedPeerIpKeyExtractor;
|
||||||
use actix_files::Files;
|
use actix_files::Files;
|
||||||
|
use actix_governor::{Governor, GovernorConfigBuilder};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
http::header::{HeaderName, CONTENT_SECURITY_POLICY},
|
http::header::{HeaderName, CONTENT_SECURITY_POLICY},
|
||||||
middleware::{self, DefaultHeaders, Logger},
|
middleware::{self, DefaultHeaders, Logger},
|
||||||
|
@ -52,9 +55,19 @@ async fn main() -> std::io::Result<()> {
|
||||||
template::write_prefillable_templates(&config).await;
|
template::write_prefillable_templates(&config).await;
|
||||||
let config = Data::new(config);
|
let config = Data::new(config);
|
||||||
|
|
||||||
|
let governor_conf = GovernorConfigBuilder::default()
|
||||||
|
.per_second(config.rate_limit_per_second)
|
||||||
|
.burst_size(config.rate_limit_burst)
|
||||||
|
.key_extractor(ForwardedPeerIpKeyExtractor {
|
||||||
|
proxied: config.proxied,
|
||||||
|
})
|
||||||
|
.use_headers()
|
||||||
|
.finish()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
HttpServer::new({
|
HttpServer::new({
|
||||||
move || {
|
move || {
|
||||||
App::new()
|
let app = App::new()
|
||||||
.wrap(Logger::new(r#"%{r}a "%r" =%s %bbytes %Tsec"#))
|
.wrap(Logger::new(r#"%{r}a "%r" =%s %bbytes %Tsec"#))
|
||||||
.wrap(DefaultHeaders::new().add(DEFAULT_CSP))
|
.wrap(DefaultHeaders::new().add(DEFAULT_CSP))
|
||||||
.wrap(middleware::Compress::default())
|
.wrap(middleware::Compress::default())
|
||||||
|
@ -68,7 +81,19 @@ async fn main() -> std::io::Result<()> {
|
||||||
.route(web::get().to(upload::uploaded)),
|
.route(web::get().to(upload::uploaded)),
|
||||||
)
|
)
|
||||||
.service(Files::new("/static", "static").disable_content_disposition())
|
.service(Files::new("/static", "static").disable_content_disposition())
|
||||||
.service(
|
.default_service(web::route().to(not_found));
|
||||||
|
if config.enable_rate_limit {
|
||||||
|
app.service(
|
||||||
|
web::resource([
|
||||||
|
"/{id:[a-z0-9]{5}}",
|
||||||
|
"/{id:[a-z0-9]{5}}/",
|
||||||
|
"/{id:[a-z0-9]{5}}/{name}",
|
||||||
|
])
|
||||||
|
.wrap(Governor::new(&governor_conf))
|
||||||
|
.route(web::get().to(download::download)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
app.service(
|
||||||
web::resource([
|
web::resource([
|
||||||
"/{id:[a-z0-9]{5}}",
|
"/{id:[a-z0-9]{5}}",
|
||||||
"/{id:[a-z0-9]{5}}/",
|
"/{id:[a-z0-9]{5}}/",
|
||||||
|
@ -76,7 +101,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
])
|
])
|
||||||
.route(web::get().to(download::download)),
|
.route(web::get().to(download::download)),
|
||||||
)
|
)
|
||||||
.default_service(web::route().to(not_found))
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.bind(bind_address)?
|
.bind(bind_address)?
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
use actix_governor::KeyExtractor;
|
||||||
|
use actix_governor::PeerIpKeyExtractor;
|
||||||
|
use actix_web::{dev::ServiceRequest, http::header::ContentType};
|
||||||
|
use governor::clock::{Clock, DefaultClock, QuantaInstant};
|
||||||
|
use governor::NotUntil;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ForwardedPeerIpKeyExtractor {
|
||||||
|
pub proxied: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyExtractor for ForwardedPeerIpKeyExtractor {
|
||||||
|
type Key = IpAddr;
|
||||||
|
type KeyExtractionError = &'static str;
|
||||||
|
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Forwarded peer IP"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract(&self, req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError> {
|
||||||
|
let forwarded_for = req.headers().get("x-forwarded-for");
|
||||||
|
if !self.proxied && forwarded_for.is_some() {
|
||||||
|
let forwarded_for = forwarded_for
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| "x-forwarded-for contains invalid header value")?;
|
||||||
|
forwarded_for
|
||||||
|
.parse::<IpAddr>()
|
||||||
|
.map_err(|_| "x-forwarded-for contains invalid ip adress")
|
||||||
|
} else {
|
||||||
|
PeerIpKeyExtractor.extract(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_error_content(&self, negative: &NotUntil<QuantaInstant>) -> (String, ContentType) {
|
||||||
|
let wait_time = negative
|
||||||
|
.wait_time_from(DefaultClock::default().now())
|
||||||
|
.as_secs();
|
||||||
|
(
|
||||||
|
format!("too many requests, retry in {}s", wait_time),
|
||||||
|
ContentType::plaintext(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
fn key_name(&self, key: &Self::Key) -> Option<String> {
|
||||||
|
Some(key.to_string())
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,8 @@ const UPLOAD_HTML: &str = include_str!("../template/upload.html");
|
||||||
const UPLOAD_SHORT_HTML: &str = include_str!("../template/upload-short.html");
|
const UPLOAD_SHORT_HTML: &str = include_str!("../template/upload-short.html");
|
||||||
|
|
||||||
const ID_CHARS: &[char] = &[
|
const ID_CHARS: &[char] = &[
|
||||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u',
|
||||||
'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||||
];
|
];
|
||||||
|
|
||||||
pub async fn index(config: web::Data<Config>) -> Result<NamedFile, Error> {
|
pub async fn index(config: web::Data<Config>) -> Result<NamedFile, Error> {
|
||||||
|
|
Loading…
Reference in New Issue