1
0
Fork 0
mirror of https://github.com/SinTan1729/chhoto-url synced 2024-12-26 23:58:35 -06:00

Merge branch 'rust-backend' into 'main'

Complete rewrite of backend in Rust

See merge request SinTan1729/simply-shorten!3
This commit is contained in:
Sayantan Santra 2023-04-08 20:59:25 +00:00
commit c3aa7438e9
31 changed files with 1877 additions and 794 deletions

View file

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/java">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/resources">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

17
.gitignore vendored
View file

@ -1,12 +1,9 @@
# Ignore Gradle project-specific cache directory # Ignore cargo build outputs
.gradle actix/target
# Ignore Gradle build output directory # Ignore SQLite file
build
.idea/
local.properties
url.iml
urls.csv
.env
urls.sqlite urls.sqlite
# Ignore irrelevant dotfiles
.vscode/
.directory

View file

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>url</name>
<comment>Project url created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<filteredResources>
<filter>
<id>1667542035221</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View file

@ -1,13 +0,0 @@
arguments=--init-script /home/sintan/.config/Code/User/globalStorage/redhat.java/1.12.0/config_linux/org.eclipse.osgi/51/0/.cp/gradle/init/init.gradle --init-script /home/sintan/.config/Code/User/globalStorage/redhat.java/1.12.0/config_linux/org.eclipse.osgi/51/0/.cp/gradle/protobuf/init.gradle
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=/usr/lib/jvm/java-19-openjdk
jvm.arguments=
offline.mode=false
override.workspace.settings=true
show.console.view=true
show.executions.view=true

View file

