revealjs-hosting/src/main.rs

286 lines
9.3 KiB
Rust

extern crate actix;
extern crate actix_web;
extern crate git2;
extern crate http;
extern crate serde_json;
extern crate env_logger;
// This example serves the docs from target/doc/staticfile at /doc/
//
// Run `cargo doc && cargo run --example doc_server`, then
// point your browser to http://127.0.0.1:3000/doc/
use std::path::{Path, PathBuf};
use std::error::Error;
use std::env;
use std::sync::{Arc, Mutex};
use actix::ActorContext;
use actix_web::{
fs, ws, middleware, server, App, HttpMessage, HttpRequest, HttpResponse
};
use git2::Repository;
struct WebHookConfig {
secret: String,
repo_root: PathBuf,
repo_prefixes: Vec<String>,
}
fn redeploy_repo(url: &str, path: &Path, name: &str) -> Result<(), git2::Error> {
let mut new_path = path.to_path_buf();
new_path.push(name);
eprintln!("Cloning {} into {:?} ({:?} / {})", url, new_path, path, name);
let repo = match Repository::open(&new_path) {
Ok(repo) => repo,
Err(e) => {
eprintln!("can't open repo, going to try cloning it: {}", e.description());
match Repository::clone(url, &new_path) {
Ok(repo) => repo,
Err(e) => panic!("failed to clone: {}", e),
}
}
};
repo.find_remote("origin")?.fetch(&["master"], None, None)?;
repo.set_head("FETCH_HEAD")?;
repo.checkout_head(Some(git2::build::CheckoutBuilder::new().force().use_theirs(true)))?;
Ok(())
}
fn webhook_handler((contents, req): (String, HttpRequest<Arc<Mutex<WebHookConfig>>>)) -> HttpResponse {
let config = req.state().lock().unwrap();
// Ensure there's a "Push" event
if let Some(v) = req.headers().get("X-GitHub-Event") {
match v.to_str() {
Err(_) => return HttpResponse::BadRequest().body("invalid X-Github-Event header"),
Ok(o) => {
if o != "push" {
return HttpResponse::Ok().body("Ok");
}
}
}
} else {
return HttpResponse::Ok().body("Ok");
}
// Parse the JSON
let wh: serde_json::Value = match serde_json::from_str(&contents) {
Ok(o) => o,
Err(e) => return HttpResponse::BadRequest().body(format!("invalid json data: {}", e.description())),
};
// Ensure we have a "Secret" parameter, and that it matches the config.
let found_secret = wh.get("secret");
if found_secret.is_none() {
return HttpResponse::Unauthorized().body("invalid secret");
}
let found_secret = found_secret.unwrap();
if found_secret != &config.secret {
return HttpResponse::Unauthorized().body("invalid secret");
}
// Ensure the repo is valid.
let repository = wh.get("repository");
if repository.is_none() {
eprintln!("No 'repository' key found");
return HttpResponse::PreconditionFailed().body("no 'repository' key found");
}
let repository = repository.unwrap();
// Ensure there's an HTML URL that's a valid string
let html_url = match repository.get("html_url") {
None => {
eprintln!("No HTML URL found");
return HttpResponse::PreconditionFailed().body("no repository html_url found");
}
Some(s) => match s.as_str() {
None => {
eprintln!("HTML URL isn't a string");
return HttpResponse::PreconditionFailed().body("html_url is not a string");
}
Some(s) => s,
},
};
// Check to make sure the repo prefix is one we recognize
let mut found_match = false;
for prefix in &config.repo_prefixes {
if html_url.starts_with(prefix) {
found_match = true;
break;
}
}
if !found_match {
eprintln!("URL doesn't start with match");
return HttpResponse::PreconditionFailed().body("url doesn't start with a recognized prefix");
}
// And ensure the URL is a string
let website_url: http::uri::Uri = match repository.get("website") {
None => {
eprintln!("The website isn't configured for this repo");
return HttpResponse::PreconditionFailed().body("this repo has no website configured");
}
Some(s) => match s.as_str() {
None => {
eprintln!("Website URL isn't a string");
return HttpResponse::PreconditionFailed().body("repo's website url is not a string");
}
Some(s) => match s.parse() {
Ok(u) => u,
Err(e) => {
eprintln!("Unable to parse URL: {:?}", e);
return HttpResponse::PreconditionFailed().body("repo's url is unparseable");
}
},
},
};
let target_path = match website_url.path().split("/").last() {
Some(s) => {
if s.is_empty() || s == "." || s == ".." {
eprintln!("No website URL path was found -- defaulting to \"current\"");
"current".to_owned()
} else {
s.to_string()
}
}
None => {
eprintln!("No website URL path was found");
return HttpResponse::PreconditionFailed().body("no website url path could be found");
}
};
eprintln!("Final path: {:?} / {:?}", config.repo_root, target_path);
if let Err(e) = redeploy_repo(html_url, &config.repo_root, &target_path) {
eprintln!("unable to clone/update repo: {}", e.description());
return HttpResponse::NotModified().body("unable to clone/update repo");
}
HttpResponse::Ok().body("Ok") // <- send response
}
/// websocket connection is long running connection, it easier
/// to handle with an actor
struct MyWebSocket {
// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
// otherwise we drop connection.
//hb: Instant,
}
impl actix::prelude::Actor for MyWebSocket {
type Context = ws::WebsocketContext<Self, Arc<Mutex<WebHookConfig>>>;
/// Method is called on actor start. We start the heartbeat process here.
fn started(&mut self, ctx: &mut Self::Context) {
//self.hb(ctx);
}
}
/// Handler for `ws::Message`
impl actix::prelude::StreamHandler<ws::Message, ws::ProtocolError> for MyWebSocket {
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
// process websocket messages
println!("WS: {:?}", msg);
match msg {
ws::Message::Ping(msg) => {
// self.hb = Instant::now();
ctx.pong(&msg);
}
ws::Message::Pong(_) => {
// self.hb = Instant::now();
}
ws::Message::Text(text) => ctx.text(text),
ws::Message::Binary(bin) => ctx.binary(bin),
ws::Message::Close(_) => {
ctx.stop();
}
}
}
}
impl MyWebSocket {
fn new() -> Self {
Self { /*hb: Instant::now()*/ }
}
/*
/// helper method that sends ping to client every second.
///
/// also this method checks heartbeats from client
fn hb(&self, ctx: &mut <Self as actix::prelude::Actor>::Context) {
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
// check client heartbeats
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
// heartbeat timed out
println!("Websocket Client heartbeat failed, disconnecting!");
// stop actor
ctx.stop();
// don't try to send a ping
return;
}
ctx.ping("");
});
}
*/
}
/// do websocket handshake and start `MyWebSocket` actor
fn ws_index(r: &HttpRequest<Arc<Mutex<WebHookConfig>>>) -> Result<HttpResponse, actix_web::Error> {
//ws::start(r, MyWebSocket::new())
Ok(HttpResponse::Ok().body("go ahead"))
}
fn main() {
env::set_var("RUST_LOG", "actix_web=debug");
env::set_var("RUST_BACKTRACE", "1");
env_logger::init();
let hostaddr = "0.0.0.0:9119".to_owned();
let config = WebHookConfig {
secret: "1234".to_owned(),
repo_root: Path::new("D:\\Code\\talkserved").to_path_buf(),
repo_prefixes: vec!["https://git.xobs.io/xobs".to_owned()],
};
println!("Doc server running on http://{}", hostaddr);
let repo_root_copy = Arc::new(Mutex::new(config.repo_root.clone()));
let config = Arc::new(Mutex::new(config));
let sys = actix::System::new("basic-example");
server::new(move || {
App::with_state(config.clone())
// enable logger
.middleware(middleware::Logger::default())
// websocket route
.resource("/socket.io/", |r| r.method(actix_web::http::Method::GET).f(ws_index))
// Our webhook
.resource("/webhook", |r|
r.method(actix_web::http::Method::POST)
.with_config(webhook_handler, |((cfg, _),)| {
cfg.limit(16384);
})
)
// static files
.handler("/", fs::StaticFiles::new(&*repo_root_copy.lock().unwrap().clone()).unwrap().index_file("index.html"))
})
.bind(hostaddr.clone()).expect(&format!("Can not bind to {}", hostaddr))
.shutdown_timeout(0) // <- Set shutdown timeout to 0 seconds (default 60s)
.start();
let _ = sys.run();
}