Compare commits

..

4 commits

Author SHA1 Message Date
0451d494c5
0.7.0: Ununsafe urouter
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2024-03-17 15:47:34 +03:00
653b41a67f
0.6.1: Add "html" alias type, fix --alias-file
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2024-01-21 08:36:30 +03:00
9cfa1cf950
0.6.0: change alias.json path logic
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2023-12-23 07:04:36 +03:00
82ddc180cb
0.5.3: create "external" alias type
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2023-12-22 17:08:20 +03:00
6 changed files with 175 additions and 105 deletions

2
.gitignore vendored
View file

@ -1 +1,3 @@
/target /target
.idea/
*.DS_Store

View file

@ -1,6 +1,6 @@
[package] [package]
name = "urouter" name = "urouter"
version = "0.5.2" version = "0.7.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/ivabus/urouter" repository = "https://github.com/ivabus/urouter"
@ -9,9 +9,11 @@ description = "Small HTTP router"
[dependencies] [dependencies]
rocket = "0.5.0" rocket = "0.5.0"
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.108" serde_json = "1.0.114"
url-escape = "0.1.1" url-escape = "0.1.1"
smurf = "0.3.0" once_cell = "1.19"
clap = { version = "4.4.11", features = ["derive"] } clap = { version = "4.5.3", features = ["derive"] }
regex = "1.10.2" regex = "1.10.3"
ureq = { version = "2.9.6", features = ["brotli", "native-certs"] }
users = "0.11.0"

View file

@ -16,18 +16,20 @@ JSON file with array of sets (or set with one field of arrays of sets with `--al
Each set contains 2 necessary elements and 1 optional. Each set contains 2 necessary elements and 1 optional.
- Necessary - `uri` (string) - of URL after host (e.g., `/`, `some/cool/path`, should not start with `/` (only for root))
- `uri` (string) - of url after host (e.g., `/`, `some/cool/path`, should not start with `/` (only for root)) - `alias` (set) - set of one field
- `alias` (set) - set of one field - `url` (string) - redirect to URL with HTTP 303 See Other
- `url` (string) - redirect to url with HTTP 303 See Other - `file` (string) - read file from path `--dir/file` where `--dir` is option (default: `.`, see `--help`) and respond with HTTP 200 OK with `content-type: text/plain`
- `file` (string) - read file from path `--dir/file` where `--dir` is option (default: `.`, see `--help`) and respond with HTTP 200 OK with `content-type: text/plain` - `text` (string) - plain text with HTTP 200 OK with `content-type: text/plain`
- `text` (string) - plain text HTTP 200 OK with `content-type: text/plain` - `html` (string) - plain text with HTTP 200 OK with `content-type: text/html`
- Optional - `external` (set) - download (every time) file using `ureq` HTTP library and response with contents of downloaded resource with HTTP 200 OK and extracted `content-type` from response
- `agent` (set) - set of one necessary field and one optional - `url` (string) - URL to download
- `regex` (string) - regular expression to match user-agent HTTP header - `headers` (set, optional) - headers to include with request
- `only_matching` (bool, optional, false by default) - if false whole alias will be visible for any user agent, if true only for regex matched - `agent` (set, optional) - set of one necessary field and one optional
- `regex` (string) - regular expression to match user-agent HTTP header
- `only_matching` (bool, optional, false by default) - if false whole alias will be visible for any user agent, if true only for regex matched
#### Set of array of sets #### Set of array of sets (use only for very specific workarounds)
```json ```json
{ {
@ -67,12 +69,31 @@ Each set contains 2 necessary elements and 1 optional.
"alias": { "alias": {
"text": "sometext" "text": "sometext"
} }
},
{
"uri": "external",
"alias": {
"external": {
"url": "https://somecool.external.link",
"headers": {
"user-agent": "curl/8.6.0"
}
}
}
} }
] ]
``` ```
Agent matching made for `curl https://url | sh` like scripts. Agent matching made for `curl https://url | sh` like scripts.
## `alias.json` location
- Passed with `--alias_file`, will look up to this path, if file doesn't exist (or no access to it) will panic
- If urouter started with privileges (EUID = 0), file would be `/etc/urouter/alias.json`
- Otherwise if `XDG_CONFIG_HOME` is set, file would be `$XDG_CONFIG_HOME/urouter/alias.json`
- Otherwise if `$HOME` is set, file would be `$HOME/.config/urouter/alias.json`
- If not matched any, will panic and exit
## License ## License
The project is licensed under the terms of the [MIT license](./LICENSE). The project is licensed under the terms of the [MIT license](./LICENSE).

View file

@ -17,11 +17,22 @@
{ {
"uri":"text", "uri":"text",
"alias": { "alias": {
"text": "sometext" "html": "sometext"
}, },
"agent": { "agent": {
"regex": "^curl/[0-9].[0-9].[0-9]$", "regex": "^curl/[0-9].[0-9].[0-9]$",
"only_matching": true "only_matching": true
} }
},
{
"uri": "external",
"alias": {
"external": {
"url": "https://iva.bz",
"headers": {
"user-agent": "curl/8.6.0"
}
}
}
} }
] ]

