use parking_lot::RwLock;
use pyo3::prelude::*;
use pyo3::types::{PyCFunction, PyDict, PyList, PyTuple};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::runtime::Runtime;
use super::client::HypertorError;
use crate::onion_service::{OnionService, OnionServiceConfig};
#[allow(dead_code)]
struct PyRoute {
method: String,
path: String,
handler: Py<PyAny>,
response_model: Option<String>,
}
#[pyclass(name = "Request")]
#[derive(Clone)]
pub struct PyRequest {
#[pyo3(get)]
pub method: String,
#[pyo3(get)]
pub path: String,
#[pyo3(get)]
pub params: HashMap<String, String>,
#[pyo3(get)]
pub query: HashMap<String, String>,
#[pyo3(get)]
pub headers: HashMap<String, String>,
body: Vec<u8>,
}
#[pymethods]
impl PyRequest {
fn text(&self) -> PyResult<String> {
String::from_utf8(self.body.clone())
.map_err(|e| HypertorError::new_err(format!("Invalid UTF-8: {}", e)))
}
fn json(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let text = self.text()?;
let json_module = py.import("json")?;
json_module.call_method1("loads", (text,)).map(|v| v.into())
}
fn body(&self) -> &[u8] {
&self.body
}
fn header(&self, name: &str) -> Option<String> {
self.headers.get(&name.to_lowercase()).cloned()
}
fn query_param(&self, name: &str) -> Option<String> {
self.query.get(name).cloned()
}
fn path_param(&self, name: &str) -> Option<String> {
self.params.get(name).cloned()
}
fn __repr__(&self) -> String {
format!("<Request {} {}>", self.method, self.path)
}
}
#[pyclass(name = "AppResponse")]
#[derive(Clone)]
#[allow(dead_code)]
pub struct PyAppResponse {
#[pyo3(get, set)]
pub status: u16,
headers: HashMap<String, String>,
body: Vec<u8>,
content_type: String,
}
#[pymethods]
impl PyAppResponse {
#[new]
#[pyo3(signature = (body, status=200, content_type="text/plain"))]
fn new(body: &Bound<'_, PyAny>, status: u16, content_type: &str) -> PyResult<Self> {
let body_bytes = if let Ok(s) = body.extract::<String>() {
s.into_bytes()
} else if let Ok(b) = body.extract::<Vec<u8>>() {
b
} else {
let json_module = body.py().import("json")?;
let json_str: String = json_module.call_method1("dumps", (body,))?.extract()?;
return Ok(Self {
status,
headers: HashMap::new(),
body: json_str.into_bytes(),
content_type: "application/json".to_string(),
});
};
Ok(Self {
status,
headers: HashMap::new(),
body: body_bytes,
content_type: content_type.to_string(),
})
}
#[staticmethod]
#[pyo3(signature = (data, status=None))]
fn json(py: Python<'_>, data: &Bound<'_, PyAny>, status: Option<u16>) -> PyResult<Self> {
let json_module = py.import("json")?;
let json_str: String = json_module.call_method1("dumps", (data,))?.extract()?;
Ok(Self {
status: status.unwrap_or(200),
headers: HashMap::new(),
body: json_str.into_bytes(),
content_type: "application/json".to_string(),
})
}
#[staticmethod]
#[pyo3(signature = (content, status=None))]
fn html(content: String, status: Option<u16>) -> Self {
Self {
status: status.unwrap_or(200),
headers: HashMap::new(),
body: content.into_bytes(),
content_type: "text/html; charset=utf-8".to_string(),
}
}
#[staticmethod]
#[pyo3(signature = (location, status=None))]
fn redirect(location: &str, status: Option<u16>) -> Self {
let mut headers = HashMap::new();
headers.insert("Location".to_string(), location.to_string());
Self {
status: status.unwrap_or(302),
headers,
body: Vec::new(),
content_type: "text/plain".to_string(),
}
}
fn set_header(&mut self, name: &str, value: &str) {
self.headers.insert(name.to_string(), value.to_string());
}
fn get_headers(&self) -> HashMap<String, String> {
self.headers.clone()
}
fn __repr__(&self) -> String {
format!("<Response status={}>", self.status)
}
}
#[pyclass(name = "AppConfig")]
#[derive(Clone)]
pub struct PyAppConfig {
#[pyo3(get, set)]
pub port: u16,
#[pyo3(get, set)]
pub debug: bool,
#[pyo3(get, set)]
pub log_requests: bool,
#[pyo3(get, set)]
pub timeout: u64,
#[pyo3(get, set)]
pub max_body_size: usize,
#[pyo3(get, set)]
pub key_file: Option<String>,
#[pyo3(get, set)]
pub enable_pow: bool,
#[pyo3(get, set)]
pub security_level: String,
}
#[pymethods]
impl PyAppConfig {
#[new]
#[pyo3(signature = (port=80, debug=false, log_requests=true, timeout=30, max_body_size=10485760, key_file=None, enable_pow=false, security_level="standard"))]
#[allow(clippy::too_many_arguments)]
fn new(
port: u16,
debug: bool,
log_requests: bool,
timeout: u64,
max_body_size: usize,
key_file: Option<String>,
enable_pow: bool,
security_level: &str,
) -> Self {
Self {
port,
debug,
log_requests,
timeout,
max_body_size,
key_file,
enable_pow,
security_level: security_level.to_string(),
}
}
fn __repr__(&self) -> String {
format!(
"<AppConfig port={} security={}>",
self.port, self.security_level
)
}
}
#[pyclass(name = "OnionApp")]
pub struct PyOnionApp {
config: PyAppConfig,
routes: Arc<RwLock<Vec<PyRoute>>>,
middleware_count: Arc<RwLock<usize>>,
error_handler_count: Arc<RwLock<usize>>,
startup_hook_count: Arc<RwLock<usize>>,
shutdown_hook_count: Arc<RwLock<usize>>,
address: Arc<RwLock<Option<String>>>,
running: Arc<RwLock<bool>>,
}
#[pymethods]
impl PyOnionApp {
#[new]
#[pyo3(signature = (config=None, port=80, debug=false, key_file=None))]
fn new(config: Option<PyAppConfig>, port: u16, debug: bool, key_file: Option<String>) -> Self {
let config = config.unwrap_or_else(|| PyAppConfig {
port,
debug,
log_requests: true,
timeout: 30,
max_body_size: 10 * 1024 * 1024,
key_file,
enable_pow: false,
security_level: "standard".to_string(),
});
Self {
config,
routes: Arc::new(RwLock::new(Vec::new())),
middleware_count: Arc::new(RwLock::new(0)),
error_handler_count: Arc::new(RwLock::new(0)),
startup_hook_count: Arc::new(RwLock::new(0)),
shutdown_hook_count: Arc::new(RwLock::new(0)),
address: Arc::new(RwLock::new(None)),
running: Arc::new(RwLock::new(false)),
}
}
#[pyo3(signature = (path, response_model=None))]
fn get(
&self,
py: Python<'_>,
path: &str,
response_model: Option<String>,
) -> PyResult<Py<PyAny>> {
self.route_decorator(py, "GET", path, response_model)
}
#[pyo3(signature = (path, response_model=None))]
fn post(
&self,
py: Python<'_>,
path: &str,
response_model: Option<String>,
) -> PyResult<Py<PyAny>> {
self.route_decorator(py, "POST", path, response_model)
}
#[pyo3(signature = (path, response_model=None))]
fn put(
&self,
py: Python<'_>,
path: &str,
response_model: Option<String>,
) -> PyResult<Py<PyAny>> {
self.route_decorator(py, "PUT", path, response_model)
}
#[pyo3(signature = (path, response_model=None))]
fn delete(
&self,
py: Python<'_>,
path: &str,
response_model: Option<String>,
) -> PyResult<Py<PyAny>> {
self.route_decorator(py, "DELETE", path, response_model)
}
#[pyo3(signature = (path, response_model=None))]
fn patch(
&self,
py: Python<'_>,
path: &str,
response_model: Option<String>,
) -> PyResult<Py<PyAny>> {
self.route_decorator(py, "PATCH", path, response_model)
}
#[pyo3(signature = (path, methods=None, response_model=None))]
fn route(
&self,
py: Python<'_>,
path: &str,
methods: Option<Vec<String>>,
response_model: Option<String>,
) -> PyResult<Py<PyAny>> {
let methods = methods.unwrap_or_else(|| vec!["GET".to_string()]);
let routes = Arc::clone(&self.routes);
let path = path.to_string();
let decorator = PyCFunction::new_closure(
py,
None,
None,
move |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| {
let func = args.get_item(0)?;
let handler: Py<PyAny> = func.into();
for method in &methods {
routes.write().push(PyRoute {
method: method.clone(),
path: path.clone(),
handler: handler.clone_ref(args.py()),
response_model: response_model.clone(),
});
}
Ok::<_, PyErr>(handler)
},
)?;
Ok(decorator.into())
}
fn middleware(&self, _py: Python<'_>, func: Py<PyAny>) -> PyResult<Py<PyAny>> {
*self.middleware_count.write() += 1;
Ok(func)
}
fn error_handler(&self, py: Python<'_>, status_code: u16) -> PyResult<Py<PyAny>> {
let error_handler_count = Arc::clone(&self.error_handler_count);
let _ = status_code;
let decorator = PyCFunction::new_closure(
py,
None,
None,
move |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| {
let func = args.get_item(0)?;
let handler: Py<PyAny> = func.into();
*error_handler_count.write() += 1;
Ok::<_, PyErr>(handler)
},
)?;
Ok(decorator.into())
}
fn on_startup(&self, _py: Python<'_>, func: Py<PyAny>) -> PyResult<Py<PyAny>> {
*self.startup_hook_count.write() += 1;
Ok(func)
}
fn on_shutdown(&self, _py: Python<'_>, func: Py<PyAny>) -> PyResult<Py<PyAny>> {
*self.shutdown_hook_count.write() += 1;
Ok(func)
}
fn routes_info(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let routes = self.routes.read();
let list = PyList::empty(py);
for route in routes.iter() {
let dict = PyDict::new(py);
dict.set_item("method", &route.method)?;
dict.set_item("path", &route.path)?;
list.append(dict)?;
}
Ok(list.into())
}
fn address(&self) -> Option<String> {
self.address.read().clone()
}
fn is_running(&self) -> bool {
*self.running.read()
}
fn stop(&self) {
*self.running.write() = false;
}
#[pyo3(signature = (host=None, port=None, reload=false))]
fn run(
&self,
py: Python<'_>,
host: Option<&str>,
port: Option<u16>,
reload: bool,
) -> PyResult<()> {
let _ = (host, port, reload);
println!("╔════════════════════════════════════════════════════════════════════╗");
println!("║ 🧅 hypertor OnionApp ║");
println!("╠════════════════════════════════════════════════════════════════════╣");
println!("║ Starting Tor connection... ║");
*self.running.write() = true;
let service_config = {
let mut config = OnionServiceConfig::new("hypertor-app").port(self.config.port);
if self.config.enable_pow {
config = config.with_pow();
}
if let Some(ref key_path) = self.config.key_file {
config = config.key_dir(std::path::PathBuf::from(key_path));
}
match self.config.security_level.as_str() {
"enhanced" => {
config = config.vanguards_lite();
}
"maximum" | "paranoid" => {
config = config
.vanguards_full()
.with_pow()
.max_streams_per_circuit(100)
.rate_limit_at_intro(10.0, 20)
.num_intro_points(5);
}
_ => {} }
config
};
let rt = Runtime::new()
.map_err(|e| HypertorError::new_err(format!("Failed to create runtime: {}", e)))?;
let address = rt
.block_on(async {
let mut service = OnionService::new(service_config);
service.start().await
})
.map_err(|e| HypertorError::new_err(format!("Failed to start onion service: {}", e)))?;
*self.address.write() = Some(address.clone());
println!(
"║ Port: {:5} ║",
self.config.port
);
println!(
"║ Routes: {:3} ║",
self.routes.read().len()
);
println!(
"║ Security: {:10} ║",
self.config.security_level
);
println!("╠════════════════════════════════════════════════════════════════════╣");
println!(
"║ 🧅 Service live at: {} ║",
&address[..52.min(address.len())]
);
println!("╚════════════════════════════════════════════════════════════════════╝");
println!();
println!("Press Ctrl+C to stop");
let mut interrupted = false;
while *self.running.read() {
if py.check_signals().is_err() {
*self.running.write() = false;
interrupted = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
println!("\n🧅 Shutting down onion service...");
*self.running.write() = false;
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {
}));
rt.shutdown_timeout(std::time::Duration::from_secs(1));
std::panic::set_hook(prev_hook);
println!("🧅 Onion service stopped.");
if interrupted {
return Err(pyo3::exceptions::PyKeyboardInterrupt::new_err(""));
}
Ok(())
}
fn __repr__(&self) -> String {
let routes = self.routes.read().len();
let addr = self
.address
.read()
.clone()
.unwrap_or_else(|| "not started".to_string());
format!("<OnionApp routes={} address={}>", routes, addr)
}
}
impl PyOnionApp {
fn route_decorator(
&self,
py: Python<'_>,
method: &str,
path: &str,
response_model: Option<String>,
) -> PyResult<Py<PyAny>> {
let routes = Arc::clone(&self.routes);
let method = method.to_string();
let path = path.to_string();
let decorator = PyCFunction::new_closure(
py,
None,
None,
move |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| {
let func = args.get_item(0)?;
let handler: Py<PyAny> = func.into();
routes.write().push(PyRoute {
method: method.clone(),
path: path.clone(),
handler: handler.clone_ref(args.py()),
response_model: response_model.clone(),
});
Ok::<_, PyErr>(handler)
},
)?;
Ok(decorator.into())
}
}
#[allow(dead_code)]
pub fn py_to_response(py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult<PyAppResponse> {
if let Ok(response) = value.extract::<PyAppResponse>() {
return Ok(response);
}
if let Ok(s) = value.extract::<String>() {
return Ok(PyAppResponse {
status: 200,
headers: HashMap::new(),
body: s.into_bytes(),
content_type: "text/plain; charset=utf-8".to_string(),
});
}
if let Ok(b) = value.extract::<Vec<u8>>() {
return Ok(PyAppResponse {
status: 200,
headers: HashMap::new(),
body: b,
content_type: "application/octet-stream".to_string(),
});
}
if value.is_instance_of::<PyDict>() || value.is_instance_of::<PyList>() {
return PyAppResponse::json(py, value, None);
}
if value.is_none() {
return Ok(PyAppResponse {
status: 204,
headers: HashMap::new(),
body: Vec::new(),
content_type: "text/plain".to_string(),
});
}
let s: String = value.str()?.extract()?;
Ok(PyAppResponse {
status: 200,
headers: HashMap::new(),
body: s.into_bytes(),
content_type: "text/plain; charset=utf-8".to_string(),
})
}