mirror of
https://github.com/transatoshi-mw/grin-faucet.git
synced 2025-10-06 15:42:47 +00:00
Initial commit
This commit is contained in:
15
Cargo.toml
Normal file
15
Cargo.toml
Normal 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"
|
13
README.md
13
README.md
@@ -1,2 +1,11 @@
|
||||
# grin-faucet
|
||||
Backend for a Grin faucet built in Rust
|
||||
# grin_faucet
|
||||
|
||||
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\> 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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
188
src/main.rs
Normal file
188
src/main.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
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: 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("app.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: 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 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) {
|
||||
info!("Criminal {} requested funds too often.", ip_hash);
|
||||
return warp::reply::json(&Response {
|
||||
message: "You can only request 1ツ every 24 hours".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
let output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(format!(
|
||||
"echo '<PASSWORD>' | <DIR>/grin-wallet send -d {} 1",
|
||||
address
|
||||
))
|
||||
.output()
|
||||
.expect("Failed to execute command");
|
||||
|
||||
// Update the last sent time
|
||||
rate_limiter.last_sent.insert(ip_hash.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 = "<PATHTOCERT>";
|
||||
let key_path = "<PATHTOPRIVKEY>";
|
||||
|
||||
// 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
|
||||
}
|
BIN
target/release/grin_faucet
Normal file
BIN
target/release/grin_faucet
Normal file
Binary file not shown.
Reference in New Issue
Block a user