@ -1,14 +1,27 @@
FROM gradle:jdk17-alpine AS build FROM rust:1 as build
COPY --chown=gradle:gradle . /home/gradle/src RUN cargo install cargo-build-deps
WORKDIR /home/gradle/src
RUN gradle fatJar --no-daemon
FROM openjdk:17-alpine RUN cargo new --bin simply-shorten
WORKDIR /simply-shorten
EXPOSE 4567 COPY ./actix/Cargo.toml .
COPY ./actix/Cargo.lock .
RUN mkdir /app RUN cargo build-deps --release
COPY --from=build /home/gradle/src/build/libs/*.jar /app/application.jar COPY ./actix/src ./src
COPY ./actix/resources ./resources
ENTRYPOINT ["java", "-jar","/app/application.jar"] RUN cargo build --release
FROM frolvlad/alpine-glibc:latest
EXPOSE 2000
RUN apk add sqlite-libs
WORKDIR /opt
COPY --from=build /simply-shorten/target/release/simply-shorten /opt/simply-shorten
COPY --from=build /simply-shorten/resources /opt/resources
CMD ["./simply-shorten"]

View file

@ -1,4 +1,4 @@
# ![Logo](src/main/resources/public/assets/favicon-32.png) <span style="font-size:42px">Simply Shorten</span> # ![Logo](actix/resources/assets/favicon-32.png) <span style="font-size:42px">Simply Shorten</span>
# What is it? # What is it?
A simple selfhosted URL shortener with no unnecessary features. A simple selfhosted URL shortener with no unnecessary features.
@ -28,7 +28,7 @@ unnecessary features, or they didn't have all the features I wanted.
generate short links locally. generate short links locally.
- Links are stored in an SQLite database. - Links are stored in an SQLite database.
- Available as a Docker container. - Available as a Docker container.
- Backend written in Java using [Spark Java](http://sparkjava.com/), frontend - Backend written in Rust using [Actix](https://actix.rs/), frontend
written in plain HTML and vanilla JS, using [Pure CSS](https://purecss.io/) written in plain HTML and vanilla JS, using [Pure CSS](https://purecss.io/)
for styling. for styling.
@ -62,19 +62,10 @@ Clone this repository
``` ```
git clone https://gitlab.com/SinTan1729/simply-shorten git clone https://gitlab.com/SinTan1729/simply-shorten
``` ```
Note that Gradle 6.x.x and JDK 11 are required. Other versions are not tested
### 1. Build the `.jar` file
```
gradle build --no-daemon
```
The `--no-daemon` option means that gradle should exit as soon as the build is
finished. Without it, gradle would still be running in the background
in order to speed up future builds.
### 2. Set environment variables ### 2. Set environment variables
```bash ```bash
# Required for authentication # Required for authentication
export username=<api username>
export password=<api password> export password=<api password>
# Sets where the database exists. Can be local or remote (optional) # Sets where the database exists. Can be local or remote (optional)
export db_url=<url> # Default: './urls.sqlite' export db_url=<url> # Default: './urls.sqlite'
@ -83,9 +74,10 @@ export db_url=<url> # Default: './urls.sqlite'
export site_url=<url> export site_url=<url>
``` ```
### 3. Run it ### 3. Build and run it
``` ```
java -jar build/libs/url.jar cd actix
cargo run
``` ```
You can optionally set the port the server listens on by appending `--port=[port]` You can optionally set the port the server listens on by appending `--port=[port]`
### 4. Navigate to `http://localhost:4567` in your browser, add links as you wish. ### 4. Navigate to `http://localhost:4567` in your browser, add links as you wish.
@ -99,7 +91,6 @@ docker build . -t simply-shorten:latest
1. Run the image 1. Run the image
``` ```
docker run -p 4567:4567 docker run -p 4567:4567
-d url:latest
-e username="username" -e username="username"
-e password="password" -e password="password"
-d simply-shorten:latest -d simply-shorten:latest
@ -127,20 +118,16 @@ docker run -p 4567:4567 \
``` ```
## Disable authentication ## Disable authentication
As requested in #5, it is possible to completely disable the authentication. It's not possible to completely disable authentication. It's rather easy to implement
This if not recommended, as it will allow anyone to create new links and delete but there's literally no point. Rather, for testing purposes, you can omit the password
environment variable, and any provided password should work.
This if not recommended in actual use however, as it will allow anyone to create new links and delete
old ones. This might not seem like a bad idea, until you have hundreds of links old ones. This might not seem like a bad idea, until you have hundreds of links
pointing to illegal content. Since there are no logs, it's impossible to prove pointing to illegal content. Since there are no logs, it's impossible to prove
that those links aren't created by you. that those links aren't created by you.
If you still want to do it, then you need to set an environment variable to
an exact value:
```
INSECURE_DISABLE_PASSWORD=I_KNOW_ITS_BAD
```
Any other value will not work.
## Notes ## Notes
- This is a fork of [this project](https://gitlab.com/draganczukp/simply-shorten). - It started as a fork of [this project](https://gitlab.com/draganczukp/simply-shorten).
- The list of adjectives and names used for random short url generation is a modified - The list of adjectives and names used for random short url generation is a modified
version of [this list used by docker](https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go). version of [this list used by docker](https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go).

3
actix/.directory Normal file
View file

@ -0,0 +1,3 @@
[Dolphin]
Timestamp=2023,4,2,17,52,37.922
Version=4

1487
actix/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

14
actix/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "simply-shorten"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "4"
actix-files = "0.6.2"
rusqlite = "0.29.0"
regex = "1.7.3"
rand = "0.8.5"
actix-session = {version = "0.7.2", features = ["cookie-session"]}

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,4 +1,4 @@
const getSiteUrl = async () => await fetch("/api/site") const getSiteUrl = async () => await fetch("/api/siteurl")
.then(res => res.text()) .then(res => res.text())
.then(text => { .then(text => {
if (text == "unset") { if (text == "unset") {
@ -9,8 +9,22 @@ const getSiteUrl = async () => await fetch("/api/site")
} }
}); });
const auth_fetch = async (link) => {
let reply = await fetch(link).then(res => res.text());
if (reply == "logged_out") {
pass = prompt("Please enter passkey to access this website");
await fetch("/api/login", {
method: "POST",
body: pass
});
return auth_fetch(link);
} else {
return reply;
}
}
const refreshData = async () => { const refreshData = async () => {
let data = await fetch("/api/all").then(res => res.text()); let data = await auth_fetch("/api/all");
data = data data = data
.split("\n") .split("\n")
.filter(line => line !== "") .filter(line => line !== "")
@ -108,7 +122,7 @@ const deleteButton = (shortUrl) => {
e.preventDefault(); e.preventDefault();
if (confirm("Do you want to delete the entry " + shortUrl + "?")) { if (confirm("Do you want to delete the entry " + shortUrl + "?")) {
document.getElementById("alertBox")?.remove(); document.getElementById("alertBox")?.remove();
fetch(`/api/${shortUrl}`, { fetch(`/api/del/${shortUrl}`, {
method: "DELETE" method: "DELETE"
}).then(_ => refreshData()); }).then(_ => refreshData());
} }

47
actix/src/auth.rs Normal file
View file

@ -0,0 +1,47 @@
use actix_session::Session;
use std::time::SystemTime;
pub fn validate(session: Session) -> bool {
let token = session.get::<String>("session-token");
if token.is_err() {
false
} else if !check(token.unwrap()) {
false
} else {
true
}
}
fn check(token: Option<String>) -> bool {
if token.is_none() {
false
} else {
let token_body = token.unwrap();
let token_parts: Vec<&str> = token_body.split(";").collect();
if token_parts.len() < 2 {
false
} else {
let token_text = token_parts[0];
let token_time = token_parts[1].parse::<u64>().unwrap_or(0);
let time_now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("Time went backwards!")
.as_secs();
if token_text == "valid-session-token" && time_now < token_time + 1209600 {
// There are 1209600 seconds in 14 days
true
} else {
false
}
}
}
}
pub fn gen_token() -> String {
let token_text = "valid-session-token".to_string();
let time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("Time went backwards!")
.as_secs();
format!("{token_text};{time}")
}

73
actix/src/database.rs Normal file
View file

@ -0,0 +1,73 @@
use rusqlite::Connection;
pub fn find_url(shortlink: &str, db: &Connection) -> String {
let mut statement = db
.prepare_cached("SELECT long_url FROM urls WHERE short_url = ?1")
.unwrap();
let links = statement
.query_map([shortlink], |row| Ok(row.get("long_url")?))
.unwrap();
let mut longlink = "".to_string();
for link in links {
longlink = link.unwrap();
}
longlink
}
pub fn getall(db: &Connection) -> Vec<String> {
let mut statement = db.prepare_cached("SELECT * FROM urls").unwrap();
let mut data = statement.query([]).unwrap();
let mut links: Vec<String> = Vec::new();
while let Some(row) = data.next().unwrap() {
let short_url: String = row.get("short_url").unwrap();
let long_url: String = row.get("long_url").unwrap();
let hits: i64 = row.get("hits").unwrap();
links.push(format!("{short_url},{long_url},{hits}"));
}
links
}
pub fn add_hit(shortlink: &str, db: &Connection) -> () {
db.execute(
"UPDATE urls SET hits = hits + 1 WHERE short_url = ?1",
[shortlink],
)
.unwrap();
}
pub fn add_link(shortlink: String, longlink: String, db: &Connection) -> bool {
match db.execute(
"INSERT INTO urls (long_url, short_url, hits) VALUES (?1, ?2, ?3)",
(longlink, shortlink, 0),
) {
Ok(_) => true,
Err(_) => false,
}
}
pub fn delete_link(shortlink: String, db: &Connection) -> () {
db.execute("DELETE FROM urls WHERE short_url = ?1", [shortlink])
.unwrap();
}
pub fn open_db(path: String) -> Connection {
let db = Connection::open(path).expect("Unable to open database!");
// Create table if it doesn't exist
db.execute(
"CREATE TABLE IF NOT EXISTS urls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
long_url TEXT NOT NULL,
short_url TEXT NOT NULL,
hits INTEGER NOT NULL
)",
[],
)
.unwrap();
db
}

140
actix/src/main.rs Normal file
View file

@ -0,0 +1,140 @@
use std::env;
use actix_files::{Files, NamedFile};
use actix_session::{storage::CookieSessionStore, Session, SessionMiddleware};
use actix_web::{
cookie::Key,
delete, get, middleware, post,
web::{self, Redirect},
App, HttpResponse, HttpServer, Responder,
};
use rusqlite::Connection;
mod auth;
mod database;
mod utils;
// This struct represents state
struct AppState {
db: Connection,
}
// Define the routes
// Add new links
#[post("/api/new")]
async fn add_link(req: String, data: web::Data<AppState>, session: Session) -> HttpResponse {
if auth::validate(session) {
let out = utils::add_link(req, &data.db);
if out.0 {
HttpResponse::Ok().body(out.1)
} else {
HttpResponse::BadRequest().body(out.1)
}
} else {
HttpResponse::Forbidden().body("logged_out")
}
}
// Return all active links
#[get("/api/all")]
async fn getall(data: web::Data<AppState>, session: Session) -> HttpResponse {
if auth::validate(session) {
HttpResponse::Ok().body(utils::getall(&data.db))
} else {
HttpResponse::Forbidden().body("logged_out")
}
}
// Get the site URL
#[get("/api/siteurl")]
async fn siteurl(session: Session) -> HttpResponse {
if auth::validate(session) {
let site_url = env::var("site_url").unwrap_or("unset".to_string());
HttpResponse::Ok().body(site_url)
} else {
HttpResponse::Forbidden().body("logged_out")
}
}
// 404 error page
#[get("/err/404")]
async fn error404() -> impl Responder {
NamedFile::open_async("./resources/404.html").await
}
// Handle a given shortlink
#[get("/{shortlink}")]
async fn link_handler(shortlink: web::Path<String>, data: web::Data<AppState>) -> impl Responder {
let shortlink_str = shortlink.to_string();
let longlink = utils::get_longurl(shortlink_str, &data.db);
if longlink == "".to_string() {
Redirect::to("/err/404")
} else {
database::add_hit(shortlink.as_str(), &data.db);
Redirect::to(longlink).permanent()
}
}
// Handle login
#[post("/api/login")]
async fn login(req: String, session: Session) -> HttpResponse {
if req == env::var("password").unwrap_or(req.clone()) {
// If no password was provided, match any password
session.insert("session-token", auth::gen_token()).unwrap();
HttpResponse::Ok().body("Correct password!")
} else {
eprintln!("Failed login attempt!");
HttpResponse::Forbidden().body("Wrong password!")
}
}
// Delete a given shortlink
#[delete("/api/del/{shortlink}")]
async fn delete_link(
shortlink: web::Path<String>,
data: web::Data<AppState>,
session: Session,
) -> HttpResponse {
if auth::validate(session) {
database::delete_link(shortlink.to_string(), &data.db);
HttpResponse::Ok().body("")
} else {
HttpResponse::Forbidden().body("Wrong password!")
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Generate session key in runtime so that restarts invalidates older logins
let secret_key = Key::generate();
let db_location = env::var("db_url").unwrap_or("/urls.sqlite".to_string());
let port = env::var("port")
.unwrap_or("4567".to_string())
.parse::<u16>()
.expect("Supplied port is not an integer");
// Actually start the server
HttpServer::new(move || {
App::new()
.wrap(SessionMiddleware::new(
CookieSessionStore::default(),
secret_key.clone(),
))
// Maintain a single instance of database throughout
.app_data(web::Data::new(AppState {
db: database::open_db(env::var("db_url").unwrap_or(db_location.clone())),
}))
.wrap(middleware::Compress::default())
.service(link_handler)
.service(error404)
.service(getall)
.service(siteurl)
.service(add_link)
.service(delete_link)
.service(login)
.default_service(Files::new("/", "./resources/").index_file("index.html"))
})
.bind(("0.0.0.0", port))?
.run()
.await
}

View file

@ -1,18 +1,53 @@
package tk.SinTan1729.url; use crate::database;
use rand::seq::SliceRandom;
use regex::Regex;
use rusqlite::Connection;
import java.util.Random; pub fn get_longurl(shortlink: String, db: &Connection) -> String {
import java.util.regex.Pattern; if validate_link(&shortlink) {
database::find_url(shortlink.as_str(), db)
} else {
"".to_string()
}
}
public class Utils { fn validate_link(link: &str) -> bool {
private static final Random random = new Random(System.currentTimeMillis()); let re = Regex::new("[a-z0-9-_]+").unwrap();
re.is_match(link)
}
private static final String SHORT_URL_PATTERN = "[a-z0-9-_]+"; pub fn getall(db: &Connection) -> String {
private static final Pattern PATTERN = Pattern.compile(SHORT_URL_PATTERN); let links = database::getall(db);
links.join("\n")
}
// The following lists are modified versions of the strings in pub fn add_link(req: String, db: &Connection) -> (bool, String) {
// https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go let chunks: Vec<&str> = req.split(';').collect();
let longlink = chunks[0].to_string();
private static final String[] adjective = new String[] {"admiring", "adoring", "affectionate", "agitated", "amazing", "angry", "awesome", "beautiful", let mut shortlink;
if chunks.len() > 1 {
shortlink = chunks[1].to_string().to_lowercase();
if shortlink == "".to_string() {
shortlink = random_name();
}
} else {
shortlink = random_name();
}
if validate_link(shortlink.as_str()) && get_longurl(shortlink.clone(), db) == "".to_string() {
(
database::add_link(shortlink.clone(), longlink, db),
shortlink,
)
} else {
(false, "shortUrl not valid or already in use".to_string())
}
}
fn random_name() -> String {
#[rustfmt::skip]
static ADJECTIVES: [&str; 108] = ["admiring", "adoring", "affectionate", "agitated", "amazing", "angry", "awesome", "beautiful",
"blissful", "bold", "boring", "brave", "busy", "charming", "clever", "compassionate", "competent", "condescending", "confident", "cool", "blissful", "bold", "boring", "brave", "busy", "charming", "clever", "compassionate", "competent", "condescending", "confident", "cool",
"cranky", "crazy", "dazzling", "determined", "distracted", "dreamy", "eager", "ecstatic", "elastic", "elated", "elegant", "eloquent", "epic", "cranky", "crazy", "dazzling", "determined", "distracted", "dreamy", "eager", "ecstatic", "elastic", "elated", "elegant", "eloquent", "epic",
"exciting", "fervent", "festive", "flamboyant", "focused", "friendly", "frosty", "funny", "gallant", "gifted", "goofy", "gracious", "exciting", "fervent", "festive", "flamboyant", "focused", "friendly", "frosty", "funny", "gallant", "gifted", "goofy", "gracious",
@ -21,9 +56,9 @@ public class Utils {
"nifty", "nostalgic", "objective", "optimistic", "peaceful", "pedantic", "pensive", "practical", "priceless", "quirky", "quizzical", "nifty", "nostalgic", "objective", "optimistic", "peaceful", "pedantic", "pensive", "practical", "priceless", "quirky", "quizzical",
"recursing", "relaxed", "reverent", "romantic", "sad", "serene", "sharp", "silly", "sleepy", "stoic", "strange", "stupefied", "suspicious", "recursing", "relaxed", "reverent", "romantic", "sad", "serene", "sharp", "silly", "sleepy", "stoic", "strange", "stupefied", "suspicious",
"sweet", "tender", "thirsty", "trusting", "unruffled", "upbeat", "vibrant", "vigilant", "vigorous", "wizardly", "wonderful", "xenodochial", "sweet", "tender", "thirsty", "trusting", "unruffled", "upbeat", "vibrant", "vigilant", "vigorous", "wizardly", "wonderful", "xenodochial",
"youthful", "zealous", "zen"}; "youthful", "zealous", "zen"];
#[rustfmt::skip]
private static final String[] name = new String[] {"agnesi", "albattani", "allen", "almeida", "antonelli", "archimedes", "ardinghelli", "aryabhata", "austin", static NAMES: [&str; 241] = ["agnesi", "albattani", "allen", "almeida", "antonelli", "archimedes", "ardinghelli", "aryabhata", "austin",
"babbage", "banach", "banzai", "bardeen", "bartik", "bassi", "beaver", "bell", "benz", "bhabha", "bhaskara", "black", "blackburn", "blackwell", "babbage", "banach", "banzai", "bardeen", "bartik", "bassi", "beaver", "bell", "benz", "bhabha", "bhaskara", "black", "blackburn", "blackwell",
"bohr", "booth", "borg", "bose", "bouman", "boyd", "brahmagupta", "brattain", "brown", "buck", "burnell", "cannon", "carson", "cartwright", "bohr", "booth", "borg", "bose", "bouman", "boyd", "brahmagupta", "brattain", "brown", "buck", "burnell", "cannon", "carson", "cartwright",
"carver", "cauchy", "cerf", "chandrasekhar", "chaplygin", "chatelet", "chatterjee", "chaum", "chebyshev", "clarke", "cohen", "colden", "cori", "carver", "cauchy", "cerf", "chandrasekhar", "chaplygin", "chatelet", "chatterjee", "chaum", "chebyshev", "clarke", "cohen", "colden", "cori",
@ -40,24 +75,11 @@ public class Utils {
"rhodes", "ride", "riemann", "ritchie", "robinson", "roentgen", "rosalind", "rubin", "saha", "sammet", "sanderson", "satoshi", "shamir", "shannon", "rhodes", "ride", "riemann", "ritchie", "robinson", "roentgen", "rosalind", "rubin", "saha", "sammet", "sanderson", "satoshi", "shamir", "shannon",
"shaw", "shirley", "shockley", "shtern", "sinoussi", "snyder", "solomon", "spence", "stonebraker", "sutherland", "swanson", "swartz", "swirles", "shaw", "shirley", "shockley", "shtern", "sinoussi", "snyder", "solomon", "spence", "stonebraker", "sutherland", "swanson", "swartz", "swirles",
"taussig", "tesla", "tharp", "thompson", "torvalds", "tu", "turing", "varahamihira", "vaughan", "vaughn", "villani", "visvesvaraya", "volhard", "taussig", "tesla", "tharp", "thompson", "torvalds", "tu", "turing", "varahamihira", "vaughan", "vaughn", "villani", "visvesvaraya", "volhard",
"wescoff", "weierstrass", "wilbur", "wiles", "williams", "williamson", "wilson", "wing", "wozniak", "wright", "wu", "yalow", "yonath", "zhukovsky"}; "wescoff", "weierstrass", "wilbur", "wiles", "williams", "williamson", "wilson", "wing", "wozniak", "wright", "wu", "yalow", "yonath", "zhukovsky"];
public static boolean validate(String shortUrl) { format!(
return PATTERN.matcher(shortUrl) "{0}-{1}",
.matches(); NAMES.choose(&mut rand::thread_rng()).unwrap(),
} ADJECTIVES.choose(&mut rand::thread_rng()).unwrap()
)
public static boolean isPasswordEnabled(){
String disablePasswordEnv = System.getenv("INSECURE_DISABLE_PASSWORD");
if(disablePasswordEnv != null && disablePasswordEnv.equals("I_KNOW_ITS_BAD")){
return false;
}
return true;
}
public static String randomName() {
return adjective[random.nextInt(adjective.length)]+"-"+name[random.nextInt(name.length)];
}
} }

View file

@ -1,33 +0,0 @@
plugins {
// Apply the java plugin to add support for Java
id 'java'
// Apply the application plugin to add support for building a CLI application.
id 'application'
}
repositories {
jcenter()
}
task fatJar(type: Jar) {
manifest {
attributes 'Main-Class': 'tk.SinTan1729.url.App'
}
archiveBaseName = 'url'
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
dependencies {
implementation 'com.sparkjava:spark-core:2.9.4'
implementation 'com.qmetric:spark-authentication:1.4'
implementation 'org.slf4j:slf4j-simple:1.6.1'
implementation group: 'org.xerial', name: 'sqlite-jdbc', version: '3.30.1'
}
application {
mainClassName = 'tk.SinTan1729.url.App'
}

View file

@ -8,6 +8,7 @@ services:
environment: environment:
# Change if you want to mount the database somewhere else # Change if you want to mount the database somewhere else
# In this case, you can get rid of the db volume below # In this case, you can get rid of the db volume below
# and instead do a mount manually by specifying the location
# - db_url=/urls.sqlite # - db_url=/urls.sqlite
# Change it in case you want to set the website name # Change it in case you want to set the website name
# displayed in front of the shorturls, defaults to # displayed in front of the shorturls, defaults to

Binary file not shown.

View file

@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

240
gradlew vendored
View file

@ -1,240 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

91
gradlew.bat vendored
View file

@ -1,91 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -1,10 +0,0 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
*
* Detailed information about configuring a multi-project build in Gradle can be found
* in the user manual at https://docs.gradle.org/6.1.1/userguide/multi_project_builds.html
*/
rootProject.name = 'url'

