Compare commits
6 commits
fb48d77a10
...
b835e5b2e9
Author | SHA1 | Date | |
---|---|---|---|
b835e5b2e9 | |||
0a8af55f05 | |||
062c8f5a2e | |||
b59cb4f9e3 | |||
983ad3733e | |||
0a19214f36 |
7 changed files with 498 additions and 693 deletions
968
Cargo.lock
generated
968
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -9,7 +9,8 @@ readme = "README.md"
|
||||||
repository = "https://github.com/edward-shen/bunbun"
|
repository = "https://github.com/edward-shen/bunbun"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "1.0"
|
actix-web = "2.0"
|
||||||
|
actix-rt = "1.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_yaml = "0.8"
|
serde_yaml = "0.8"
|
||||||
handlebars = "2.0"
|
handlebars = "2.0"
|
||||||
|
|
|
@ -31,7 +31,7 @@ If you're looking to build a release binary, here are the steps I use:
|
||||||
|
|
||||||
1. `cargo build --release`
|
1. `cargo build --release`
|
||||||
2. `strip target/release/bunbun`
|
2. `strip target/release/bunbun`
|
||||||
3. `upx --lzma bunbun`
|
3. `upx --lzma target/release/bunbun`
|
||||||
|
|
||||||
LZMA provides the best level of compress for Rust binaries; it performs at the
|
LZMA provides the best level of compress for Rust binaries; it performs at the
|
||||||
same level as `upx --ultra-brute` without the time cost and [without breaking
|
same level as `upx --ultra-brute` without the time cost and [without breaking
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: "bunbun"
|
name: "bunbun"
|
||||||
about: "Search/jump multiplexer service"
|
about: "Search/jump multiplexer service."
|
||||||
|
|
||||||
args:
|
args:
|
||||||
- verbose:
|
- verbose:
|
||||||
|
|
38
src/error.rs
Normal file
38
src/error.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
|
pub enum BunBunError {
|
||||||
|
IoError(std::io::Error),
|
||||||
|
ParseError(serde_yaml::Error),
|
||||||
|
WatchError(hotwatch::Error),
|
||||||
|
LoggerInitError(log::SetLoggerError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for BunBunError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
BunBunError::IoError(e) => e.fmt(f),
|
||||||
|
BunBunError::ParseError(e) => e.fmt(f),
|
||||||
|
BunBunError::WatchError(e) => e.fmt(f),
|
||||||
|
BunBunError::LoggerInitError(e) => e.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a from implementation from the specified type to the provided
|
||||||
|
/// bunbun error.
|
||||||
|
macro_rules! from_error {
|
||||||
|
($from:ty, $to:ident) => {
|
||||||
|
impl From<$from> for BunBunError {
|
||||||
|
fn from(e: $from) -> Self {
|
||||||
|
BunBunError::$to(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
from_error!(std::io::Error, IoError);
|
||||||
|
from_error!(serde_yaml::Error, ParseError);
|
||||||
|
from_error!(hotwatch::Error, WatchError);
|
||||||
|
from_error!(log::SetLoggerError, LoggerInitError);
|
136
src/main.rs
136
src/main.rs
|
@ -1,6 +1,7 @@
|
||||||
use actix_web::middleware::Logger;
|
use actix_web::middleware::Logger;
|
||||||
use actix_web::{App, HttpServer};
|
use actix_web::{App, HttpServer};
|
||||||
use clap::{crate_authors, crate_version, load_yaml, App as ClapApp};
|
use clap::{crate_authors, crate_version, load_yaml, App as ClapApp};
|
||||||
|
use error::BunBunError;
|
||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
use hotwatch::{Event, Hotwatch};
|
use hotwatch::{Event, Hotwatch};
|
||||||
use libc::daemon;
|
use libc::daemon;
|
||||||
|
@ -8,54 +9,17 @@ use log::{debug, error, info, trace, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
|
||||||
use std::fs::{read_to_string, OpenOptions};
|
use std::fs::{read_to_string, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
mod error;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod template_args;
|
mod template_args;
|
||||||
|
|
||||||
const DEFAULT_CONFIG: &[u8] = include_bytes!("../bunbun.default.yaml");
|
const DEFAULT_CONFIG: &[u8] = include_bytes!("../bunbun.default.yaml");
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
|
||||||
enum BunBunError {
|
|
||||||
IoError(std::io::Error),
|
|
||||||
ParseError(serde_yaml::Error),
|
|
||||||
WatchError(hotwatch::Error),
|
|
||||||
LoggerInitError(log::SetLoggerError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for BunBunError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
BunBunError::IoError(e) => e.fmt(f),
|
|
||||||
BunBunError::ParseError(e) => e.fmt(f),
|
|
||||||
BunBunError::WatchError(e) => e.fmt(f),
|
|
||||||
BunBunError::LoggerInitError(e) => e.fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a from implementation from the specified type to the provided
|
|
||||||
/// bunbun error.
|
|
||||||
macro_rules! from_error {
|
|
||||||
($from:ty, $to:ident) => {
|
|
||||||
impl From<$from> for BunBunError {
|
|
||||||
fn from(e: $from) -> Self {
|
|
||||||
BunBunError::$to(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
from_error!(std::io::Error, IoError);
|
|
||||||
from_error!(serde_yaml::Error, ParseError);
|
|
||||||
from_error!(hotwatch::Error, WatchError);
|
|
||||||
from_error!(log::SetLoggerError, LoggerInitError);
|
|
||||||
|
|
||||||
/// Dynamic variables that either need to be present at runtime, or can be
|
/// Dynamic variables that either need to be present at runtime, or can be
|
||||||
/// changed during runtime.
|
/// changed during runtime.
|
||||||
pub struct State {
|
pub struct State {
|
||||||
|
@ -64,10 +28,10 @@ pub struct State {
|
||||||
groups: Vec<RouteGroup>,
|
groups: Vec<RouteGroup>,
|
||||||
/// Cached, flattened mapping of all routes and their destinations.
|
/// Cached, flattened mapping of all routes and their destinations.
|
||||||
routes: HashMap<String, String>,
|
routes: HashMap<String, String>,
|
||||||
renderer: Handlebars,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), BunBunError> {
|
#[actix_rt::main]
|
||||||
|
async fn main() -> Result<(), BunBunError> {
|
||||||
let yaml = load_yaml!("cli.yaml");
|
let yaml = load_yaml!("cli.yaml");
|
||||||
let matches = ClapApp::from(yaml)
|
let matches = ClapApp::from(yaml)
|
||||||
.version(crate_version!())
|
.version(crate_version!())
|
||||||
|
@ -82,13 +46,11 @@ fn main() -> Result<(), BunBunError> {
|
||||||
// config has default location provided, unwrapping is fine.
|
// config has default location provided, unwrapping is fine.
|
||||||
let conf_file_location = String::from(matches.value_of("config").unwrap());
|
let conf_file_location = String::from(matches.value_of("config").unwrap());
|
||||||
let conf = read_config(&conf_file_location)?;
|
let conf = read_config(&conf_file_location)?;
|
||||||
let renderer = compile_templates();
|
|
||||||
let state = Arc::from(RwLock::new(State {
|
let state = Arc::from(RwLock::new(State {
|
||||||
public_address: conf.public_address,
|
public_address: conf.public_address,
|
||||||
default_route: conf.default_route,
|
default_route: conf.default_route,
|
||||||
routes: cache_routes(&conf.groups),
|
routes: cache_routes(&conf.groups),
|
||||||
groups: conf.groups,
|
groups: conf.groups,
|
||||||
renderer,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Daemonize after trying to read from config and before watching; allow user
|
// Daemonize after trying to read from config and before watching; allow user
|
||||||
|
@ -100,42 +62,12 @@ fn main() -> Result<(), BunBunError> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut watch = Hotwatch::new_with_custom_delay(Duration::from_millis(500))?;
|
let _watch = start_watch(state.clone(), conf_file_location)?;
|
||||||
// TODO: keep retry watching in separate thread
|
|
||||||
// Closures need their own copy of variables for proper lifecycle management
|
|
||||||
let state_ref = state.clone();
|
|
||||||
let conf_file_location_clone = conf_file_location.clone();
|
|
||||||
let watch_result = watch.watch(&conf_file_location, move |e: Event| {
|
|
||||||
if let Event::Write(_) = e {
|
|
||||||
trace!("Grabbing writer lock on state...");
|
|
||||||
let mut state = state.write().unwrap();
|
|
||||||
trace!("Obtained writer lock on state!");
|
|
||||||
match read_config(&conf_file_location_clone) {
|
|
||||||
Ok(conf) => {
|
|
||||||
state.public_address = conf.public_address;
|
|
||||||
state.default_route = conf.default_route;
|
|
||||||
state.routes = cache_routes(&conf.groups);
|
|
||||||
state.groups = conf.groups;
|
|
||||||
info!("Successfully updated active state");
|
|
||||||
}
|
|
||||||
Err(e) => warn!("Failed to update config file: {}", e),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug!("Saw event {:#?} but ignored it", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
match watch_result {
|
|
||||||
Ok(_) => info!("Watcher is now watching {}", &conf_file_location),
|
|
||||||
Err(e) => warn!(
|
|
||||||
"Couldn't watch {}: {}. Changes to this file won't be seen!",
|
|
||||||
&conf_file_location, e
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.data(state_ref.clone())
|
.data(state.clone())
|
||||||
|
.app_data(compile_templates())
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.service(routes::hop)
|
.service(routes::hop)
|
||||||
.service(routes::list)
|
.service(routes::list)
|
||||||
|
@ -143,7 +75,8 @@ fn main() -> Result<(), BunBunError> {
|
||||||
.service(routes::opensearch)
|
.service(routes::opensearch)
|
||||||
})
|
})
|
||||||
.bind(&conf.bind_address)?
|
.bind(&conf.bind_address)?
|
||||||
.run()?;
|
.run()
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -224,6 +157,9 @@ fn read_config(config_file_path: &str) -> Result<Config, BunBunError> {
|
||||||
Ok(serde_yaml::from_str(&config_str)?)
|
Ok(serde_yaml::from_str(&config_str)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a hashmap of routes from the data structure created by the config
|
||||||
|
/// file. This should improve runtime performance and is a better solution than
|
||||||
|
/// just iterating over the config object for every hop resolution.
|
||||||
fn cache_routes(groups: &[RouteGroup]) -> HashMap<String, String> {
|
fn cache_routes(groups: &[RouteGroup]) -> HashMap<String, String> {
|
||||||
let mut mapping = HashMap::new();
|
let mut mapping = HashMap::new();
|
||||||
for group in groups {
|
for group in groups {
|
||||||
|
@ -261,3 +197,51 @@ fn compile_templates() -> Handlebars {
|
||||||
register_template!["index", "list", "opensearch"];
|
register_template!["index", "list", "opensearch"];
|
||||||
handlebars
|
handlebars
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starts the watch on a file, if possible. This will only return an Error if
|
||||||
|
/// the notify library (used by Hotwatch) fails to initialize, which is
|
||||||
|
/// considered to be a more serve error as it may be indicative of a low-level
|
||||||
|
/// problem. If a watch was unsuccessfully obtained (the most common is due to
|
||||||
|
/// the file not existing), then this will simply warn before returning a watch
|
||||||
|
/// object.
|
||||||
|
///
|
||||||
|
/// This watch object should be kept in scope as dropping it releases all
|
||||||
|
/// watches.
|
||||||
|
fn start_watch(
|
||||||
|
state: Arc<RwLock<State>>,
|
||||||
|
config_file_path: String,
|
||||||
|
) -> Result<Hotwatch, BunBunError> {
|
||||||
|
let mut watch = Hotwatch::new_with_custom_delay(Duration::from_millis(500))?;
|
||||||
|
// TODO: keep retry watching in separate thread
|
||||||
|
// Closures need their own copy of variables for proper lifecycle management
|
||||||
|
let config_file_path_clone = config_file_path.clone();
|
||||||
|
let watch_result = watch.watch(&config_file_path, move |e: Event| {
|
||||||
|
if let Event::Write(_) = e {
|
||||||
|
trace!("Grabbing writer lock on state...");
|
||||||
|
let mut state = state.write().unwrap();
|
||||||
|
trace!("Obtained writer lock on state!");
|
||||||
|
match read_config(&config_file_path_clone) {
|
||||||
|
Ok(conf) => {
|
||||||
|
state.public_address = conf.public_address;
|
||||||
|
state.default_route = conf.default_route;
|
||||||
|
state.routes = cache_routes(&conf.groups);
|
||||||
|
state.groups = conf.groups;
|
||||||
|
info!("Successfully updated active state");
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to update config file: {}", e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("Saw event {:#?} but ignored it", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match watch_result {
|
||||||
|
Ok(_) => info!("Watcher is now watching {}", &config_file_path),
|
||||||
|
Err(e) => warn!(
|
||||||
|
"Couldn't watch {}: {}. Changes to this file won't be seen!",
|
||||||
|
&config_file_path, e
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(watch)
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,8 @@ use crate::State;
|
||||||
use actix_web::get;
|
use actix_web::get;
|
||||||
use actix_web::http::header;
|
use actix_web::http::header;
|
||||||
use actix_web::web::{Data, Query};
|
use actix_web::web::{Data, Query};
|
||||||
use actix_web::{HttpResponse, Responder};
|
use actix_web::{HttpRequest, HttpResponse, Responder};
|
||||||
|
use handlebars::Handlebars;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||||
|
@ -11,6 +12,8 @@ use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
type StateData = Data<Arc<RwLock<State>>>;
|
||||||
|
|
||||||
/// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
/// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
||||||
const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
|
const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
|
||||||
.add(b' ')
|
.add(b' ')
|
||||||
|
@ -21,9 +24,18 @@ const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
|
||||||
.add(b'+');
|
.add(b'+');
|
||||||
|
|
||||||
#[get("/ls")]
|
#[get("/ls")]
|
||||||
pub fn list(data: Data<Arc<RwLock<State>>>) -> impl Responder {
|
pub async fn list(
|
||||||
|
data: Data<Arc<RwLock<State>>>,
|
||||||
|
req: HttpRequest,
|
||||||
|
) -> impl Responder {
|
||||||
let data = data.read().unwrap();
|
let data = data.read().unwrap();
|
||||||
HttpResponse::Ok().body(data.renderer.render("list", &data.groups).unwrap())
|
HttpResponse::Ok().body(
|
||||||
|
req
|
||||||
|
.app_data::<Handlebars>()
|
||||||
|
.unwrap()
|
||||||
|
.render("list", &data.groups)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -32,8 +44,9 @@ pub struct SearchQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/hop")]
|
#[get("/hop")]
|
||||||
pub fn hop(
|
pub async fn hop(
|
||||||
data: Data<Arc<RwLock<State>>>,
|
data: StateData,
|
||||||
|
req: HttpRequest,
|
||||||
query: Query<SearchQuery>,
|
query: Query<SearchQuery>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let data = data.read().unwrap();
|
let data = data.read().unwrap();
|
||||||
|
@ -42,8 +55,9 @@ pub fn hop(
|
||||||
(Some(path), args) => HttpResponse::Found()
|
(Some(path), args) => HttpResponse::Found()
|
||||||
.header(
|
.header(
|
||||||
header::LOCATION,
|
header::LOCATION,
|
||||||
data
|
req
|
||||||
.renderer
|
.app_data::<Handlebars>()
|
||||||
|
.unwrap()
|
||||||
.render_template(
|
.render_template(
|
||||||
&path,
|
&path,
|
||||||
&template_args::query(
|
&template_args::query(
|
||||||
|
@ -109,11 +123,12 @@ fn resolve_hop(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub fn index(data: Data<Arc<RwLock<State>>>) -> impl Responder {
|
pub async fn index(data: StateData, req: HttpRequest) -> impl Responder {
|
||||||
let data = data.read().unwrap();
|
let data = data.read().unwrap();
|
||||||
HttpResponse::Ok().body(
|
HttpResponse::Ok().body(
|
||||||
data
|
req
|
||||||
.renderer
|
.app_data::<Handlebars>()
|
||||||
|
.unwrap()
|
||||||
.render(
|
.render(
|
||||||
"index",
|
"index",
|
||||||
&template_args::hostname(data.public_address.clone()),
|
&template_args::hostname(data.public_address.clone()),
|
||||||
|
@ -123,7 +138,7 @@ pub fn index(data: Data<Arc<RwLock<State>>>) -> impl Responder {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/bunbunsearch.xml")]
|
#[get("/bunbunsearch.xml")]
|
||||||
pub fn opensearch(data: Data<Arc<RwLock<State>>>) -> impl Responder {
|
pub async fn opensearch(data: StateData, req: HttpRequest) -> impl Responder {
|
||||||
let data = data.read().unwrap();
|
let data = data.read().unwrap();
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.header(
|
.header(
|
||||||
|
@ -131,8 +146,9 @@ pub fn opensearch(data: Data<Arc<RwLock<State>>>) -> impl Responder {
|
||||||
"application/opensearchdescription+xml",
|
"application/opensearchdescription+xml",
|
||||||
)
|
)
|
||||||
.body(
|
.body(
|
||||||
data
|
req
|
||||||
.renderer
|
.app_data::<Handlebars>()
|
||||||
|
.unwrap()
|
||||||
.render(
|
.render(
|
||||||
"opensearch",
|
"opensearch",
|
||||||
&template_args::hostname(data.public_address.clone()),
|
&template_args::hostname(data.public_address.clone()),
|
||||||
|
|
Loading…
Reference in a new issue