rust implementation
This commit is contained in:
54
src/crypto.rs
Normal file
54
src/crypto.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use std::fmt;
|
||||
|
||||
use aes::Aes128;
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use cbc::{
|
||||
cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit},
|
||||
Decryptor,
|
||||
};
|
||||
|
||||
type Aes128CbcDec = Decryptor<Aes128>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CryptoError {
|
||||
Base64(base64::DecodeError),
|
||||
Aes(cbc::cipher::InvalidLength),
|
||||
Unpad(cbc::cipher::block_padding::UnpadError),
|
||||
}
|
||||
|
||||
impl fmt::Display for CryptoError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CryptoError::Base64(e) => write!(f, "Base64 decode failed: {}", e),
|
||||
CryptoError::Aes(e) => write!(f, "AES decryption failed: {}", e),
|
||||
CryptoError::Unpad(e) => write!(f, "PKCS7 unpadding failed: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CryptoError {}
|
||||
|
||||
pub fn decrypt(ciphertext_b64: &str, key: &str, iv: &str) -> Result<String, CryptoError> {
|
||||
// 1. Base64 decode the ciphertext
|
||||
let ciphertext = STANDARD
|
||||
.decode(ciphertext_b64)
|
||||
.map_err(CryptoError::Base64)?;
|
||||
|
||||
// 2. Initialize AES-128 in CBC mode
|
||||
let key_bytes = key.as_bytes();
|
||||
let iv_bytes = iv.as_bytes();
|
||||
let decryptor = Aes128CbcDec::new_from_slices(key_bytes, iv_bytes).map_err(CryptoError::Aes)?;
|
||||
|
||||
// 3. Decrypt the ciphertext, handling padding
|
||||
let decrypted_len = ciphertext.len();
|
||||
let mut plaintext = vec![0u8; decrypted_len];
|
||||
let copy_len = ciphertext.len();
|
||||
plaintext[..copy_len].copy_from_slice(&ciphertext);
|
||||
|
||||
let plaintext_slice = decryptor
|
||||
.decrypt_padded_mut::<Pkcs7>(&mut plaintext[..decrypted_len])
|
||||
.map_err(CryptoError::Unpad)?;
|
||||
|
||||
// 4. Convert plaintext to a UTF-8 string
|
||||
Ok(String::from_utf8_lossy(plaintext_slice).to_string())
|
||||
}
|
||||
81
src/main.rs
Normal file
81
src/main.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{routing::any, Router};
|
||||
use dotenvy::dotenv;
|
||||
use tracing::{error, info, level_filters::LevelFilter};
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
|
||||
mod crypto;
|
||||
mod proxy;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub client: reqwest::Client,
|
||||
pub target_url: reqwest::Url,
|
||||
pub key: String,
|
||||
pub iv: String,
|
||||
}
|
||||
|
||||
const DEFAULT_TARGET_URL: &str = "https://cloud.linspirer.com:883";
|
||||
const DEFAULT_HOST: &str = "0.0.0.0";
|
||||
const DEFAULT_PORT: &str = "8080";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Load environment variables from .env file
|
||||
dotenv().ok();
|
||||
|
||||
// Set up logging
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer())
|
||||
.with(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy(),
|
||||
)
|
||||
.init();
|
||||
|
||||
// Load configuration from environment
|
||||
let key = std::env::var("LINSPIRER_KEY").context("LINSPIRER_KEY must be set")?;
|
||||
let iv = std::env::var("LINSPIRER_IV").context("LINSPIRER_IV must be set")?;
|
||||
let target_url_str = std::env::var("LINSPIRER_TARGET_URL");
|
||||
let target_url_str = target_url_str.as_deref().unwrap_or(DEFAULT_TARGET_URL);
|
||||
let target_url = reqwest::Url::parse(target_url_str)?;
|
||||
|
||||
let host_str = std::env::var("LINSPIRER_HOST");
|
||||
let host = host_str.as_deref().unwrap_or(DEFAULT_HOST);
|
||||
let port_str = std::env::var("LINSPIRER_PORT");
|
||||
let port = port_str.as_deref().unwrap_or(DEFAULT_PORT);
|
||||
let addr_str = format!("{}:{}", host, port);
|
||||
let addr: SocketAddr = addr_str
|
||||
.parse()
|
||||
.context(format!("Invalid address format: {}", addr_str))?;
|
||||
|
||||
// Create a reqwest client that ignores SSL certificate verification
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
|
||||
// Create shared state
|
||||
let state = Arc::new(AppState {
|
||||
client,
|
||||
target_url,
|
||||
key,
|
||||
iv,
|
||||
});
|
||||
|
||||
// Build our application with a single route
|
||||
let app = Router::new()
|
||||
.route("/{*path}", any(proxy::proxy_handler))
|
||||
.with_state(state);
|
||||
|
||||
// Run the server
|
||||
info!("Proxy started on {} => {}", addr_str, target_url_str);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
if let Err(e) = axum::serve(listener, app.into_make_service()).await {
|
||||
error!("Server error: {}", e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
150
src/proxy.rs
Normal file
150
src/proxy.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::{Path, State},
|
||||
http::{Request, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use http_body_util::BodyExt;
|
||||
use serde_json::Value;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{crypto, AppState};
|
||||
|
||||
pub async fn proxy_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(path): Path<String>,
|
||||
req: Request<axum::body::Body>,
|
||||
) -> impl IntoResponse {
|
||||
let (parts, body) = req.into_parts();
|
||||
let body_bytes = match body.collect().await {
|
||||
Ok(body) => body.to_bytes(),
|
||||
Err(e) => {
|
||||
error!("Failed to read request body: {}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to read request body".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let decrypted_request_log = match process_and_log_request(&body_bytes, &state.key, &state.iv) {
|
||||
Ok(log) => log,
|
||||
Err(e) => {
|
||||
warn!("Failed to process request for logging: {}", e);
|
||||
Value::String("Could not decrypt request".to_string())
|
||||
}
|
||||
};
|
||||
|
||||
let mut target_url = state.target_url.clone();
|
||||
target_url.set_path(&path);
|
||||
target_url.set_query(parts.uri.query());
|
||||
|
||||
let mut forwarded_req = state.client.request(parts.method, target_url);
|
||||
|
||||
if !parts.headers.is_empty() {
|
||||
forwarded_req = forwarded_req.headers(parts.headers);
|
||||
}
|
||||
forwarded_req = forwarded_req.body(body_bytes);
|
||||
|
||||
let resp = match forwarded_req.send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
error!("Failed to forward request: {}", e);
|
||||
return (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
format!("Failed to forward request: {}", e),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let (resp_parts, resp_body_bytes) = match clone_response(resp).await {
|
||||
Ok(tuple) => tuple,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to clone response".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let decrypted_response_log =
|
||||
match process_and_log_response(&resp_body_bytes, &state.key, &state.iv).await {
|
||||
Ok(log) => log,
|
||||
Err(e) => {
|
||||
warn!("Failed to process response for logging: {}", e);
|
||||
"Could not decrypt response".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let method = decrypted_request_log
|
||||
.get("method")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("UNKNOWN");
|
||||
info!(
|
||||
"[{}] {}\nRequest:\n{}\nResponse:\n{}\n{}",
|
||||
chrono::Local::now().format("%Y/%m/%d %H:%M:%S"),
|
||||
method,
|
||||
serde_json::to_string_pretty(&decrypted_request_log).unwrap_or_default(),
|
||||
decrypted_response_log,
|
||||
"-".repeat(80),
|
||||
);
|
||||
|
||||
let mut response_builder = Response::builder().status(resp_parts.status);
|
||||
if !resp_parts.headers.is_empty() {
|
||||
*response_builder.headers_mut().unwrap() = resp_parts.headers;
|
||||
}
|
||||
response_builder
|
||||
.body(axum::body::Body::from(resp_body_bytes))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn process_and_log_request(body_bytes: &Bytes, key: &str, iv: &str) -> anyhow::Result<Value> {
|
||||
let mut request_data: Value = serde_json::from_slice(body_bytes)?;
|
||||
|
||||
if let Some(params_value) = request_data.get_mut("params")
|
||||
&& let Some(params_str) = params_value.as_str() {
|
||||
let params_str_owned = params_str.to_string();
|
||||
match crypto::decrypt(¶ms_str_owned, key, iv) {
|
||||
Ok(decrypted_str) => {
|
||||
let decrypted_params: Value = serde_json::from_str(&decrypted_str)
|
||||
.unwrap_or(Value::String(decrypted_str));
|
||||
*params_value = decrypted_params;
|
||||
}
|
||||
Err(e) => {
|
||||
*params_value = Value::String(format!("decrypt failed: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(request_data)
|
||||
}
|
||||
|
||||
async fn process_and_log_response(
|
||||
body_bytes: &Bytes,
|
||||
key: &str,
|
||||
iv: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let decrypted = crypto::decrypt(std::str::from_utf8(body_bytes)?, key, iv)?;
|
||||
let formatted: Value = serde_json::from_str(&decrypted)?;
|
||||
Ok(serde_json::to_string_pretty(&formatted)?)
|
||||
}
|
||||
|
||||
async fn clone_response(
|
||||
resp: reqwest::Response,
|
||||
) -> anyhow::Result<(axum::http::response::Parts, Bytes)> {
|
||||
let mut parts_builder = axum::http::response::Builder::new()
|
||||
.status(resp.status())
|
||||
.version(resp.version());
|
||||
|
||||
if !resp.headers().is_empty() {
|
||||
*parts_builder.headers_mut().unwrap() = resp.headers().clone();
|
||||
}
|
||||
let parts = parts_builder.body(())?.into_parts().0;
|
||||
let body_bytes = resp.bytes().await?;
|
||||
|
||||
Ok((parts, body_bytes))
|
||||
}
|
||||
Reference in New Issue
Block a user