0.5.0: Advanced agent matching

Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
This commit is contained in:
Ivan Bushchik 2023-12-21 17:04:44 +03:00
parent b7c96c8b4e
commit e84e1a3317
No known key found for this signature in database
GPG key ID: 2F16FBF3262E090C
5 changed files with 137 additions and 105 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "urouter" name = "urouter"
version = "0.4.0" version = "0.5.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/ivabus/urouter" repository = "https://github.com/ivabus/urouter"
@ -14,3 +14,4 @@ serde_json = "1.0.108"
url-escape = "0.1.1" url-escape = "0.1.1"
smurf = "0.3.0" smurf = "0.3.0"
clap = { version = "4.4.11", features = ["derive"] } clap = { version = "4.4.11", features = ["derive"] }
regex = "1.10.2"

View file

@ -16,28 +16,31 @@ Edit `alias.json` (or any other JSON file, check `--alias-file` option) and `car
```json ```json
[ [
{ {
"uri": "uri", "uri":"/",
"alias": { "alias": {
"file": "somefile" "url": "https://somecoolwebsite"
} }
}, },
{ {
"uri": "uri2", "uri":"/",
"alias": { "alias": {
"url": "http://example.com" "file": "somecoolscript"
}
},
{
"uri": "/",
"alias": {
"url": "https://somecoolscript.sh"
}, },
"curl_only": true "agent": {
"regex": "^curl/[0-9].[0-9].[0-9]$",
"only_matching": false
}
},
{
"uri":"/text",
"alias": {
"text": "sometext"
}
} }
] ]
``` ```
`"curl_only"` thing for `curl https://url | sh` like scripts. Agent matching made for `curl https://url | sh` like scripts.
## License ## License

View file

@ -1,9 +0,0 @@
[default]
address = "127.0.0.1"
port = 8080
keep_alive = 0
ident = "urouter"
ip_header = false
log_level = "normal"
temp_dir = "/tmp"
cli_colors = true

View file

