Initial commit

This commit is contained in:
Pascal Engélibert 2025-03-29 22:15:07 +01:00
commit b35120be22
9 changed files with 677 additions and 0 deletions

17
src/challenge.html Normal file
View file

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex"/>
<title>Antispam working...</title>
</head>
<body>
<h1>Fighting crawlers</h1>
<script type="text/javascript">
/*setTimeout(function() {
document.cookie = "mesozoa=1234; max-age=3600";
window.location.reload();
}, 1000);*/
</script>
</body>
</html>

58
src/http.rs Normal file
View file

@ -0,0 +1,58 @@
/// Iterates lines of HTTP header and stops at end of header
pub struct HeaderLineIterator<'a> {
packet: &'a [u8],
start: usize,
cr: bool,
i: usize,
}
impl<'a> HeaderLineIterator<'a> {
pub fn new(packet: &'a [u8]) -> Self {
Self {
packet,
start: 0,
cr: false,
i: 0,
}
}
}
impl<'a> Iterator for HeaderLineIterator<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
while self.i < self.packet.len() {
let c = self.packet[self.i];
if c == b'\r' {
self.cr = true;
} else if c == b'\n' && self.cr {
self.cr = false;
self.i += 1;
let start = self.start;
self.start = self.i;
return Some(&self.packet[start..self.i - 2]);
} else {
self.cr = false;
}
self.i += 1;
}
None
}
}
pub fn parse_cookies<'a>(line: &'a [u8]) -> Option<&'a [u8]> {
if line.get(0..7) != Some(b"Cookie:") {
return None;
}
let mut waiting_for_name = true;
let mut iter = line.iter().enumerate().skip(7);
while let Some((i, c)) = iter.next() {
if *c == b' ' {
continue
}
if waiting_for_name {
}
//iter.advance_by(5);
}
None
}

95
src/main.rs Normal file
View file

@ -0,0 +1,95 @@
mod http;
mod policy;
use http::HeaderLineIterator;
use policy::{CompiledPolicies, Policy};
use regex::bytes::Regex;
use std::{
io::{BufReader, Write},
net::SocketAddr,
time::Duration,
};
use tokio::{io::{ReadBuf, AsyncWriteExt}, time::timeout};
static CHALLENGE_BODY: &str = include_str!("challenge.html");
macro_rules! mk_static {
($t:ty, $val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val));
x
}};
}
#[tokio::main]
async fn main() {
let listen_addr = "127.0.0.1:8000".parse().unwrap();
let policy_groups = &[&[Policy {
name: String::from("Block"),
filter: policy::Filter::FirstLineMatch(String::from("GET /block")),
action: policy::Action::Drop,
priority: 0,
}]];
let challenge_response = &*mk_static!(String, format!("HTTP/1.1 200\r\ncontent-type: text/html\r\ncontent-length: {}\r\n\r\n{}", CHALLENGE_BODY.len(), CHALLENGE_BODY));
let policy_groups: Vec<CompiledPolicies> = policy_groups.into_iter().map(|policies| CompiledPolicies::new(*policies)).collect();
let socket = realm_syscall::new_tcp_socket(&listen_addr).unwrap();
socket.set_reuse_address(true).ok();
socket.bind(&listen_addr.into()).unwrap();
socket.listen(1024).unwrap();
let listener = tokio::net::TcpListener::from_std(socket.into()).unwrap();
let cookie_regex = Regex::new(r"^Cookie: *(?:[^;=]+=[^;=]* *; *)*mesozoa *= *([0-9a-zA-Z]{4})").unwrap();
loop {
let Ok((mut client_stream, client_addr)) = listener.accept().await else {
continue;
};
//client_stream.set_nodelay(true).ok();
let cookie_regex = cookie_regex.clone();
tokio::spawn(async move {
let mut buf = [0u8; 1024];
let mut buf_reader = ReadBuf::new(&mut buf);
if let Err(_) = timeout(
Duration::from_millis(100),
std::future::poll_fn(|cx| client_stream.poll_peek(cx, &mut buf_reader)),
)
.await
{
println!("peek timeout");
return;
}
let mut header_line_iter = HeaderLineIterator::new(&buf);
let Some(first_line) = header_line_iter.next() else {
println!("Not HTTP, or too long line");
return;
};
// TODO matching
// for test we will challenge everything!
if let Some(captures) = header_line_iter.find_map(|line| cookie_regex.captures(line)) {
if let Some(cookie) = captures.get(1) {
let mut stdout = std::io::stdout();
stdout.write_all(cookie.as_bytes()).unwrap();
stdout.flush().unwrap();
println!("");
} else {
println!("cookie header, but no cookie")
}
} else {
println!("no cookie");
}
client_stream.writable().await.unwrap();
client_stream.write_all(challenge_response.as_bytes()).await.unwrap();
});
}
}

