mirror of
https://github.com/transatoshi-mw/grin-faucet.git
synced 2025-10-06 15:42:47 +00:00
Final version
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "grin_faucet"
|
name = "grin-faucet"
|
||||||
version = "1.0.0"
|
version = "1.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -10,6 +10,8 @@ tokio-rustls = "0.22"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
simplelog = "0.11"
|
simplelog = "0.11"
|
||||||
|
arti-client = "0.30.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
|
arti = "1.4.3"
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
# grin-faucet
|
# grin_faucet
|
||||||
|
|
||||||
Grin faucet backend written in Rust.
|
Grin faucet written in Rust with a vanilla HTML/CSS/JS frontend.
|
||||||
|
|
||||||
It is meant to run on your Grin node, you will need to edit the wallet \<PASSWORD\>, \<DIRs\> and \<URLs\> in main.rs.
|
It is meant to run on your Grin node, you will need to edit the wallet \<PASSWORD\> and URLs in main.rs.
|
||||||
|
|
||||||
There is no front end or webserver included so you will need to serve an HTML/CSS/JS page with something like Nginx.
|
There is no front end or webserver included so you will need to serve an HTML/CSS/JS page with something like Nginx.
|
||||||
|
|
||||||
To run after cloning and changing parameters, cd in to it, and execute 'cargo run'. Your faucet will listen on port 3031. Check out the releases page for an executable. To create your own executable run 'cargo build --release'.
|
To run after changing parameters, simply clone the repository, cd in to it, and execute 'cargo run'. Your faucet will listen on port 3031. Check out the releases page for an executable.
|
||||||
|
|
||||||
|
|
||||||
|
128
src/main.rs
128
src/main.rs
@@ -20,13 +20,14 @@ struct Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct RateLimiter {
|
struct RateLimiter {
|
||||||
last_sent: HashMap<String, chrono::DateTime<Local>>,
|
last_sent_ip: HashMap<String, chrono::DateTime<Local>>,
|
||||||
|
last_sent_address: HashMap<String, chrono::DateTime<Local>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_valid_address(address: &str) -> bool {
|
fn is_valid_address(address: &str) -> bool {
|
||||||
if address.is_empty()
|
if address.is_empty()
|
||||||
|| address.contains(' ')
|
|| address.contains(' ')
|
||||||
|| !address.starts_with("grin1")
|
|| (!address.starts_with("grin1"))
|
||||||
|| !address.chars().all(|c| c.is_alphanumeric())
|
|| !address.chars().all(|c| c.is_alphanumeric())
|
||||||
|| address.len() < 62
|
|| address.len() < 62
|
||||||
{
|
{
|
||||||
@@ -35,18 +36,18 @@ fn is_valid_address(address: &str) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to hash the IP address
|
// hash the IP address
|
||||||
fn hash_ip(ip: &str) -> String {
|
fn hash_ip(ip: &str) -> String {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(ip);
|
hasher.update(ip);
|
||||||
let result = hasher.finalize();
|
let result = hasher.finalize();
|
||||||
hex::encode(result) // Convert the hash to a hexadecimal string
|
hex::encode(result) // hash to hex
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Initialize logging
|
// logging
|
||||||
let log_file = File::create("app.log").unwrap();
|
let log_file = File::create("faucet.log").unwrap();
|
||||||
CombinedLogger::init(vec![
|
CombinedLogger::init(vec![
|
||||||
TermLogger::new(
|
TermLogger::new(
|
||||||
LevelFilter::Info,
|
LevelFilter::Info,
|
||||||
@@ -59,7 +60,8 @@ async fn main() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let rate_limiter = Arc::new(Mutex::new(RateLimiter {
|
let rate_limiter = Arc::new(Mutex::new(RateLimiter {
|
||||||
last_sent: HashMap::new(),
|
last_sent_ip: HashMap::new(),
|
||||||
|
last_sent_address: HashMap::new(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let rate_limiter_filter = warp::any().map(move || rate_limiter.clone());
|
let rate_limiter_filter = warp::any().map(move || rate_limiter.clone());
|
||||||
@@ -68,18 +70,18 @@ async fn main() {
|
|||||||
.and(warp::path("send"))
|
.and(warp::path("send"))
|
||||||
.and(warp::body::json())
|
.and(warp::body::json())
|
||||||
.and(rate_limiter_filter.clone())
|
.and(rate_limiter_filter.clone())
|
||||||
.and(warp::addr::remote()) // Get the remote address
|
.and(warp::addr::remote())
|
||||||
.map(
|
.map(
|
||||||
|request: SendRequest,
|
|request: SendRequest,
|
||||||
rate_limiter: Arc<Mutex<RateLimiter>>,
|
rate_limiter: Arc<Mutex<RateLimiter>>,
|
||||||
remote_addr: Option<std::net::SocketAddr>| {
|
remote_addr: Option<std::net::SocketAddr>| {
|
||||||
let address = request.address;
|
let address = request.address;
|
||||||
|
|
||||||
// Validate the address
|
// wallet address validator
|
||||||
if !is_valid_address(&address) {
|
if !is_valid_address(&address) {
|
||||||
return warp::reply::json(&Response {
|
return warp::reply::json(&Response {
|
||||||
message:
|
message:
|
||||||
"Invalid: Must start with 'grin1' and be a valid 62 character address"
|
"Invalid: Must start with 'grin1' and be a valid address"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -87,7 +89,7 @@ async fn main() {
|
|||||||
let mut rate_limiter = rate_limiter.lock().unwrap();
|
let mut rate_limiter = rate_limiter.lock().unwrap();
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
|
|
||||||
// Hash the IP address
|
// hash the IP address
|
||||||
let ip_hash = match remote_addr {
|
let ip_hash = match remote_addr {
|
||||||
Some(addr) => hash_ip(&addr.ip().to_string()),
|
Some(addr) => hash_ip(&addr.ip().to_string()),
|
||||||
None => {
|
None => {
|
||||||
@@ -97,72 +99,98 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("IP Hash: {}", ip_hash);
|
// check IP for sends in the last 24 hours
|
||||||
|
if let Some(last_sent) = rate_limiter.last_sent_ip.get(&ip_hash) {
|
||||||
// Check if the address has been sent funds in the last 24 hours
|
|
||||||
if let Some(last_sent) = rate_limiter.last_sent.get(&ip_hash) {
|
|
||||||
if now - *last_sent < Duration::hours(24) {
|
if now - *last_sent < Duration::hours(24) {
|
||||||
info!("Criminal {} requested funds too often.", ip_hash);
|
|
||||||
return warp::reply::json(&Response {
|
return warp::reply::json(&Response {
|
||||||
message: "You can only request 1ツ every 24 hours".to_string(),
|
message: "You can only request 1ツ every 24 hours".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the command
|
// check address for sends in the last 24 hours
|
||||||
|
if let Some(last_sent) = rate_limiter.last_sent_address.get(&address) {
|
||||||
|
if now - *last_sent < Duration::hours(24) {
|
||||||
|
return warp::reply::json(&Response {
|
||||||
|
message: "This wallet address can only request 1ツ every 24 hours".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// grin-wallet send
|
||||||
let output = Command::new("bash")
|
let output = Command::new("bash")
|
||||||
.arg("-c")
|
.arg("-c")
|
||||||
.arg(format!(
|
.arg(format!(
|
||||||
"echo '<PASSWORD>' | <DIR>/grin-wallet send -d {} 1",
|
"echo '<PASSWORD>' | /usr/local/bin/grin-wallet send -d {} 1",
|
||||||
address
|
address
|
||||||
))
|
))
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to execute command");
|
.expect("Failed to execute command");
|
||||||
|
|
||||||
// Update the last sent time
|
// Handle grin-wallet output
|
||||||
rate_limiter.last_sent.insert(ip_hash.clone(), now);
|
|
||||||
|
|
||||||
// Handle command output
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
if stderr.is_empty() {
|
// Not enough funds error
|
||||||
if let Some(slatepack_message) = extract_slatepack_message(&stdout) {
|
let combined_output = format!("{}{}", stdout, stderr);
|
||||||
info!(" {}", slatepack_message);
|
if combined_output.contains("Not enough funds") ||
|
||||||
return warp::reply::json(&Response {
|
combined_output.contains("LibWallet Error: Not enough funds") ||
|
||||||
message: slatepack_message,
|
combined_output.contains("Wallet command failed: Not enough funds") {
|
||||||
});
|
error!("Faucet is empty ¯\\_(ツ)_/¯");
|
||||||
} 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 {
|
return warp::reply::json(&Response {
|
||||||
message: format!("Error: {}", stderr),
|
message: "Faucet is empty ¯\\_(ツ)_/¯".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load SSL keys and certs
|
// Offline wallet slatepack response
|
||||||
let cert_path = "<PATHTOCERT>";
|
if let Some(slatepack_message) = extract_slatepack_message(&stdout) {
|
||||||
let key_path = "<PATHTOPRIVKEY>";
|
rate_limiter.last_sent_ip.insert(ip_hash.clone(), now);
|
||||||
|
rate_limiter.last_sent_address.insert(address.clone(), now);
|
||||||
|
|
||||||
// Enable CORS only from this site
|
info!("IP: {}, Wallet: {}, Slatepack issued", ip_hash, address);
|
||||||
|
return warp::reply::json(&Response {
|
||||||
|
message: slatepack_message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tor sending success
|
||||||
|
if stdout.contains("WARN grin_wallet_api::owner - Attempting to send transaction via TOR")
|
||||||
|
&& stdout.contains("Tx sent successfully")
|
||||||
|
&& stdout.contains("Command 'send' completed successfully")
|
||||||
|
{
|
||||||
|
rate_limiter.last_sent_ip.insert(ip_hash.clone(), now);
|
||||||
|
rate_limiter.last_sent_address.insert(address.clone(), now);
|
||||||
|
|
||||||
|
info!("Grin sent via Tor to Wallet: {} (IP: {})", address, ip_hash);
|
||||||
|
return warp::reply::json(&Response {
|
||||||
|
message: "Grin sent via Tor ツ".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// sending errors (log but don’t rate limit)
|
||||||
|
error!("Error sending funds to Wallet {} (IP: {}): {}", address, ip_hash, stderr);
|
||||||
|
warp::reply::json(&Response {
|
||||||
|
message: "Error: Transaction did not complete successfully.".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load TLS keys
|
||||||
|
let cert_path = "/etc/ssl/cert.pem";
|
||||||
|
let key_path = "/etc/ssl/privkey.pem";
|
||||||
|
|
||||||
|
// Enable CORS
|
||||||
let cors = warp::cors()
|
let cors = warp::cors()
|
||||||
.allow_origin("https://<URL>")
|
.allow_origin("https://spigot.grinminer.net")
|
||||||
.allow_methods(vec!["POST"]) // Allow POST requests
|
.allow_methods(vec!["POST"])
|
||||||
.allow_headers(vec!["Content-Type"]); // Allow Content-Type header
|
.allow_headers(vec!["Content-Type"]);
|
||||||
|
|
||||||
// Start the warp server with CORS & TLS
|
// Start the warp server
|
||||||
warp::serve(send_faucet.with(cors))
|
warp::serve(send_faucet.with(cors))
|
||||||
.tls()
|
.tls()
|
||||||
.cert_path(cert_path)
|
.cert_path(cert_path)
|
||||||
.key_path(key_path)
|
.key_path(key_path)
|
||||||
.run(([0, 0, 0, 0], 3031)) // Listen on all interfaces
|
.run(([0, 0, 0, 0], 3031))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,9 +204,9 @@ fn extract_slatepack_message(stdout: &str) -> Option<String> {
|
|||||||
let slatepack_message = &stdout[start..end + end_marker.len()];
|
let slatepack_message = &stdout[start..end + end_marker.len()];
|
||||||
|
|
||||||
let trimmed_message = if slatepack_message.starts_with(' ') {
|
let trimmed_message = if slatepack_message.starts_with(' ') {
|
||||||
&slatepack_message[1..] // Remove the first character (space)
|
&slatepack_message[1..] // Remove the whitespace at beginning
|
||||||
} else {
|
} else {
|
||||||
slatepack_message // Return the original message if no leading space
|
slatepack_message // Return original message if no space
|
||||||
};
|
};
|
||||||
|
|
||||||
return Some(trimmed_message.to_string());
|
return Some(trimmed_message.to_string());
|
||||||
|
Reference in New Issue
Block a user