@ -22,93 +22,26 @@
* DEALINGS IN THE SOFTWARE. * DEALINGS IN THE SOFTWARE.
*/ */
mod structs;
use structs::*;
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
use rocket::http::Status; use rocket::http::Status;
use std::cell::OnceCell; use std::cell::{OnceCell, RefCell};
use std::ffi::OsStr; use std::collections::HashMap;
use std::net::IpAddr;
use std::path::PathBuf; use std::path::PathBuf;
use rocket::request::{FromRequest, Outcome};
use rocket::response::content::RawText; use rocket::response::content::RawText;
use rocket::response::{Redirect, Responder}; use rocket::response::Redirect;
use rocket::Request;
use serde::Deserialize;
use clap::Parser; use clap::Parser;
use regex::Regex;
use rocket::figment::Figment; use rocket::figment::Figment;
static mut ALIAS: OnceCell<Vec<Alias>> = OnceCell::new(); static mut ALIAS: OnceCell<Vec<Alias>> = OnceCell::new();
static mut COMPILED_REGEXES: RefCell<Option<HashMap<String, Regex>>> = RefCell::new(None);
#[derive(Parser, Debug)]
#[command(about, author)]
struct Args {
#[arg(long, default_value = "./alias.json")]
alias_file: PathBuf,
/// For internal usage
#[arg(long, default_value = "false")]
alias_file_is_set_not_a_list: bool,
/// Dir to lookup file alias
#[arg(long, default_value = ".")]
dir: PathBuf,
#[arg(short, long, default_value = "127.0.0.1")]
address: IpAddr,
#[arg(short, long, default_value = "8080")]
port: u16,
}
// For better compatability with Nix (with set on the top of alias.json instead of a list)
#[derive(Deserialize, Clone, Debug)]
struct NixJson {
alias: Vec<Alias>,
}
#[derive(Deserialize, Clone, Debug)]
struct Alias {
uri: String,
alias: AliasType,
curl_only: Option<bool>,
}
#[derive(Deserialize, Clone, Debug)]
enum AliasType {
#[serde(alias = "url")]
Url(String),
#[serde(alias = "file")]
File(String),
#[serde(alias = "text")]
Text(String),
}
#[derive(Responder)]
enum Response {
Text(RawText<String>),
Redirect(Redirect),
Status(Status),
}
struct UserAgent(String);
#[derive(Debug)]
enum UserAgentError {}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for UserAgent {
type Error = UserAgentError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match req.headers().get_one("user-agent") {
Some(key) => Outcome::Success(UserAgent(key.to_string())),
_ => Outcome::Success(UserAgent("".to_string())),
}
}
}
fn get_return(alias: &Alias) -> Response { fn get_return(alias: &Alias) -> Response {
let args = Args::parse(); let args = Args::parse();
@ -129,20 +62,40 @@ fn get_page(page: String, user_agent: UserAgent) -> Response {
url_escape::decode_to_string(page, &mut decoded_page); url_escape::decode_to_string(page, &mut decoded_page);
let alias = unsafe { ALIAS.get().unwrap() }; let alias = unsafe { ALIAS.get().unwrap() };
let mut pages = Vec::new(); let mut pages = Vec::new();
let curl_check = user_agent.0.contains("curl");
for i in alias { for i in alias {
if i.uri == decoded_page { if i.uri == decoded_page {
if (i.curl_only == Some(true)) == curl_check.clone() { match &i.agent {
return get_return(i); Some(agent) => unsafe {
}; let re = if let Some(regexes) = COMPILED_REGEXES.get_mut() {
match regexes.get(&agent.regex) {
Some(re) => re.clone(),
None => {
let re = Regex::new(&agent.regex).unwrap();
regexes.insert(agent.regex.clone(), re.clone());
re.clone()
}
}
} else {
// guaranteed to be initialized at the beginning
unreachable!()
};
if re.is_match(&user_agent.0) {
return get_return(&i);
}
if let Some(true) = agent.only_matching {
continue;
}
},
_ => {}
}
pages.push(i); pages.push(i);
} }
} }
// Returning normal page (if found) to curl users. // Returning normal page (if found) to curl users.
for i in pages { if pages.len() != 0 {
if i.curl_only != Some(true) { return get_return(pages[0]);
return get_return(i);
}
} }
Response::Status(Status::NotFound) Response::Status(Status::NotFound)
} }
@ -164,6 +117,7 @@ async fn main() -> Result<(), rocket::Error> {
}; };
unsafe { unsafe {
ALIAS.set(alias).unwrap(); ALIAS.set(alias).unwrap();
*COMPILED_REGEXES.get_mut() = Some(HashMap::new());
} }
let figment = Figment::from(rocket::Config::default()) let figment = Figment::from(rocket::Config::default())

83
src/structs.rs Normal file
View file

@ -0,0 +1,83 @@
use clap::Parser;
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use rocket::response::content::RawText;
use rocket::response::Redirect;
use rocket::Request;
use serde::Deserialize;
use std::net::IpAddr;
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(about, author)]
pub struct Args {
#[arg(long, default_value = "./alias.json")]
pub alias_file: PathBuf,
/// For internal usage
#[arg(long, default_value = "false")]
pub alias_file_is_set_not_a_list: bool,
/// Dir to lookup file alias
#[arg(long, default_value = ".")]
pub dir: PathBuf,
#[arg(short, long, default_value = "127.0.0.1")]
pub address: IpAddr,
#[arg(short, long, default_value = "8080")]
pub port: u16,
}
// For better compatability with Nix (with set on the top of alias.json instead of a list)
#[derive(Deserialize, Clone, Debug)]
pub struct NixJson {
pub alias: Vec<Alias>,
}
#[derive(Deserialize, Clone, Debug)]
pub struct Alias {
pub uri: String,
pub alias: AliasType,
pub agent: Option<Agent>,
}
#[derive(Deserialize, Clone, Debug)]
pub enum AliasType {
#[serde(alias = "url")]
Url(String),
#[serde(alias = "file")]
File(String),
#[serde(alias = "text")]
Text(String),
}
#[derive(Deserialize, Clone, Debug)]
pub struct Agent {
pub regex: String,
pub only_matching: Option<bool>,
}
#[derive(Responder)]
pub enum Response {
Text(RawText<String>),
Redirect(Redirect),
Status(Status),
}
pub struct UserAgent(pub String);
#[derive(Debug)]
pub enum UserAgentError {}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for UserAgent {
type Error = UserAgentError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match req.headers().get_one("user-agent") {
Some(key) => Outcome::Success(UserAgent(key.to_string())),
_ => Outcome::Success(UserAgent("".to_string())),
}
}
}