Initial commit.
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
urls.txt
|
||||||
131
Cargo.lock
generated
Normal file
131
Cargo.lock
generated
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.186"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.52.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"pin-project-lite",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urls-txt"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "urls-txt"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.52.3", features = ["io-util", "macros", "net", "rt-multi-thread"] }
|
||||||
64
readme.md
Normal file
64
readme.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# urls-txt
|
||||||
|
|
||||||
|
a _really_ simple url shortener: less than 200 lines of Rust and one dependency (tokio).
|
||||||
|
|
||||||
|
## usage
|
||||||
|
|
||||||
|
0. clone and install:
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://git.alv.cx/alvierahman90/urls-txt.git
|
||||||
|
cd urls-txt
|
||||||
|
cargo install --path .
|
||||||
|
```
|
||||||
|
1. create a list of links:
|
||||||
|
|
||||||
|
```
|
||||||
|
lines that don't start with a slash (/) are ignore (they're comments)
|
||||||
|
shortlink followed by redirect link, separated by a space character
|
||||||
|
/ http://alv.cx everything after the link is ignored (they're comments)
|
||||||
|
/urls-txt http://git.alv.cx/alvierahman90/urls-txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. run:
|
||||||
|
|
||||||
|
```
|
||||||
|
urls-txt path/to/urls.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## why?
|
||||||
|
|
||||||
|
all url shorteners i've come across are too complicated:
|
||||||
|
they have account management and remote APIs and whatnot.
|
||||||
|
too complicated.
|
||||||
|
|
||||||
|
this includes my own [sus url shortener](https://pls.cx/sus),
|
||||||
|
which for some reason i decided should use redis for its database and have a HTTP API for
|
||||||
|
updating it.
|
||||||
|
|
||||||
|
### why not just configure your reverse proxy?
|
||||||
|
|
||||||
|
good question.
|
||||||
|
i'm not sure, but i think i want to separate my concerns?
|
||||||
|
you might just want to configure your reverse proxy instead.
|
||||||
|
|
||||||
|
#### caddy
|
||||||
|
|
||||||
|
```
|
||||||
|
pls.cx {
|
||||||
|
redir / http://alv.cx
|
||||||
|
redir /urls-txt https://git.alv.cx/alvierahman90/urls-txt
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### nginx
|
||||||
|
|
||||||
|
```
|
||||||
|
location / { return 301 http://alv.cx; }
|
||||||
|
location /urls-txt { return 301 https://git.alv.cx/alvierahman90/urls-txt; }
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
##### apache
|
||||||
|
|
||||||
|
there's probably a way ¯\_(ツ)_/¯
|
||||||
116
src/main.rs
Normal file
116
src/main.rs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
use {
|
||||||
|
std::{collections::HashMap, env, sync::Arc},
|
||||||
|
tokio::{
|
||||||
|
io::{AsyncBufReadExt, AsyncWriteExt, BufStream},
|
||||||
|
net::{TcpListener, TcpStream},
|
||||||
|
task::spawn_blocking,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ResponseCode {
|
||||||
|
BadRequest,
|
||||||
|
Forbidden,
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn simple_response(mut socket: BufStream<TcpStream>, rc: ResponseCode) {
|
||||||
|
let rc = match rc {
|
||||||
|
ResponseCode::BadRequest => "400 Bad Request",
|
||||||
|
ResponseCode::Forbidden => "403 Forbidden",
|
||||||
|
ResponseCode::NotFound => "404 Not Found",
|
||||||
|
};
|
||||||
|
let content_length = rc.len();
|
||||||
|
let response = format!(
|
||||||
|
"HTTP/1.1 {rc}\r\nContent-Type: text/plaintext; charset=utf-8\r\nContent-Length: {content_length}\r\nX-Served-By: urls-txt\r\n\r\n{rc}"
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = socket.write(response.as_bytes()).await {
|
||||||
|
println!("process_request: failed: error writing response: {e}");
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = socket.flush().await {
|
||||||
|
println!("process_request: failed: error flushing socket: {e}");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_socket(mut socket: BufStream<TcpStream>, urlmap: Arc<HashMap<String, String>>) {
|
||||||
|
let mut request_line = String::default();
|
||||||
|
if let Err(e) = socket.read_line(&mut request_line).await {
|
||||||
|
println!("process_request: failed to read line: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut request_line = request_line.split(' ');
|
||||||
|
let (Some(http_method), Some(path)) = (request_line.next(), request_line.next()) else {
|
||||||
|
println!("process_request: malformed request");
|
||||||
|
simple_response(socket, ResponseCode::BadRequest).await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if http_method != "GET" {
|
||||||
|
println!("process_request: forbidden method");
|
||||||
|
simple_response(socket, ResponseCode::Forbidden).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(location) = urlmap.get(path) else {
|
||||||
|
println!("process_request: not found");
|
||||||
|
simple_response(socket, ResponseCode::NotFound).await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = format!("<a href=\"{location}\">Found</a>\r\n");
|
||||||
|
let content_length = content.len();
|
||||||
|
let response = format!(
|
||||||
|
"HTTP/1.1 302 Found\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {content_length}\r\nLocation: {location}\r\nX-Served-By: urls-txt\r\n\r\n{content}"
|
||||||
|
);
|
||||||
|
if let Err(e) = socket.write(response.as_bytes()).await {
|
||||||
|
println!("process_request: failed: error writing response: {e}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = socket.flush().await {
|
||||||
|
println!("process_request: failed: error flushing socket: {e}");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() != 2 {
|
||||||
|
println!("USAGE: {} <urls.txt>", args[0]);
|
||||||
|
Err("Incorrect arguments")?
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = spawn_blocking(|| std::fs::read_to_string("urls.txt")).await??;
|
||||||
|
let mut urlmap = HashMap::new();
|
||||||
|
for url_pair in config.split('\n') {
|
||||||
|
if url_pair.len() == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if url_pair.chars().next().expect("String is longer than 1 character") != '/' {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut split = url_pair.split_whitespace();
|
||||||
|
|
||||||
|
let (Some(slug), Some(redirect)) = (split.next(), split.next()) else {
|
||||||
|
Err(format!("Unable to parse line: '{}'", url_pair))?
|
||||||
|
};
|
||||||
|
|
||||||
|
urlmap.insert(slug.to_string(), redirect.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlmap = Arc::new(urlmap);
|
||||||
|
|
||||||
|
println!("Starting TCP Listener...");
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:3000").await?;
|
||||||
|
loop {
|
||||||
|
let (socket, _addr) = listener.accept().await?;
|
||||||
|
let urlmap = urlmap.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
process_socket(BufStream::new(socket), urlmap).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user