View file

@ -1,44 +0,0 @@
package tk.SinTan1729.url;
import static spark.Spark.*;
public class App {
public static void main(String[] args) {
// Useful for developing the frontend
// http://sparkjava.com/documentation#examples-and-faq -> How do I enable automatic refresh of static files?
if (System.getenv("dev") != null) {
String projectDir = System.getProperty("user.dir");
String staticDir = "/src/main/resources/public";
staticFiles.externalLocation(projectDir + staticDir);
} else {
staticFiles.location("/public");
}
port(Integer.parseInt(System.getenv().getOrDefault("port", "4567")));
// Add GZIP compression
after(Filters::addGZIP);
// No need to auth in dev
if (System.getenv("dev") == null && Utils.isPasswordEnabled()) {
// Authenticate
before("/api/*", Filters.createAuthFilter());
}
get("/", (req, res) -> {
res.redirect("/index.html");
return "Redirect";
});
path("/api", () -> {
get("/all", Routes::getAll);
post("/new", Routes::addUrl);
delete("/:shortUrl", Routes::delete);
get("/site", Routes::getSiteUrl);
});
get("/:shortUrl", Routes::goToLongUrl);
}
}

View file

@ -1,20 +0,0 @@
package tk.SinTan1729.url;
import com.qmetric.spark.authentication.AuthenticationDetails;
import com.qmetric.spark.authentication.BasicAuthenticationFilter;
import spark.Filter;
import spark.Request;
import spark.Response;
public class Filters {
public static void addGZIP(Request request, Response response) {
response.header("Content-Encoding", "gzip");
}
public static Filter createAuthFilter() {
String username = System.getenv("username");
String password = System.getenv("password");
return new BasicAuthenticationFilter(new AuthenticationDetails(username, password));
}
}