131
src/policy.rs Normal file
View file

@ -0,0 +1,131 @@
use regex::{Regex, RegexSet};
#[derive(Clone, Debug)]
pub enum Action {
Allow,
Challenge,
Drop,
}
#[derive(Clone, Debug)]
pub enum Filter {
Bool(bool),
FirstLineMatch(String),
HeaderLineMatch(String),
And(Vec<Filter>),
Or(Vec<Filter>),
Not(Box<Filter>),
}
impl Filter {
fn compile<'a>(
&'a self,
first_line_regexes: &mut Vec<&'a str>,
header_line_regexes: &mut Vec<&'a str>,
) -> CompiledFilter {
match self {
Filter::Bool(v) => CompiledFilter::Bool(*v),
Filter::And(filters) => CompiledFilter::And(
filters
.iter()
.map(|filter| filter.compile(first_line_regexes, header_line_regexes))
.collect(),
),
Filter::Or(filters) => CompiledFilter::Or(
filters
.iter()
.map(|filter| filter.compile(first_line_regexes, header_line_regexes))
.collect(),
),
Filter::Not(filter) => CompiledFilter::Not(Box::new(
filter.compile(first_line_regexes, header_line_regexes),
)),
Filter::FirstLineMatch(regex) => {
let filter = CompiledFilter::FirstLineMatch(first_line_regexes.len());
first_line_regexes.push(regex);
filter
}
Filter::HeaderLineMatch(regex) => {
let filter = CompiledFilter::HeaderLineMatch(header_line_regexes.len());
header_line_regexes.push(regex);
filter
}
}
}
}
#[derive(Clone, Debug)]
pub struct Policy {
pub name: String,
pub filter: Filter,
pub action: Action,
pub priority: i32,
}
pub enum CompiledFilter {
Bool(bool),
FirstLineMatch(usize),
HeaderLineMatch(usize),
And(Vec<CompiledFilter>),
Or(Vec<CompiledFilter>),
Not(Box<CompiledFilter>),
}
pub struct CompiledPolicy {
pub name: String,
pub filter: CompiledFilter,
pub priority: i32,
pub action: Action,
}
pub struct CompiledPolicies {
pub first_line_regex_set: Option<RegexSet>,
pub header_line_regex_set: Option<RegexSet>,
pub policies: Vec<CompiledPolicy>,
}
impl CompiledPolicies {
pub fn new<'a>(policies: impl IntoIterator<Item = &'a Policy>) -> Self {
let mut first_line_regexes = Vec::new();
let mut header_line_regexes = Vec::new();
let mut compiled_policies = Vec::new();
for policy in policies {
let compiled_policy = CompiledPolicy {
name: policy.name.clone(),
filter: policy
.filter
.compile(&mut first_line_regexes, &mut header_line_regexes),
priority: policy.priority,
action: policy.action.clone(),
};
compiled_policies.push(compiled_policy);
}
CompiledPolicies {
first_line_regex_set: if first_line_regexes.is_empty() {None} else {Some(RegexSet::new(&first_line_regexes).unwrap())},
header_line_regex_set: if header_line_regexes.is_empty() {None} else {Some(RegexSet::new(&header_line_regexes).unwrap())},
policies: compiled_policies,
}
}
pub fn evaluate<'a>(&self, mut header_lines: impl Iterator<Item=&'a [u8]>) -> Result<Option<&CompiledPolicy>, PolicyEvaluationError> {
let mut best_policy = None;
let mut best_priority = i32::MAX;
let first_line = header_lines.next().ok_or(PolicyEvaluationError::NoFirstLine)?;
if let Some(first_line_regex_set) = &self.first_line_regex_set {
//let matches = first_line_regex_set.matches(first_line);
}
Ok(best_policy)
}
}
#[derive(Debug)]
pub enum PolicyEvaluationError {
/// First HTTP line is too long or absent
NoFirstLine,
}