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, } 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>>)) -> 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>>; /// 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 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 ::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>>) -> Result { //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(); }