View file

@ -1,76 +0,0 @@
package tk.SinTan1729.url;
import org.eclipse.jetty.http.HttpStatus;
import spark.Request;
import spark.Response;
public class Routes {
private static final UrlRepository urlRepository;
static {
urlRepository = new UrlRepository();
}
public static String getAll(Request req, Response res) {
return String.join("\n", urlRepository.getAll());
}
public static String addUrl(Request req, Response res) {
var body = req.body();
var split = body.split(";");
String longUrl = split[0];
boolean unique = false;
String shortUrl;
try {
shortUrl = split[1];
shortUrl = shortUrl.toLowerCase();
if (urlRepository.findForShortUrl(shortUrl).isEmpty()) {
unique = true;
}
} catch (ArrayIndexOutOfBoundsException e) {
do {
shortUrl = Utils.randomName();
if (urlRepository.findForShortUrl(shortUrl).isEmpty()) {
unique = true;
}
} while (unique == false);
}
if (unique && Utils.validate(shortUrl)) {
return urlRepository.addUrl(longUrl, shortUrl);
} else {
res.status(HttpStatus.BAD_REQUEST_400);
return "shortUrl not valid or already in use";
}
}
public static String getSiteUrl(Request req, Response res) {
return System.getenv().getOrDefault("site_url", "unset");
}
public static String goToLongUrl(Request req, Response res) {
String shortUrl = req.params("shortUrl");
shortUrl = shortUrl.toLowerCase();
var longUrlOpt = urlRepository
.findForShortUrl(shortUrl);
if (longUrlOpt.isEmpty()) {
res.redirect("404.html");
return "";
}
urlRepository.addHit(shortUrl);
res.redirect(longUrlOpt.get(), HttpStatus.PERMANENT_REDIRECT_308);
return "";
}
public static String delete(Request req, Response res) {
String shortUrl = req.params("shortUrl");
urlRepository.deleteEntry(shortUrl);
return "";
}
}

