commit 7a847fe028d3bf321aca746ef9fe6b367dcd6f54 Author: Ivan Bushchik Date: Sat Jun 3 17:18:57 2023 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..255cb84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +access_keys +alias.json +.idea +*.DS_Store +Cargo.lock \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..32e806d --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,9 @@ +edition = "2021" +hard_tabs = true +merge_derives = true +reorder_imports = true +reorder_modules = true +use_field_init_shorthand = true +use_small_heuristics = "Off" +wrap_comments = true +comment_width = 80 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6d55d5c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "aliurl" +version = "0.1.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/ivabus/aliurl" +description = "Small aliaser for URLs" + + +[dependencies] +rocket = "0.5.0-rc.3" +serde = { version = "1.0.163", features = ["derive"] } +serde_json = "1.0.96" +url-escape = "0.1.1" +uuid = { version = "1.3.3", features = ["v4"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e5e9014 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2023 Ivan Bushchik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), 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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd80c74 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# aliurl + +> ALIaser for URLs + +Small http service to create aliases for URLs. + +## Installation + +```shell +git clone https://github.com/ivabus/aliurl +cd aliurl +cargo b -r +``` + +### Configuration + +Add your access_keys to `./access_keys` or don't add any, if you don't want to use authorization. + +Edit `Rocket.toml` to set port and ip. + +### Running + +```shell +cargo run -r +``` + +## Usage + +### Create new alias + +#### Request + +```http request +POST /post HTTP/1.1 +``` + +#### Request body + +```json +{ + "url": "", + "alias": "", // If not provided, UUID will be generated + "access_key": "" // May not be provided, if no ./access_keys file +} +``` + +### Use alias + +```http request +GET / HTTP/1.1 +``` + +```http request +HTTP/1.1 303 See Other +location: +``` + +### Alias for `/` + +Aliases for root is declared in `src/main.rs` file in `INDEX_REDIRECT` const. + +## License + +The project is licensed under the terms of the [MIT license](./LICENSE). \ No newline at end of file diff --git a/Rocket.toml b/Rocket.toml new file mode 100644 index 0000000..cd6b578 --- /dev/null +++ b/Rocket.toml @@ -0,0 +1,11 @@ +[release] +address = "127.0.0.1" +port = 8080 +workers = 8 +keep_alive = 5 +ident = "Rocket" +ip_header = "X-Real-IP" # set to `false` to disable +log_level = "normal" +temp_dir = "/tmp" +cli_colors = true +ctrlc = false diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a90b8ce --- /dev/null +++ b/src/main.rs @@ -0,0 +1,139 @@ +#[macro_use] +extern crate rocket; + +use std::io::prelude::*; +use std::io::BufReader; + +use rocket::http::RawStr; +use rocket::http::Status; +use rocket::response::Redirect; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize)] +struct CreateAliasRequest { + url: String, + access_key: Option, + alias: Option, +} + +static mut ACCESS_KEY_REQUIRED: bool = true; +const INDEX_REDIRECT: &'static str = "https://ivabus.dev"; + +#[derive(Deserialize, Serialize, Clone)] +struct Alias { + url: String, + alias: String, +} + +fn read_alias() -> Vec { + if !std::path::Path::new("./alias.json").exists() { + let mut file = std::fs::File::create("./alias.json").unwrap(); + file.write_all(b"[]").unwrap(); + return vec![]; + } + if std::fs::File::open("./alias.json").unwrap().metadata().unwrap().len() == 0 { + let mut file = std::fs::File::options().write(true).open("./alias.json").unwrap(); + file.write_all(b"[]").unwrap(); + return vec![]; + } + let file = std::fs::File::open("./alias.json").unwrap(); + let mut buf_reader = BufReader::new(file); + let mut contents = String::new(); + buf_reader.read_to_string(&mut contents).unwrap(); + let alias_list: Vec = serde_json::from_str(&contents).unwrap(); + alias_list +} + +#[post("/post", data = "")] +fn create_alias(data: &RawStr) -> (Status, String) { + let data: CreateAliasRequest = match serde_json::from_str(&data.to_string()) { + Ok(req) => req, + Err(e) => return (Status::BadRequest, format!("Error: {e}")), + }; + let mut file = std::fs::File::open("./access_keys").unwrap(); + let mut buffer: String = String::new(); + file.read_to_string(&mut buffer).unwrap(); + let access_keys: Vec<&str> = buffer.split("\n").collect(); + if let Some(key) = data.access_key { + if !access_keys.contains(&key.as_str()) { + return (Status::Forbidden, "Access key is invalid".to_string()); + } + } else { + unsafe { + if ACCESS_KEY_REQUIRED { + return (Status::Forbidden, "Access key needs to be provided".to_string()); + } + } + }; + + let mut alias_list = read_alias(); + let mut file = std::fs::File::options().write(true).open("./alias.json").unwrap(); + let alias = match data.alias { + None => uuid::Uuid::new_v4().to_string(), + Some(alias) => alias, + }; + if alias.contains("?") { + return (Status::BadRequest, format!("Error: alias should not contain '?'")); + } + alias_list.push(Alias { + url: data.url.clone(), + alias: alias.clone(), + }); + alias_list.dedup_by(|a, b| a.alias == b.alias); + + file.write_all(serde_json::to_string(&alias_list).unwrap().as_bytes()).unwrap(); + + file.sync_all().unwrap(); + return (Status::Ok, format!("Created {} at {}", data.url, alias)); +} + +#[get("/404")] +fn not_found() -> Status { + Status::NotFound +} + +#[get("/")] +async fn get_page(page: String) -> Redirect { + let mut decoded_page = String::new(); + url_escape::decode_to_string(page, &mut decoded_page); + let alias_list = read_alias(); + for i in alias_list { + if i.alias == decoded_page { + return Redirect::to(i.url); + } + } + Redirect::to("/404") +} + +#[get("/")] +async fn get_index() -> Redirect { + Redirect::to(INDEX_REDIRECT) +} + +#[rocket::main] +async fn main() -> Result<(), rocket::Error> { + if !std::path::Path::new("./access_keys").exists() { + eprintln!("No ./access_keys found. Falling back to no authorization"); + eprintln!("Continue? (press enter or ctrl-c to exit)"); + let mut s = String::new(); + std::io::stdin().read_line(&mut s).unwrap(); + unsafe { + ACCESS_KEY_REQUIRED = false; + } + } else if std::fs::File::open("./access_keys").unwrap().metadata().unwrap().len() == 0 { + eprintln!("No keys in ./access_keys found. Falling back to no authorization"); + eprintln!("Continue? (press enter or ctrl-c to exit)"); + let mut s = String::new(); + std::io::stdin().read_line(&mut s).unwrap(); + unsafe { + ACCESS_KEY_REQUIRED = false; + } + } + + let _rocket = rocket::build() + .mount("/", routes![not_found, create_alias, get_page, get_index]) + .launch() + .await?; + + Ok(()) +}