use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use octocrab::models::webhook_events::{
WebhookEvent, WebhookEventError, WebhookEventType, WebhookVerificationError, WebhookVerifier,
};
fn handle_client(mut stream: TcpStream, verifier: &WebhookVerifier) {
let mut buffer = Vec::new();
let mut temp = [0u8; 1024];
let (headers, body_offset) = loop {
let n = match stream.read(&mut temp) {
Ok(0) => return,
Ok(n) => n,
Err(_) => return,
};
buffer.extend_from_slice(&temp[..n]);
if let Some(pos) = buffer.windows(4).position(|w| w == b"\r\n\r\n") {
let header_str = String::from_utf8_lossy(&buffer[..pos]);
break (header_str.to_string(), pos + 4);
}
};
let content_length: usize = headers
.lines()
.find_map(|line| {
let line = line.to_lowercase();
if line.starts_with("content-length:") {
line.split(':').nth(1)?.trim().parse().ok()
} else {
None
}
})
.unwrap_or(0);
while buffer.len() < body_offset + content_length {
let n = match stream.read(&mut temp) {
Ok(0) => break,
Ok(n) => n,
Err(_) => break,
};
buffer.extend_from_slice(&temp[..n]);
}
let body = &buffer[body_offset..body_offset + content_length];
let event_type = headers.lines().find_map(|line| {
if line.to_lowercase().starts_with("x-github-event:") {
Some(line.split(':').nth(1)?.trim())
} else {
None
}
});
let signature = headers.lines().find_map(|line| {
if line.to_lowercase().starts_with("x-hub-signature-256:") {
Some(line.split(':').nth(1)?.trim())
} else {
None
}
});
let (event_type, signature) = match (event_type, signature) {
(Some(e), Some(s)) => (e, s),
(None, _) => {
let response =
"HTTP/1.1 400 Bad Request\r\nContent-Length: 26\r\n\r\nMissing X-GitHub-Event";
let _ = stream.write_all(response.as_bytes());
return;
}
(_, None) => {
let response =
"HTTP/1.1 400 Bad Request\r\nContent-Length: 33\r\n\r\nMissing X-Hub-Signature-256";
let _ = stream.write_all(response.as_bytes());
return;
}
};
match verifier.verify_and_parse(event_type, signature, body) {
Ok(event) => {
println!(
"Successfully verified signature for event: {:?}",
event.kind
);
handle_webhook_event(event);
let response = "HTTP/1.1 200 OK\r\nContent-Length: 16\r\n\r\nWebhook received";
let _ = stream.write_all(response.as_bytes());
}
Err(WebhookEventError::Verification {
source: WebhookVerificationError::DeprecatedSha1,
}) => {
eprintln!("Rejected delivery: SHA-1 signatures are deprecated by GitHub");
let response =
"HTTP/1.1 400 Bad Request\r\nContent-Length: 35\r\n\r\nSHA-1 signatures are deprecated";
let _ = stream.write_all(response.as_bytes());
}
Err(WebhookEventError::Verification { source }) => {
eprintln!("Signature verification failed: {}", source);
let response =
"HTTP/1.1 401 Unauthorized\r\nContent-Length: 29\r\n\r\nInvalid webhook signature";
let _ = stream.write_all(response.as_bytes());
}
Err(WebhookEventError::Json { source }) => {
eprintln!("JSON deserialization failed: {}", source);
let response =
"HTTP/1.1 400 Bad Request\r\nContent-Length: 25\r\n\r\nMalformed JSON payload";
let _ = stream.write_all(response.as_bytes());
}
}
}
fn handle_webhook_event(event: WebhookEvent) {
match event.kind {
WebhookEventType::Ping => {
println!("-> Received Ping event from GitHub!");
}
WebhookEventType::Issues => {
println!("-> Received Issues event!");
}
WebhookEventType::PullRequest => {
println!("-> Received PullRequest event!");
}
WebhookEventType::Push => {
println!("-> Received Push event!");
}
other => {
println!("-> Received event: {:?}", other);
}
}
}
fn main() {
let secret = std::env::var("WEBHOOK_SECRET").unwrap_or_else(|_| "secret".to_string());
let verifier = WebhookVerifier::new(&secret);
let address = "127.0.0.1:3000";
let listener = match TcpListener::bind(address) {
Ok(l) => l,
Err(e) => {
eprintln!("Failed to bind to {}: {}", address, e);
return;
}
};
println!("GitHub Webhook Server listening on http://{}", address);
println!("Webhook secret: '{}'", secret);
println!("Awaiting GitHub webhook deliveries...");
for stream in listener.incoming() {
match stream {
Ok(stream) => handle_client(stream, &verifier),
Err(e) => eprintln!("Connection failed: {}", e),
}
}
}