View file

@ -1,117 +0,0 @@
package tk.SinTan1729.url;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class UrlRepository {
private static final String INSERT_ROW_SQL = "INSERT INTO urls (long_url, short_url, hits) VALUES (?, ?, ?)";
private static final String ADD_HIT_SQL = "UPDATE urls SET hits = hits + 1 WHERE short_url = ?";
private static final String CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS urls\n" +
"(\n" +
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n" +
" long_url TEXT NOT NULL,\n" +
" short_url TEXT NOT NULL,\n" +
" hits INTEGER NOT NULL\n" +
");";
private static final String SELECT_FOR_SHORT_SQL = "SELECT long_url FROM urls WHERE short_url = ?";
private static final String DELETE_ROW_SQL = "DELETE FROM urls WHERE short_url = ?";
private final String databaseUrl;
public UrlRepository() {
String path = System.getenv().getOrDefault("db_url", "/urls.sqlite");
databaseUrl = "jdbc:sqlite:" + path;
try (Connection conn = DriverManager.getConnection(databaseUrl)) {
if (conn != null) {
DatabaseMetaData meta = conn.getMetaData();
conn.createStatement()
.execute(CREATE_TABLE_SQL);
System.out.println("Database initialised");
}
} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
public List<String> getAll() {
try (final var con = DriverManager.getConnection(databaseUrl)) {
var statement = con.createStatement();
statement.execute("SELECT * FROM urls");
ResultSet rs = statement.getResultSet();
List<String> result = new ArrayList<>();
while (rs.next()) {
result.add(String.format("%s,%s,%s", rs.getString("short_url"), rs.getString("long_url"), String.valueOf(rs.getInt("hits"))));
}
return result;
} catch (SQLException e) {
e.printStackTrace();
}
return List.of();
}
public String addUrl(String longURL, String shortUrl) {
try (final var con = DriverManager.getConnection(databaseUrl)) {
final var stmt = con.prepareStatement(INSERT_ROW_SQL);
stmt.setString(1, longURL);
stmt.setString(2, shortUrl);
stmt.setInt(3, 0);
if (stmt.execute()) {
return String.format("%s,%s", shortUrl, longURL);
}
} catch (SQLException e) {
e.printStackTrace();
}
return shortUrl;
}
public void addHit(String shortURL) {
try (final var con = DriverManager.getConnection(databaseUrl)) {
final var stmt = con.prepareStatement(ADD_HIT_SQL);
stmt.setString(1, shortURL);
stmt.execute();
} catch (SQLException e) {
e.printStackTrace();
}
}
public Optional<String> findForShortUrl(String shortUrl) {
try (final var con = DriverManager.getConnection(databaseUrl)) {
final var stmt = con.prepareStatement(SELECT_FOR_SHORT_SQL);
stmt.setString(1, shortUrl);
if (stmt.execute()) {
ResultSet rs = stmt.getResultSet();
if (rs.next()) {
return Optional.of(rs.getString("long_url"));
}
}
return Optional.empty();
} catch (SQLException e) {
e.printStackTrace();
}
return Optional.empty();
}
public void deleteEntry(String shortUrl) {
try (final var con = DriverManager.getConnection(databaseUrl)) {
final var stmt = con.prepareStatement(DELETE_ROW_SQL);
stmt.setString(1, shortUrl);
stmt.execute();
} catch (SQLException e) {
e.printStackTrace();
}
}
}