added Rust code

This commit is contained in:
transatoshi
2025-06-10 14:05:02 -07:00
parent 4a53d7fb0b
commit b36758a20f
2 changed files with 216 additions and 0 deletions

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "grin-faucet"
version = "1.0.0"
edition = "2024"
[dependencies]
warp = { version = "0.3", features = ["tls"] }
tokio = { version = "1", features = ["full"] }
tokio-rustls = "0.22"
serde = { version = "1.0", features = ["derive"] }
chrono = "0.4"
simplelog = "0.11"
log = "0.4"
sha2 = "0.10"
hex = "0.4.3"

201
src/main.rs Normal file
View File

@ -0,0 +1,201 @@
use chrono::{Duration, Local};
use log::{LevelFilter, error, info};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use simplelog::{ColorChoice, CombinedLogger, Config, TermLogger, TerminalMode, WriteLogger};
use std::collections::HashMap;
use std::fs::File;
use std::process::Command;
use std::sync::{Arc, Mutex};
use warp::Filter;
#[derive(Deserialize)]
struct SendRequest {
address: String,
}
#[derive(Serialize)]
struct Response {
message: String,
}
struct RateLimiter {
last_sent_ip: HashMap<String, chrono::DateTime<Local>>,
last_sent_address: HashMap<String, chrono::DateTime<Local>>,
}
fn is_valid_address(address: &str) -> bool {
if address.is_empty()
|| address.contains(' ')
|| !address.starts_with("grin1")
|| !address.chars().all(|c| c.is_alphanumeric())
|| address.len() < 62
{
return false;
}
true
}
// Function to hash the IP address
fn hash_ip(ip: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(ip);
let result = hasher.finalize();
hex::encode(result) // Convert the hash to a hexadecimal string
}
#[tokio::main]
async fn main() {
// Initialize logging
let log_file = File::create("faucet.log").unwrap();
CombinedLogger::init(vec![
TermLogger::new(
LevelFilter::Info,
Config::default(),
TerminalMode::Mixed,
ColorChoice::Always,
),
WriteLogger::new(LevelFilter::Info, Config::default(), log_file),
])
.unwrap();
let rate_limiter = Arc::new(Mutex::new(RateLimiter {
last_sent_ip: HashMap::new(),
last_sent_address: HashMap::new(),
}));
let rate_limiter_filter = warp::any().map(move || rate_limiter.clone());
let send_faucet = warp::post()
.and(warp::path("send"))
.and(warp::body::json())
.and(rate_limiter_filter.clone())
.and(warp::addr::remote()) // Get the remote address
.map(
|request: SendRequest,
rate_limiter: Arc<Mutex<RateLimiter>>,
remote_addr: Option<std::net::SocketAddr>| {
let address = request.address;
// Validate the address
if !is_valid_address(&address) {
return warp::reply::json(&Response {
message:
"Invalid: Must start with 'grin1' and be a valid 62 character address"
.to_string(),
});
}
let mut rate_limiter = rate_limiter.lock().unwrap();
let now = Local::now();
// Hash the IP address
let ip_hash = match remote_addr {
Some(addr) => hash_ip(&addr.ip().to_string()),
None => {
return warp::reply::json(&Response {
message: "Could not retrieve IP address".to_string(),
});
}
};
info!("IP Hash: {}", ip_hash);
// Check if the IP address has been sent funds in the last 24 hours
if let Some(last_sent) = rate_limiter.last_sent_ip.get(&ip_hash) {
if now - *last_sent < Duration::hours(24) {
info!("Criminal {} rate limited.", ip_hash);
return warp::reply::json(&Response {
message: "You can only request 1ツ every 24 hours".to_string(),
});
}
}
// Check if the wallet address has been sent funds in the last 24 hours
if let Some(last_sent) = rate_limiter.last_sent_address.get(&address) {
if now - *last_sent < Duration::hours(24) {
info!("Wallet {} rate limited.", address);
return warp::reply::json(&Response {
message: "This wallet address can only request 1ツ every 24 hours".to_string(),
});
}
}
// Execute the command
let output = Command::new("bash")
.arg("-c")
.arg(format!(
"echo '<password>' | /usr/local/bin/grin-wallet send -d {} 1",
address
))
.output()
.expect("Failed to execute command");
// Update the last sent time for both IP and wallet address
rate_limiter.last_sent_ip.insert(ip_hash.clone(), now);
rate_limiter.last_sent_address.insert(address.clone(), now);
// Handle command output
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.is_empty() {
if let Some(slatepack_message) = extract_slatepack_message(&stdout) {
info!(" {}", slatepack_message);
return warp::reply::json(&Response {
message: slatepack_message,
});
} else {
info!("Grin sent successfully to address: {}", address);
return warp::reply::json(&Response {
message: "Grin sent via TOR ツ".to_string(),
});
}
} else {
error!("Error sending funds to address {}: {}", address, stderr);
return warp::reply::json(&Response {
message: format!("Error: {}", stderr),
});
}
},
);
// Load SSL keys and certs
let cert_path = "/etc/ssl/cert.pem";
let key_path = "/etc/ssl/privkey.pem";
// Enable CORS only from this site
let cors = warp::cors()
.allow_origin("https://<URL>")
.allow_methods(vec!["POST"]) // Allow POST requests
.allow_headers(vec!["Content-Type"]); // Allow Content-Type header
// Start the warp server with CORS & TLS
warp::serve(send_faucet.with(cors))
.tls()
.cert_path(cert_path)
.key_path(key_path)
.run(([0, 0, 0, 0], 3031)) // Listen on all interfaces
.await;
}
// Function to extract the slatepack message from the output
fn extract_slatepack_message(stdout: &str) -> Option<String> {
let start_marker = "BEGINSLATEPACK.";
let end_marker = "ENDSLATEPACK.";
if let Some(start) = stdout.find(start_marker) {
if let Some(end) = stdout.find(end_marker) {
let slatepack_message = &stdout[start..end + end_marker.len()];
let trimmed_message = if slatepack_message.starts_with(' ') {
&slatepack_message[1..] // Remove the first character (space)
} else {
slatepack_message // Return the original message if no leading space
};
return Some(trimmed_message.to_string());
}
}
None
}