View file

@ -1,49 +1,67 @@
/* SPDX-License-Identifier: MIT */
/* /*
* MIT License Global comments:
* - I'm ok with unwrapping because if unwrap fails Rocket will automatically return
* Copyright (c) 2023 Ivan Bushchik 500 Internal Server Error
* */
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"), #![forbid(unsafe_code)]
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
mod structs; mod structs;
use structs::*; use structs::*;
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
use rocket::http::Status; use std::io::Write;
use std::cell::{OnceCell, RefCell}; use std::sync::Arc;
use std::collections::HashMap; use std::{collections::HashMap, path::PathBuf, time::Instant};
use std::hint::unreachable_unchecked;
use std::path::PathBuf;
use std::time::Instant;
use rocket::response::content::RawText; use rocket::http::{ContentType, Status};
use rocket::response::Redirect; use rocket::{
figment::Figment,
response::{content::RawHtml, content::RawText, Redirect},
};
use clap::Parser; use clap::Parser;
use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use rocket::figment::Figment;
static mut ALIAS: OnceCell<Vec<Alias>> = OnceCell::new(); static ALIAS: Lazy<Arc<Vec<Alias>>> = Lazy::new(|| {
static mut COMPILED_REGEXES: RefCell<Option<HashMap<String, Regex>>> = RefCell::new(None); let mut args = Args::parse();
if args.alias_file.is_none() {
args.alias_file = Some(get_config_file_location());
}
let file = std::fs::File::open(args.alias_file.unwrap()).unwrap();
let alias: Vec<Alias> = if args.alias_file_is_set_not_a_list {
serde_json::from_reader::<std::fs::File, NixJson>(file).unwrap().alias
} else {
serde_json::from_reader::<std::fs::File, Vec<Alias>>(file).unwrap()
};
Arc::new(alias)
});
static COMPILED_REGEXES: Lazy<Arc<HashMap<String, Regex>>> = Lazy::new(|| {
let compilation_start = Instant::now();
let mut regexes_len = 0;
// Precompile all regexes
let mut compiled_regexes: HashMap<String, Regex> = HashMap::new();
for i in &**ALIAS {
if let Some(agent) = &i.agent {
compiled_regexes.insert(agent.regex.clone(), Regex::new(&agent.regex).unwrap());
regexes_len += 1;
}
}
if regexes_len != 0 {
println!(
"Compiled {} regexes in {} ms",
regexes_len,
(Instant::now() - compilation_start).as_secs_f64() * 1000.0
);
}
Arc::new(compiled_regexes)
});
fn get_return(alias: &Alias) -> Response { fn get_return(alias: &Alias) -> Response {
let args = Args::parse(); let args = Args::parse();
@ -52,9 +70,22 @@ fn get_return(alias: &Alias) -> Response {
AliasType::Url(url) => Response::Redirect(Box::from(Redirect::to(url.clone()))), AliasType::Url(url) => Response::Redirect(Box::from(Redirect::to(url.clone()))),
AliasType::File(path) => { AliasType::File(path) => {
dir.push(&PathBuf::from(&path)); dir.push(&PathBuf::from(&path));
Response::Text(RawText(smurf::io::read_file_str(&dir).unwrap())) Response::Text(Box::new(RawText(std::fs::read_to_string(&dir).unwrap())))
}
AliasType::Text(text) => Response::Text(Box::new(RawText(text.clone()))),
AliasType::Html(html) => Response::Html(Box::new(RawHtml(html.clone()))),
AliasType::External(source) => {
let mut request = ureq::get(&source.url);
for (header, value) in &source.headers {
request = request.set(header, value);
}
let result = request.call().unwrap();
let ct = result.content_type();
Response::Custom(Box::new((
ContentType::parse_flexible(ct).unwrap(),
RawText(result.into_string().unwrap()),
)))
} }
AliasType::Text(text) => Response::Text(RawText(text.clone())),
} }
} }
@ -62,29 +93,22 @@ fn get_return(alias: &Alias) -> Response {
fn get_page(page: &str, user_agent: UserAgent) -> Response { fn get_page(page: &str, user_agent: UserAgent) -> Response {
let mut decoded_page = String::new(); let mut decoded_page = String::new();
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 mut pages = Vec::new(); let mut pages = Vec::new();
for i in alias { for i in &**ALIAS {
if i.uri == decoded_page { if i.uri == decoded_page {
if let Some(agent) = &i.agent { if let Some(agent) = &i.agent {
unsafe { let regexes = &COMPILED_REGEXES;
let regexes = COMPILED_REGEXES.get_mut(); let re = regexes.get(&agent.regex).unwrap();
let re = if let Some(r) = regexes {
// Unwrapping safely, guaranteed to be generated during initialization
r.get(&agent.regex).unwrap()
} else {
unreachable_unchecked()
};
if re.is_match(&user_agent.0) { if re.is_match(&user_agent.0) {
return get_return(i); return get_return(i);
} }
if let Some(true) = agent.only_matching { if let Some(true) = agent.only_matching {
continue; continue;
}
} }
} }
pages.push(i); pages.push(i);
} }
} }
@ -100,37 +124,31 @@ async fn index(user_agent: UserAgent) -> Response {
get_page("/", user_agent) get_page("/", user_agent)
} }
fn get_config_file_location() -> PathBuf {
if users::get_effective_uid() == 0 {
return "/etc/urouter/alias.json".parse().unwrap();
}
if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME") {
return format!("{}/urouter/alias.json", config_home).parse().unwrap();
}
if let Ok(home) = std::env::var("HOME") {
return format!("{}/.config/urouter/alias.json", home).parse().unwrap();
}
if !std::path::Path::new("alias.json").exists() {
let mut file = std::fs::File::create("alias.json").unwrap();
file.write_all(b"[]").unwrap();
}
"alias.json".parse().unwrap()
}
#[rocket::main] #[rocket::main]
async fn main() -> Result<(), rocket::Error> { async fn main() -> Result<(), rocket::Error> {
let args = Args::parse(); let args = Args::parse();
let file = std::fs::File::open(args.alias_file).unwrap(); let _alias = &**ALIAS;
let alias: Vec<Alias> = if args.alias_file_is_set_not_a_list { let _regex = &**COMPILED_REGEXES;
serde_json::from_reader::<std::fs::File, NixJson>(file).unwrap().alias
} else {
serde_json::from_reader::<std::fs::File, Vec<Alias>>(file).unwrap()
};
unsafe {
ALIAS.set(alias).unwrap();
let compilation_start = Instant::now();
let mut regexes_len = 0;
// Precompile all regexes
let mut compiled_regexes: HashMap<String, Regex> = HashMap::new();
for i in ALIAS.get().unwrap() {
if let Some(agent) = &i.agent {
compiled_regexes.insert(agent.regex.clone(), Regex::new(&agent.regex).unwrap());
regexes_len += 1;
}
}
if regexes_len != 0 {
println!(
"Compiled {} regexes in {} ms",
regexes_len,
(Instant::now() - compilation_start).as_secs_f64() * 1000.0
);
}
*COMPILED_REGEXES.get_mut() = Some(compiled_regexes);
}
let figment = Figment::from(rocket::Config::default()) let figment = Figment::from(rocket::Config::default())
.merge(("ident", format!("urouter/{}", env!("CARGO_PKG_VERSION")))) .merge(("ident", format!("urouter/{}", env!("CARGO_PKG_VERSION"))))

View file

@ -1,18 +1,21 @@
/* SPDX-License-Identifier: MIT */
use clap::Parser; use clap::Parser;
use rocket::http::Status; use rocket::http::{ContentType, Status};
use rocket::request::{FromRequest, Outcome}; use rocket::request::{FromRequest, Outcome};
use rocket::response::content::RawText; use rocket::response::content::{RawHtml, RawText};
use rocket::response::Redirect; use rocket::response::{Redirect, Responder};
use rocket::Request; use rocket::Request;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap;
use std::net::IpAddr; use std::net::IpAddr;
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(about, author)] #[command(about, author)]
pub struct Args { pub struct Args {
#[arg(long, default_value = "./alias.json")] #[arg(long)]
pub alias_file: PathBuf, pub alias_file: Option<PathBuf>,
/// For internal usage /// For internal usage
#[arg(long, default_value = "false")] #[arg(long, default_value = "false")]
@ -50,6 +53,10 @@ pub enum AliasType {
File(String), File(String),
#[serde(alias = "text")] #[serde(alias = "text")]
Text(String), Text(String),
#[serde(alias = "html")]
Html(String),
#[serde(alias = "external")]
External(External),
} }
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, Clone, Debug)]
@ -58,11 +65,20 @@ pub struct Agent {
pub only_matching: Option<bool>, pub only_matching: Option<bool>,
} }
#[derive(Deserialize, Clone, Debug)]
pub struct External {
pub url: String,
#[serde(default)]
pub headers: HashMap<String, String>,
}
#[derive(Responder)] #[derive(Responder)]
pub enum Response { pub enum Response {
Text(RawText<String>), Text(Box<RawText<String>>),
Html(Box<RawHtml<String>>),
Redirect(Box<Redirect>), Redirect(Box<Redirect>),
Status(Status), Status(Status),
Custom(Box<(ContentType, RawText<String>)>),
} }
pub struct UserAgent(pub String); pub struct UserAgent(pub String);