use super::*;
use actix_web::{body::SizedStream, web::Bytes, HttpResponseBuilder};
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use std::time::Duration;
use tokio::time::sleep;
#[derive(Deserialize)]
pub struct DripQuery {
duration: Option<f64>,
numbytes: Option<usize>,
code: Option<u16>,
delay: Option<f64>,
chunked: Option<bool>,
}
#[derive(Deserialize)]
pub struct BytesQuery {
seed: Option<u64>,
}
#[derive(Deserialize)]
pub struct StreamBytesQuery {
seed: Option<u64>,
chunk_size: Option<usize>,
}
#[derive(Deserialize)]
pub struct RangeQuery {
chunk_size: Option<usize>,
duration: Option<f64>,
}
fn parse_request_range(range_header_value: &str) -> (Option<usize>, Option<usize>) {
if !range_header_value.starts_with("bytes=") {
return (None, None);
}
let range_part = &range_header_value[6..];
let first_range = range_part.split(',').next().unwrap_or(range_part);
if let Some((start_str, end_str)) = first_range.split_once('-') {
let first_byte_pos = if start_str.is_empty() {
None
} else {
start_str.parse().ok()
};
let last_byte_pos = if end_str.is_empty() {
None
} else {
end_str.parse().ok()
};
return (first_byte_pos, last_byte_pos);
}
(None, None)
}
fn get_request_range(req: &HttpRequest, upper_bound: usize) -> (usize, usize) {
let (first_byte_pos, last_byte_pos) = if let Some(range_header) = req.headers().get("Range") {
if let Ok(range_str) = range_header.to_str() {
parse_request_range(range_str)
} else {
(None, None)
}
} else {
(None, None)
};
let (first_byte_pos, last_byte_pos) = match (first_byte_pos, last_byte_pos) {
(None, None) => {
(0, upper_bound - 1)
}
(None, Some(suffix_length)) => {
let first_pos = upper_bound.saturating_sub(suffix_length);
(first_pos, upper_bound - 1)
}
(Some(start), None) => {
(start, upper_bound - 1)
}
(Some(start), Some(end)) => {
(start, end)
}
};
(first_byte_pos, last_byte_pos)
}
pub async fn uuid_handler(_req: HttpRequest) -> Result<HttpResponse> {
let uuid = Uuid::new_v4();
Ok(HttpResponse::Ok().json(json!({
"uuid": uuid.to_string()
})))
}
pub async fn base64_handler(_req: HttpRequest, path: web::Path<String>) -> Result<HttpResponse> {
let encoded_value = path.into_inner();
match general_purpose::STANDARD.decode(&encoded_value) {
Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) {
Ok(decoded_string) => Ok(HttpResponse::Ok()
.content_type("text/plain; charset=utf-8")
.body(decoded_string)),
Err(e) => Ok(HttpResponse::Ok()
.content_type("application/octet-stream")
.body(e.into_bytes())),
},
Err(_) => Ok(HttpResponse::BadRequest().json(json!({
"error": "Invalid base64 data"
}))),
}
}
pub async fn base64_post_handler(body: web::Bytes) -> Result<HttpResponse> {
match general_purpose::STANDARD.decode(&body) {
Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) {
Ok(decoded_string) => Ok(HttpResponse::Ok()
.content_type("text/plain; charset=utf-8")
.body(decoded_string)),
Err(e) => Ok(HttpResponse::Ok()
.content_type("application/octet-stream")
.body(e.into_bytes())),
},
Err(_) => Ok(HttpResponse::BadRequest().json(json!({
"error": "Invalid base64 data"
}))),
}
}
pub async fn bytes_handler(
_req: HttpRequest,
path: web::Path<usize>,
query: web::Query<BytesQuery>,
config: web::Data<AppConfig>,
) -> Result<HttpResponse> {
let n = path.into_inner();
if n > config.max_bytes {
return Ok(HttpResponse::NotFound().body(format!(
"number of bytes must be in the range (0, {}]",
config.max_bytes
)));
}
let random_bytes: Vec<u8> = if let Some(seed) = query.seed {
let mut rng = StdRng::seed_from_u64(seed);
(0..n).map(|_| rng.random_range(0..=255)).collect()
} else {
let mut rng = rand::rng();
(0..n).map(|_| rng.random_range(0..=255)).collect()
};
Ok(HttpResponse::Ok()
.content_type("application/octet-stream")
.body(random_bytes))
}
pub async fn stream_bytes_handler(
_req: HttpRequest,
path: web::Path<usize>,
query: web::Query<StreamBytesQuery>,
config: web::Data<AppConfig>,
) -> Result<HttpResponse> {
let n = path.into_inner();
if n > config.max_bytes {
return Ok(HttpResponse::NotFound().body(format!(
"number of bytes must be in the range (0, {}]",
config.max_bytes
)));
}
let chunk_size = query.chunk_size.unwrap_or(10 * 1024).max(1);
let seed = query.seed;
let stream = async_stream::stream! {
let mut chunks = Vec::new();
if let Some(seed) = seed {
let mut rng = StdRng::seed_from_u64(seed);
for _ in 0..n {
chunks.push(rng.random_range(0..=255));
if chunks.len() == chunk_size {
yield Ok::<_, actix_web::Error>(Bytes::from(chunks.clone()));
chunks.clear();
}
}
} else {
let mut rng = rand::rng();
for _ in 0..n {
chunks.push(rng.random_range(0..=255));
if chunks.len() == chunk_size {
yield Ok::<_, actix_web::Error>(Bytes::from(chunks.clone()));
chunks.clear();
}
}
}
if !chunks.is_empty() {
yield Ok::<_, actix_web::Error>(Bytes::from(chunks));
}
};
Ok(HttpResponse::Ok()
.content_type("application/octet-stream")
.streaming(stream))
}
pub async fn stream_handler(
req: HttpRequest,
path: web::Path<usize>,
config: web::Data<AppConfig>,
) -> Result<HttpResponse> {
let n = path.into_inner();
let n = n.min(100);
let base_response = extract_get_request_info(&req, &config.exclude_headers);
let stream = futures_util::stream::iter((0..n).map(move |i| {
let response = json!({
"url": base_response.url,
"args": base_response.args,
"headers": base_response.headers,
"origin": base_response.origin,
"id": i
});
Ok::<_, actix_web::Error>(Bytes::from(format!("{}\n", response)))
}));
Ok(HttpResponse::Ok()
.content_type("application/json")
.streaming(stream))
}
pub async fn range_handler(
req: HttpRequest,
path: web::Path<usize>,
query: web::Query<RangeQuery>,
) -> Result<HttpResponse> {
let numbytes = path.into_inner();
if numbytes == 0 || numbytes > (100 * 1024) {
return Ok(HttpResponse::NotFound()
.append_header(("ETag", format!("range{}", numbytes)))
.append_header(("Accept-Ranges", "bytes"))
.body("number of bytes must be in the range (0, 102400]"));
}
let chunk_size = query.chunk_size.unwrap_or(10 * 1024).max(1);
let duration = query.duration.unwrap_or(0.0);
let (first_byte_pos, last_byte_pos) = get_request_range(&req, numbytes);
if first_byte_pos > last_byte_pos || first_byte_pos >= numbytes || last_byte_pos >= numbytes {
return Ok(HttpResponse::RangeNotSatisfiable()
.append_header(("ETag", format!("range{}", numbytes)))
.append_header(("Accept-Ranges", "bytes"))
.append_header(("Content-Range", format!("bytes */{}", numbytes)))
.append_header(("Content-Length", "0"))
.finish());
}
let range_length = (last_byte_pos + 1) - first_byte_pos;
let pause_per_byte = if range_length > 0 {
Duration::from_secs_f64(duration / range_length as f64)
} else {
Duration::ZERO
};
let stream = async_stream::stream! {
let mut chunks = Vec::new();
for i in first_byte_pos..=last_byte_pos {
chunks.push(b'a' + (i % 26) as u8);
if chunks.len() == chunk_size || i == last_byte_pos {
yield Ok::<_, actix_web::Error>(Bytes::from(chunks.clone()));
if pause_per_byte.as_nanos() > 0 && !chunks.is_empty() {
tokio::time::sleep(pause_per_byte * chunks.len() as u32).await;
}
chunks.clear();
}
}
};
let content_range = format!("bytes {}-{}/{}", first_byte_pos, last_byte_pos, numbytes);
let is_full_content = first_byte_pos == 0 && last_byte_pos == (numbytes - 1);
let status = if is_full_content {
StatusCode::OK
} else {
StatusCode::PARTIAL_CONTENT
};
Ok(HttpResponseBuilder::new(status)
.content_type("application/octet-stream")
.append_header(("ETag", format!("range{}", numbytes)))
.append_header(("Accept-Ranges", "bytes"))
.append_header(("Content-Length", range_length.to_string()))
.append_header(("Content-Range", content_range))
.body(SizedStream::new(range_length as u64, Box::pin(stream))))
}
pub async fn links_handler(
_req: HttpRequest,
path: web::Path<(usize, usize)>,
) -> Result<HttpResponse> {
let (n, offset) = path.into_inner();
let n = n.clamp(1, 200);
let mut html = String::from("<!DOCTYPE html><html><head><title>Links</title></head><body>");
for i in 0..n {
if i == offset {
html.push_str(&format!("{}<br>", i));
} else {
html.push_str(&format!("<a href='/links/{}/{}'>{}</a><br>", n, i, i));
}
}
html.push_str("</body></html>");
Ok(HttpResponse::Ok().content_type("text/html").body(html))
}
pub async fn links_redirect_handler(
_req: HttpRequest,
path: web::Path<usize>,
) -> Result<HttpResponse> {
let n = path.into_inner();
Ok(HttpResponse::Found()
.append_header(("Location", format!("/links/{}/0", n)))
.body(""))
}
pub async fn drip_handler(_req: HttpRequest, query: web::Query<DripQuery>) -> Result<HttpResponse> {
let duration = query.duration.unwrap_or(2.0);
let numbytes = query.numbytes.unwrap_or(10);
let code = query.code.unwrap_or(200);
let delay = query.delay.unwrap_or(0.0);
if numbytes == 0 {
return Ok(HttpResponse::BadRequest().json(json!({
"error": "number of bytes must be positive"
})));
}
let numbytes = numbytes.min(10 * 1024 * 1024);
let status = StatusCode::from_u16(code).unwrap_or(StatusCode::OK);
if delay > 0.0 {
sleep(Duration::from_secs_f64(delay)).await;
}
let pause = if numbytes == 1 {
Duration::from_secs_f64(duration)
} else {
Duration::from_secs_f64(duration / (numbytes as f64 - 1.0))
};
let stream = async_stream::stream! {
for i in 0..numbytes {
yield Ok::<_, actix_web::Error>(Bytes::from("*"));
if i < numbytes - 1 && pause.as_nanos() > 0 {
sleep(pause).await;
}
}
};
let mut builder = HttpResponseBuilder::new(status);
builder.content_type("application/octet-stream");
let response = if query.chunked.unwrap_or(false) {
builder.streaming(stream)
} else {
builder.body(SizedStream::new(numbytes as u64, Box::pin(stream)))
};
Ok(response)
}
pub async fn delay_handler_get(
req: HttpRequest,
path: web::Path<f64>,
config: web::Data<AppConfig>,
) -> Result<HttpResponse> {
let delay_seconds = path.into_inner().min(10.0);
sleep(Duration::from_secs_f64(delay_seconds)).await;
let mut request_info = extract_request_info(&req, None, &config.exclude_headers);
fix_request_info_url(&req, &mut request_info);
Ok(HttpResponse::Ok().json(request_info))
}
pub async fn delay_handler(
req: HttpRequest,
path: web::Path<f64>,
body: String,
config: web::Data<AppConfig>,
) -> Result<HttpResponse> {
let delay_seconds = path.into_inner().min(10.0);
sleep(Duration::from_secs_f64(delay_seconds)).await;
let mut request_info = extract_request_info(&req, Some(&body), &config.exclude_headers);
fix_request_info_url(&req, &mut request_info);
Ok(HttpResponse::Ok().json(request_info))
}