Skip to content

Commit 4a05e0b

Browse files
committed
Well, add the program sources
1 parent 28273d6 commit 4a05e0b

File tree

6 files changed

+226
-0
lines changed

6 files changed

+226
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ Cargo.lock
88

99
# These are backup files generated by rustfmt
1010
**/*.rs.bk
11+
12+
/target
13+
**/*.rs.bk

Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "gh-hook-command"
3+
version = "0.1.0"
4+
authors = ["Fingercomp <[email protected]>"]
5+
license = "Apache 2.0"
6+
7+
[dependencies]
8+
actix-web = "0.6"
9+
futures = "0.1"
10+
11+
rust-crypto = "0.2"
12+
13+
serde = "1.0"
14+
toml = "0.4"
15+
serde_json = "1.0"
16+
serde_derive = "1.0"

config.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
secret = "this is a secret secret you should not know about"
2+
bind = "0.0.0.0:8000"
3+
4+
[commands]
5+
push = 'cat /dev/stdin && echo ""'

src/app.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use std::sync::Arc;
2+
3+
use ::config::Config;
4+
5+
#[derive(Clone)]
6+
pub struct State {
7+
pub config: Arc<Config>,
8+
}

src/config.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use std::collections::HashMap;
2+
use std::fs::{self, File};
3+
use std::io::Write;
4+
use std::net::SocketAddr;
5+
use std::path::Path;
6+
7+
use toml;
8+
9+
const DEFAULT_CONFIG: &'static str = {
10+
r#"secret = "this is a secret secret you should not know about"
11+
bind = "0.0.0.0:8000"
12+
13+
[commands]
14+
push = 'cat /dev/stdin && echo ""'
15+
"#
16+
};
17+
18+
#[derive(Debug, Clone, Deserialize)]
19+
pub struct Config {
20+
pub commands: HashMap<String, String>,
21+
pub secret: String,
22+
pub bind: SocketAddr,
23+
}
24+
25+
impl Config {
26+
pub fn load(path: impl AsRef<Path>) -> Config {
27+
if !path.as_ref().exists() {
28+
File::create(&path).expect("Failed to create a new config file")
29+
.write_all(DEFAULT_CONFIG.as_bytes())
30+
.expect("Failed to write to a new config file");
31+
}
32+
33+
toml::from_str(
34+
&fs::read_to_string(path)
35+
.expect("Failed to read the config file"))
36+
.expect("Failed to parse the config file as a TOML file")
37+
}
38+
}

src/main.rs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
extern crate actix_web;
2+
extern crate crypto;
3+
extern crate futures;
4+
extern crate serde;
5+
#[macro_use] extern crate serde_derive;
6+
extern crate toml;
7+
8+
mod app;
9+
mod config;
10+
11+
use std::sync::Arc;
12+
use std::env;
13+
use std::io::Write;
14+
use std::process::{Command, Stdio};
15+
16+
use actix_web::{
17+
server,
18+
AsyncResponder,
19+
App,
20+
HttpMessage,
21+
HttpRequest,
22+
HttpResponse,
23+
Responder
24+
};
25+
use actix_web::dev::AsyncResult;
26+
use actix_web::http::Method;
27+
use crypto::mac::{Mac, MacResult};
28+
use crypto::hmac::Hmac;
29+
use crypto::sha1::Sha1;
30+
use futures::Future;
31+
32+
use ::config::Config;
33+
use ::app::State;
34+
35+
fn from_hex(bytes: &[u8]) -> Option<Vec<u8>> {
36+
if bytes.len() % 2 == 1 {
37+
return None;
38+
}
39+
40+
if !bytes.iter().all(|x| x.is_ascii_hexdigit()) {
41+
return None;
42+
}
43+
44+
let nibbles: Vec<u8> = bytes.iter().map(|&x| {
45+
char::from(x).to_digit(16).unwrap() as u8
46+
}).collect();
47+
48+
Some(nibbles.as_slice()
49+
.chunks(2)
50+
.map(|x| {
51+
let (n1, n2) = (x[0], x[1]);
52+
(n1 << 4 | n2) as u8
53+
})
54+
.collect())
55+
}
56+
57+
fn run_command(command: &str, input: &[u8]) {
58+
match Command::new("sh")
59+
.arg("-c")
60+
.arg(command)
61+
.stdin(Stdio::piped())
62+
.spawn()
63+
{
64+
Ok(mut child) => {
65+
match child.stdin.as_mut() {
66+
Some(stdin) => {
67+
if let Err(e) = stdin.write_all(input) {
68+
println!("Failed to write to stdin for {}: {}",
69+
command, e);
70+
}
71+
}
72+
None => {
73+
println!("Failed to open stdin for {}", command);
74+
}
75+
}
76+
77+
if let Err(e) = child.wait() {
78+
println!("Failed to run {}: {}", command, e);
79+
}
80+
}
81+
Err(e) => {
82+
println!("Failed to run command {}: {}", command, e);
83+
}
84+
}
85+
}
86+
87+
fn verify_signature<'a>(body: &'a [u8], secret: &'a [u8], signature: &'a [u8])
88+
-> bool
89+
{
90+
let mut hmac = Hmac::new(Sha1::new(), secret);
91+
hmac.input(body);
92+
hmac.result() == MacResult::new(signature)
93+
}
94+
95+
fn hook(request: HttpRequest<State>) -> impl Responder
96+
{
97+
let config = Arc::clone(&request.state().config);
98+
99+
let (event, signature) = {
100+
let headers = request.headers();
101+
(headers.get("X-GitHub-Event").cloned(),
102+
headers.get("X-Hub-Signature").cloned())
103+
};
104+
105+
if let (Some(event), Some(signature_value)) = (event, signature) {
106+
let event = String::from_utf8_lossy(event.as_bytes()).into_owned();
107+
108+
let signature = if signature_value.as_bytes().starts_with(b"sha1=") {
109+
if let Some(slice) = signature_value.as_bytes().get(5..) {
110+
from_hex(slice)
111+
} else {
112+
from_hex(signature_value.as_bytes())
113+
}
114+
} else {
115+
from_hex(signature_value.as_bytes())
116+
};
117+
118+
if let Some(signature) = signature {
119+
let secret = config.secret.clone();
120+
121+
if let Some(command) = config.commands.get(&event).cloned() {
122+
return request.body().and_then(move |bytes| {
123+
124+
if verify_signature(&bytes,
125+
&secret.as_bytes(),
126+
&signature) {
127+
run_command(&command, &bytes);
128+
}
129+
Ok(HttpResponse::NoContent())
130+
})
131+
.responder();
132+
}
133+
}
134+
}
135+
136+
AsyncResult::ok(HttpResponse::NoContent()).responder()
137+
}
138+
139+
fn main() {
140+
let config_path = env::var("GH_HOOK_CONFIG")
141+
.unwrap_or("./config.toml".to_string());
142+
143+
let config = Config::load(config_path);
144+
let bind_addr = config.bind.clone();
145+
146+
let state = State {
147+
config: Arc::new(config),
148+
};
149+
150+
server::new(move || {
151+
App::with_state(state.clone())
152+
.resource("/hook", |r| r.method(Method::POST).f(::hook))
153+
})
154+
.bind(bind_addr).unwrap()
155+
.run();
156+
}

0 commit comments

Comments
 (0)