Compare commits
316 commits
Author | SHA1 | Date | |
---|---|---|---|
|
d198135144 | ||
b838a6e027 | |||
|
0897b6b63b | ||
97b56c40ae | |||
2c8f47c0cb | |||
828019998e | |||
49d910fb3c | |||
c521ad1120 | |||
|
63020b2c24 | ||
|
d42a738861 | ||
e3eaf5aba8 | |||
3b48ce7b5e | |||
5363a1b056 | |||
0d58e626a4 | |||
e8faf660f4 | |||
67695da86b | |||
d50c183c9c | |||
90b04b1f21 | |||
babf3d8911 | |||
1ae00eb3a8 | |||
6f419c7b3d | |||
c557b8b262 | |||
a63222a71a | |||
86cea6278f | |||
f283991740 | |||
|
1775f71347 | ||
0b1224f8e5 | |||
|
1047763285 | ||
|
fc785c3eef | ||
|
17d0df943b | ||
|
7b52bd60da | ||
|
db8417d919 | ||
af1685bb70 | |||
|
a5621acfe4 | ||
1be89db43b | |||
a60853fd21 | |||
2b9fafe440 | |||
f952cb88a0 | |||
9eec252fe2 | |||
|
f8f4dae457 | ||
16bc211f9f | |||
cca5bcfa1a | |||
cba667ded8 | |||
1d9a8c202d | |||
eb4f05a87b | |||
5183279cab | |||
f1c1642976 | |||
|
eed3c2292a | ||
|
4fb8d0cb5c | ||
|
9a0cdec646 | ||
|
818dadb84f | ||
|
247cfb0476 | ||
|
6347a89725 | ||
|
9ddf043c17 | ||
|
a1f8700664 | ||
|
aab7a9b3d1 | ||
|
1ef5d539d5 | ||
|
5c2886f651 | ||
|
2c56c68637 | ||
756d675f06 | |||
e6eed2dd70 | |||
37a5300015 | |||
66d94634d9 | |||
03f5529c30 | |||
f772475d96 | |||
8b8ceca313 | |||
201d0b319f | |||
733ef6ea67 | |||
cf5909c888 | |||
dcb3144b22 | |||
e0c61bdb93 | |||
06f7a33d5d | |||
514e905299 | |||
3688692c7a | |||
a7cf0cdf30 | |||
35880f4d1e | |||
|
0d0da1141b | ||
4a8b62446c | |||
|
855145d4d7 | ||
|
59f679a1c2 | ||
5213a9df2e | |||
3e6f482533 | |||
56ab16aa4e | |||
066fa9c80a | |||
9fc8634704 | |||
5bbaad3001 | |||
892959d49d | |||
9948ce713c | |||
|
70d9d828c5 | ||
114a97a273 | |||
96495b037d | |||
69fc25a264 | |||
f19f3249cc | |||
2cf0e5d2de | |||
de9bc130d5 | |||
8ff4c3f24f | |||
eab1c9bc73 | |||
0b50a7c261 | |||
e55c6f82b4 | |||
6992d27390 | |||
39e4d2df74 | |||
41b7e37819 | |||
e67e0a88cd | |||
00ade1af40 | |||
b1632c4c87 | |||
3445d5366a | |||
|
5fb8587628 | ||
6cdacda510 | |||
ed2be0e883 | |||
0fce881654 | |||
168cff94a2 | |||
b1c4142296 | |||
75912e8f9d | |||
5ac822d5f9 | |||
233247e154 | |||
108abc2b30 | |||
4a158bbea6 | |||
51c6817487 | |||
|
0192c26fd0 | ||
|
8b815e1bbb | ||
3ad05f1e63 | |||
931b4a95e4 | |||
cdc3508a0c | |||
e742c0ab5e | |||
231fd3c8ca | |||
86ec787d96 | |||
30c0b8b50a | |||
62ae71f4ca | |||
e9bb9d0b65 | |||
ca14c02e70 | |||
0469f9b933 | |||
f27984a63f | |||
7ad874a1ff | |||
604e95aa9c | |||
917be6ade4 | |||
2594051a54 | |||
38b817fdf8 | |||
a9168e3459 | |||
d48b664c0a | |||
a0f0eb5280 | |||
34518affaf | |||
4f80b1b522 | |||
|
f38abdf1fb | ||
f2b5e1ab6d | |||
99b5298cd8 | |||
6659452c51 | |||
|
3441d3ae90 | ||
f6060eb649 | |||
6c7ca8d0ac | |||
599b013fc9 | |||
841f877ee8 | |||
088cd594a5 | |||
f9e642275a | |||
9a520c122e | |||
3a712f812a | |||
|
753b73c4a3 | ||
fbcb088260 | |||
f526e7ec5b | |||
5d8dd6fb63 | |||
5e4db14ea2 | |||
c76b39dc16 | |||
731cb41646 | |||
0cfa674029 | |||
1f18766f79 | |||
dfefff2703 | |||
6d3d220cff | |||
629e66a57c | |||
ce76f04f35 | |||
db5d1f72bd | |||
e54aa3b33b | |||
82559d38fd | |||
2baa481040 | |||
ffb4846239 | |||
5bd174d287 | |||
9221c3e371 | |||
c5cfba85f9 | |||
d6dcd2f18d | |||
d278021e1b | |||
8dbb6e9bd6 | |||
cde3fb4c89 | |||
f3d5e2cf50 | |||
fd74a941d9 | |||
2cab341e8b | |||
2a2ed7e41a | |||
5a5a1bc775 | |||
b66086be38 | |||
3be2862e9f | |||
b0603f62b4 | |||
9d5bc2d0fd | |||
94af81b802 | |||
03154fd010 | |||
4c394c8004 | |||
9d46546e44 | |||
c07bb5c25f | |||
85f150b543 | |||
d235f1aea7 | |||
13d613093b | |||
71ac19028e | |||
d8ee2ce658 | |||
5ecd29926d | |||
aa097ad982 | |||
bf0944a5fd | |||
6693941985 | |||
0bfaa49e7b | |||
a32f00c36e | |||
8b1af0a169 | |||
9fdb634d71 | |||
a5574683a6 | |||
d1ac769fc1 | |||
8ba8472940 | |||
|
f331370767 | ||
cb8f6f3e1d | |||
4d3e52cc95 | |||
8f0a6f0fc6 | |||
|
15ff8819a7 | ||
|
501f7f1d65 | ||
bd47f3c74b | |||
31212ab252 | |||
deff47db2c | |||
95a8263797 | |||
0227c5f783 | |||
4a8385955b | |||
1f9cf1d777 | |||
2a85189155 | |||
caa6c58fd2 | |||
583700cdbf | |||
432328b97e | |||
|
bcf30049a2 | ||
|
bfd7e111d9 | ||
0bd8fbe96c | |||
0c80a0ac21 | |||
f6255566b0 | |||
57d390a129 | |||
acb67fdcf6 | |||
|
9e8a9395a5 | ||
|
c77763c1c2 | ||
79cca4bc26 | |||
1ee7ebe847 | |||
ac82396584 | |||
bed96d6de5 | |||
7550d197b4 | |||
a4e7c6b444 | |||
7b59a9aa5c | |||
ff0a17e57b | |||
e3520be75b | |||
1c91855820 | |||
74f3fddd01 | |||
8be3e54861 | |||
f3984624d9 | |||
ca01676916 | |||
ff4801a476 | |||
|
2c34598faf | ||
8325f0c2d2 | |||
|
b657919a82 | ||
a91bcc0503 | |||
5e4f6aa1fc | |||
e14fc1f3b5 | |||
abe8238713 | |||
d22d88b985 | |||
43dab10d22 | |||
cdfcb901fa | |||
66ea86b6d3 | |||
e18a71a73c | |||
2f48b1e58c | |||
7e6e419dbc | |||
6b1d82bded | |||
3e07e9aadb | |||
a9c21a214f | |||
0add351480 | |||
3a2b1acf49 | |||
cc066c0716 | |||
ab5a032269 | |||
d831bd65e0 | |||
c3aa7438e9 | |||
4edf7349b0 | |||
0ddd75502d | |||
b779019716 | |||
66868effba | |||
96b3ae1297 | |||
34b2b116ba | |||
7d5ff40893 | |||
51e75f726e | |||
a14dbf5cd2 | |||
71d7253248 | |||
e4ff2df3f1 | |||
98d10cfd5b | |||
7f6ba5175f | |||
618fd0e53a | |||
046c6d63a4 | |||
3c699d8b02 | |||
a1f73c8a9d | |||
b9d76b6734 | |||
0e97516759 | |||
d9f7e9537d | |||
7acc336d4c | |||
e45ecd8eef | |||
6b8ae8657f | |||
d7d93d1873 | |||
c887b15a38 | |||
f6279cd313 | |||
36c2678e40 | |||
214f2af0b0 | |||
635d51a659 | |||
3aac41d3a7 | |||
cb56d31abe | |||
41e6231217 | |||
c8118a4c96 | |||
272dc38933 | |||
94aece0d1d | |||
5c8307c57a | |||
cfdc7a7644 | |||
cc15ffa463 | |||
9f68f2632f | |||
8956519a10 | |||
b803f5805d | |||
2c8076e2e1 |
32
.classpath
|
@ -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>
|
6
.gitattributes
vendored
|
@ -1,6 +0,0 @@
|
|||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# These are explicitly windows files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
|
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
assignees: 'SinTan1729'
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Which version of Chhoto-URL are you experiencing the problem on?**
|
||||
e.g. v5.x.x
|
||||
|
||||
**Can you reproduce the issue in the latest version?**
|
||||
Yes/No
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'feature-request'
|
||||
assignees: 'SinTan1729'
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
21
.gitignore
vendored
|
@ -1,12 +1,13 @@
|
|||
# Ignore Gradle project-specific cache directory
|
||||
.gradle
|
||||
# Ignore build outputs
|
||||
actix/target
|
||||
|
||||
# Ignore Gradle build output directory
|
||||
build
|
||||
.idea/
|
||||
|
||||
local.properties
|
||||
url.iml
|
||||
urls.csv
|
||||
.env
|
||||
# Ignore SQLite file
|
||||
urls.sqlite
|
||||
|
||||
# Ignore irrelevant dotfiles
|
||||
.vscode/
|
||||
**/.directory
|
||||
.env
|
||||
cookie*
|
||||
.idea/
|
||||
.DS_Store
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
# This file is a template, and might need editing before it works on your project.
|
||||
docker-build-main:
|
||||
# Official docker image.
|
||||
image: docker:latest
|
||||
stage: build
|
||||
services:
|
||||
- docker:dind
|
||||
before_script:
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
|
||||
script:
|
||||
- docker build --pull -t "$CI_REGISTRY_IMAGE" .
|
||||
- docker push "$CI_REGISTRY_IMAGE"
|
||||
only:
|
||||
- main
|
||||
|
||||
docker-build:
|
||||
# Official docker image.
|
||||
image: docker:latest
|
||||
stage: build
|
||||
services:
|
||||
- docker:dind
|
||||
before_script:
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
|
||||
script:
|
||||
- docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" .
|
||||
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
|
||||
except:
|
||||
- main
|
34
.project
|
@ -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>
|
|
@ -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
|
34
Dockerfile
|
@ -1,14 +1,30 @@
|
|||
FROM gradle:jdk14 AS build
|
||||
COPY --chown=gradle:gradle . /home/gradle/src
|
||||
WORKDIR /home/gradle/src
|
||||
RUN gradle build --no-daemon
|
||||
# SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
FROM openjdk:14.0-slim
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-slim AS chef
|
||||
WORKDIR /chhoto-url
|
||||
|
||||
EXPOSE 4567
|
||||
FROM chef AS planner
|
||||
COPY ./actix/Cargo.toml ./actix/Cargo.lock ./
|
||||
COPY ./actix/src ./src
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
RUN mkdir /app
|
||||
FROM chef AS builder
|
||||
ARG target=x86_64-unknown-linux-musl
|
||||
RUN apt-get update && apt-get install -y musl-tools
|
||||
RUN rustup target add $target
|
||||
|
||||
COPY --from=build /home/gradle/src/build/libs/*.jar /app/application.jar
|
||||
COPY --from=planner /chhoto-url/recipe.json recipe.json
|
||||
# Build dependencies - this is the caching Docker layer
|
||||
RUN cargo chef cook --release --target=$target --recipe-path recipe.json
|
||||
|
||||
ENTRYPOINT ["java", "-jar","/app/application.jar"]
|
||||
COPY ./actix/Cargo.toml ./actix/Cargo.lock ./
|
||||
COPY ./actix/src ./src
|
||||
# Build application
|
||||
RUN cargo build --release --target=$target --locked --bin chhoto-url
|
||||
RUN cp /chhoto-url/target/$target/release/chhoto-url /chhoto-url/release
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /chhoto-url/release /chhoto-url
|
||||
COPY ./resources /resources
|
||||
ENTRYPOINT ["/chhoto-url"]
|
||||
|
|
18
Dockerfile.multiarch
Normal file
|
@ -0,0 +1,18 @@
|
|||
# SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
FROM scratch AS builder-amd64
|
||||
COPY ./actix/target/x86_64-unknown-linux-musl/release/chhoto-url /chhoto-url
|
||||
|
||||
FROM scratch AS builder-arm64
|
||||
COPY ./actix/target/aarch64-unknown-linux-musl/release/chhoto-url /chhoto-url
|
||||
|
||||
FROM scratch AS builder-arm
|
||||
COPY ./actix/target/armv7-unknown-linux-musleabihf/release/chhoto-url /chhoto-url
|
||||
|
||||
ARG TARGETARCH
|
||||
FROM builder-$TARGETARCH
|
||||
COPY ./resources /resources
|
||||
|
||||
ENTRYPOINT ["/chhoto-url"]
|
||||
|
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Przemek Dragańczuk
|
||||
Copyright (c) 2023 Sayantan Santra
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
52
Makefile
Normal file
|
@ -0,0 +1,52 @@
|
|||
# SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
# .env file has the variables $DOCKER_USERNAME and $PASSWORD defined
|
||||
include .env
|
||||
|
||||
setup:
|
||||
cargo install cross
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
docker buildx create --use --platform=linux/arm64,linux/amd64 --name multi-platform-builder
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
build-dev:
|
||||
cargo build --release --locked --manifest-path=actix/Cargo.toml --target x86_64-unknown-linux-musl
|
||||
|
||||
docker-local: build-dev
|
||||
docker build --tag chhoto-url --build-arg TARGETARCH=amd64 -f Dockerfile.multiarch .
|
||||
|
||||
docker-stop:
|
||||
docker ps -q --filter "name=chhoto-url" | xargs -r docker stop
|
||||
docker ps -aq --filter "name=chhoto-url" | xargs -r docker rm
|
||||
|
||||
docker-test: docker-local docker-stop
|
||||
docker run -p ${PORT}:${PORT} --name chhoto-url -e password="${PASSWORD}" -e public_mode="${PUBLIC_MODE}" \
|
||||
-e site_url="${SITE_URL}" -e db_url="${DB_URL}" -e redirect_method="${REDIRECT_METHOD}" -e port="${PORT}"\
|
||||
-e slug_style="${SLUG_STYLE}" -e slug_length="${SLUG_LENGTH}" -e cache_control_header="${CACHE_CONTROL_HEADER}"\
|
||||
-e api_key="${API_KEY}" -e disable_frontend="${DISABLE_FRONTEND}"\
|
||||
-d chhoto-url
|
||||
docker logs chhoto-url -f
|
||||
|
||||
docker-dev: build-dev
|
||||
docker build --push --tag ${DOCKER_USERNAME}/chhoto-url:dev --build-arg TARGETARCH=amd64 -f Dockerfile.multiarch .
|
||||
|
||||
build-release:
|
||||
cross build --release --locked --manifest-path=actix/Cargo.toml --target aarch64-unknown-linux-musl
|
||||
cross build --release --locked --manifest-path=actix/Cargo.toml --target armv7-unknown-linux-musleabihf
|
||||
cross build --release --locked --manifest-path=actix/Cargo.toml --target x86_64-unknown-linux-musl
|
||||
|
||||
V_PATCH := $(shell cat actix/Cargo.toml | sed -rn 's/^version = "(.+)"$$/\1/p')
|
||||
V_MINOR := $(shell cat actix/Cargo.toml | sed -rn 's/^version = "(.+)\..+"$$/\1/p')
|
||||
V_MAJOR := $(shell cat actix/Cargo.toml | sed -rn 's/^version = "(.+)\..+\..+"$$/\1/p')
|
||||
docker-release: build-release
|
||||
docker buildx build --push --tag ${DOCKER_USERNAME}/chhoto-url:${V_MAJOR} --tag ${DOCKER_USERNAME}/chhoto-url:${V_MINOR} \
|
||||
--tag ${DOCKER_USERNAME}/chhoto-url:${V_PATCH} --tag ${DOCKER_USERNAME}/chhoto-url:latest \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 -f Dockerfile.multiarch .
|
||||
|
||||
clean:
|
||||
docker ps -q --filter "name=chhoto-url" | xargs -r docker stop
|
||||
docker ps -aq --filter "name=chhoto-url" | xargs -r docker rm
|
||||
cargo clean --manifest-path=actix/Cargo.toml
|
||||
|
||||
.PHONY: build-dev docker-local docker-stop build-release
|
263
README.md
|
@ -1,56 +1,86 @@
|
|||
[](https://hub.docker.com/r/sintan1729/simply-shorten)
|
||||
<!-- SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com> -->
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
|
||||
#  <span style="font-size:42px">Simply Shorten</span>
|
||||
[](https://hub.docker.com/r/sintan1729/chhoto-url)
|
||||
[](https://github.com/SinTan1729)
|
||||
[](https://github.com/SinTan1729/chhoto-url/releases/latest)
|
||||

|
||||

|
||||
[](https://spdx.org/licenses/MIT.html)
|
||||
|
||||
#  <span style="font-size:42px">Chhoto URL</span>
|
||||
|
||||
# What is it?
|
||||
A simple selfhosted URL shortener with no unnecessary features.
|
||||
A simple selfhosted URL shortener with no unnecessary features. Simplicity
|
||||
and speed are the main foci of this project. The docker image is ~6 MB (compressed),
|
||||
and it uses <5 MB of RAM under regular use.
|
||||
|
||||
Don't worry if you see no activity for a long time. I consider this project
|
||||
to be complete, not dead. I'm unlikely to add any new features, but I will try
|
||||
and fix every bug you report.
|
||||
and fix every bug you report. I will also try to keep it updated in terms of
|
||||
security vulnerabilities.
|
||||
|
||||
If you feel like a feature is missing, please let me know by creating an issue
|
||||
using the "feature request" template.
|
||||
|
||||
## But why another URL shortener?
|
||||
I've looked at a couple popular URL shorteners, however they either have
|
||||
unnecessary features, or they didn't have all the features I wanted.
|
||||
Most URL shorteners are either bloated with unnecessary features, or are a pain to set up.
|
||||
Even fewer are written with simplicity and lightness in mind. When I saw the `simply-shorten`
|
||||
project (linked below), I really liked the idea but thought that it missed some features. Also,
|
||||
I didn't like the fact that a simple app like this had a ~200 MB docker image (mostly due to the
|
||||
included java runtime). So, I decided to rewrite it in Rust and add some features to it that I
|
||||
thought were essential (e.g. hit counting).
|
||||
|
||||
## What does the name mean?
|
||||
Chhoto (ছোট, [IPA](https://en.wikipedia.org/wiki/Help:IPA/Bengali): /tʃʰoʈo/) is the Bangla word
|
||||
for small. URL means, well... URL. So the name simply means Small URL.
|
||||
|
||||
# Features
|
||||
- Shortens URLs of any length to a fixed length, randomly generated string
|
||||
- Shortens URLs of any length to a randomly generated link.
|
||||
- (Optional) Allows you to specify the shortened URL instead of the generated
|
||||
one (Missing in a surprising number of alternatives)
|
||||
- Opening the fixed length URL in your browser will instantly redirect you
|
||||
to the correct long URL (you'd think that's a standard feature, but
|
||||
apparently it's not)
|
||||
- Provides a simple API for adding new short links
|
||||
one. (It's surprisingly missing in a surprising number of alternatives.)
|
||||
- Opening the shortened URL in your browser will instantly redirect you
|
||||
to the correct long URL. (So no stupid redirecting pages.)
|
||||
- Super lightweight and snappy. (The docker image is only ~6MB and RAM uasge
|
||||
stays under 5MB under normal use.)
|
||||
- Counts number of hits for each short link in a privacy respecting way
|
||||
i.e. only the hit is recorded, and nothing else
|
||||
- Allows setting the URL of your website, in case you want to conveniently generate
|
||||
short links locally
|
||||
- Links are stored in an SQLite database
|
||||
- Available as a Docker container
|
||||
- Backend written in Java using [Spark Java](http://sparkjava.com/), frontend
|
||||
i.e. only the hit is recorded, and nothing else.
|
||||
- Has a mobile friendly UI, and automatic dark mode.
|
||||
- Has a public mode, where anyone can add links without authentication. Deleting
|
||||
or listing available links will need admin access using the password. It's also
|
||||
possible to completely disable the frontend.
|
||||
- Allows setting the URL of your website, in case you want to conveniently
|
||||
generate short links locally.
|
||||
- Links are stored in an SQLite database.
|
||||
- Available as a Docker container.
|
||||
- Backend written in Rust using [Actix](https://actix.rs/), frontend
|
||||
written in plain HTML and vanilla JS, using [Pure CSS](https://purecss.io/)
|
||||
for styling
|
||||
for styling.
|
||||
- Uses very basic authentication using a provided password. It's not encrypted in transport.
|
||||
I recommend using a reverse proxy such as [caddy](https://caddyserver.com/) to
|
||||
encrypt the connection by SSL.
|
||||
|
||||
# Bloat that will not be implemented
|
||||
- Tracking or spying of any kind. The only logs that still exist are
|
||||
errors printed to stderr and the default SLF4J warning.
|
||||
- User management. If you need a shortener for your whole organisation, either
|
||||
errors printed to stderr and the basic logging (only warnings) provided by the
|
||||
[`env_logger`](https://crates.io/crates/env_logger) crate.
|
||||
- User management. If you need a shortener for your whole organization, either
|
||||
run separate containers for everyone or use something else.
|
||||
- Cookies, newsletters, "we value your privacy" popups or any of the multiple
|
||||
other ways modern web shows how anti-user it is. We all hate those, and they're
|
||||
not needed here.
|
||||
- Paywalls or messages begging for donations. If you want to support me (for
|
||||
whatever reason), you can message me through Github issues.
|
||||
whatever reason), you can message me through GitHub issues.
|
||||
|
||||
# Screenshot
|
||||

|
||||
# Screenshots
|
||||
<p align="middle">
|
||||
<img src="screenshot-desktop.webp" height="250" alt="desktop screenshot" />
|
||||
<img src="screenshot-mobile.webp" height="250" alt="mobile screenshot" />
|
||||
</p>
|
||||
|
||||
# Usage
|
||||
## Using `docker compose` (Recommended method)
|
||||
There is a sample `docker-compose.yml` file in this repository. It contains
|
||||
There is a sample `compose.yaml` file in this repository. It contains
|
||||
everything needed for a basic install. You can use it as a base, modifying
|
||||
it as needed. Run it with
|
||||
```
|
||||
|
@ -58,91 +88,166 @@ docker compose up -d
|
|||
```
|
||||
If you're using a custom location for the `db_url`, make sure to make that file
|
||||
before running the docker image, as otherwise a directory will be created in its
|
||||
place, resulting in an error.
|
||||
## Building from source
|
||||
Clone this repository
|
||||
```
|
||||
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.
|
||||
place, resulting in possibly unwanted behavior.
|
||||
|
||||
### 2. Set environment variables
|
||||
```bash
|
||||
# Required for authentication
|
||||
export username=<api username>
|
||||
export password=<api password>
|
||||
# Sets where the database exists. Can be local or remote (optional)
|
||||
export db_url=<url> # Default: './urls.sqlite'
|
||||
# Sets the url of website, so that it displays that even when accessed
|
||||
# locally (optional, defaults to hostname you're accessing it on)
|
||||
export site_url=<url>
|
||||
```
|
||||
|
||||
### 3. Run it
|
||||
```
|
||||
java -jar build/libs/url.jar
|
||||
```
|
||||
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.
|
||||
|
||||
## Running with docker
|
||||
## Building and running with docker
|
||||
### `docker run` method
|
||||
0. (Only if you really want to) Build the image
|
||||
0. (Only if you really want to) Build the image for the default `x86_64-unknown-linux-musl` target:
|
||||
```
|
||||
docker build . -t simply-shorten:latest
|
||||
docker build . -t chhoto-url
|
||||
```
|
||||
For building on `arm64` or `arm/v7`, use the following:
|
||||
```
|
||||
docker build . -t chhoto-url --build-arg target=<desired-target>
|
||||
```
|
||||
Make sure that the desired target is a `musl` one, since the docker image is built from `scratch`.
|
||||
For cross-compilation, take a look at the `Makefile`. It builds and pushes for `linux/amd64`, `linux/aarch64`
|
||||
and `linux/arm/v7` architectures. For any other architectures, open a discussion, and I'll try to help you out.
|
||||
1. Run the image
|
||||
```
|
||||
docker run -p 4567:4567
|
||||
-d url:latest
|
||||
-e username="username"
|
||||
-e password="password"
|
||||
-d simply-shorten:latest
|
||||
-d chhoto-url:latest
|
||||
```
|
||||
1.a Make the database file available to host (optional)
|
||||
```
|
||||
touch ./urls.sqlite
|
||||
docker run -p 4567:4567 \
|
||||
-e username="username" \
|
||||
-e password="password" \
|
||||
-v ./urls.sqlite:/urls.sqlite \
|
||||
-e db_url=/urls.sqlite \
|
||||
-d simply-shorten:latest
|
||||
-d chhoto-url:latest
|
||||
```
|
||||
1.b Further, set the URL of your website (optional)
|
||||
```
|
||||
touch ./urls.sqlite
|
||||
docker run -p 4567:4567 \
|
||||
-e username="username" \
|
||||
-e password="password" \
|
||||
-v ./urls.sqlite:/urls.sqlite \
|
||||
-e db_url=/urls.sqlite \
|
||||
-e site_url="https://www.example.com" \
|
||||
-d simply-shorten:latest
|
||||
-d chhoto-url:latest
|
||||
```
|
||||
1.c Further, set an API key to activate JSON result mode (optional)
|
||||
|
||||
```
|
||||
docker run -p 4567:4567 \
|
||||
-e password="password" \
|
||||
-e api_key="SECURE_API_KEY" \
|
||||
-v ./urls.sqlite:/urls.sqlite \
|
||||
-e db_url=/urls.sqlite \
|
||||
-e site_url="https://www.example.com" \
|
||||
-d chhoto-url:latest
|
||||
```
|
||||
|
||||
You can set the redirect method to Permanent 308 (default) or Temporary 307 by setting
|
||||
the `redirect_method` variable to `TEMPORARY` or `PERMANENT` (it's matched exactly). By
|
||||
default, the auto-generated links are adjective-name pairs. You can use UIDs by setting
|
||||
the `slug_style` variable to `UID`. You can also set the length of those slug by setting
|
||||
the `slug_length` variable. It defaults to 8, and a minimum of 4 is supported.
|
||||
|
||||
To enable public mode, set `public_mode` to `Enable`. With this, anyone will be able to add
|
||||
links. Listing existing links or deleting links will need admin access using the password. To
|
||||
completely disable the frontend, set `disable_frontend` to `True`.
|
||||
|
||||
By default, the server sends no Cache-Control headers. You can set custom `cache_control_header`
|
||||
to send your desired headers. It must be a comma separated list of valid
|
||||
[RFC 7234 §5.2](https://datatracker.ietf.org/doc/html/rfc7234#section-5.2) headers. For example,
|
||||
you can set it to `no-cache, private` to disable caching. It might help during testing if
|
||||
served through a proxy.
|
||||
|
||||
## Deploying in your Kubernetes cluster with Helm
|
||||
The helm values are very sparse to keep it simple. If you need more values to be variable, feel free to adjust.
|
||||
|
||||
The PVC allocates 100Mi and the PV is using a host path volume.
|
||||
|
||||
The helm chart assumes you have [cert manager](https://github.com/jetstack/cert-manager) deployed to have TLS certificates managed easily in your cluster. Feel free to remove the issuer and adjust the ingress if you're on AWS with EKS for example. To install cert-manager, I recommend using the ["kubectl apply" way](https://cert-manager.io/docs/installation/kubectl/) to install cert-manager.
|
||||
|
||||
To get started, `cp helm-chart/values.yaml helm-chart/my-values.yaml` and adjust `password`, `fqdn` and `letsencryptmail` in your new `my-values.yaml`, then just run
|
||||
|
||||
``` bash
|
||||
cd helm-chart
|
||||
helm upgrade --install chhoto-url . -n chhoto-url --create-namespace -f my-values.yaml
|
||||
```
|
||||
|
||||
## Instructions for CLI usage
|
||||
The application can be used from the terminal using something like `curl`. In all the examples
|
||||
below, replace `http://localhost:4567` with where your instance of `chhoto-url` is accessible.
|
||||
|
||||
You can get the version of `chhoto-url` the server is running using `curl http://localhost:4567/api/version` and
|
||||
get the siteurl using `curl http://localhost:4567/api/siteurl`. These routes are accessible without any authentication.
|
||||
|
||||
### API key validation
|
||||
**This is required for programs that rely on a JSON response from Chhoto URL**
|
||||
|
||||
In order to use API key validation, set the `api_key` environment variable. If this is not set, the API will default to cookie
|
||||
validation (see section above). If the API key is insecure, a warning will be outputted along with a generated API key which may be used.
|
||||
|
||||
Example Linux command for generating a secure API key: `tr -dc A-Za-z0-9 </dev/urandom | head -c 128`
|
||||
|
||||
To add a link:
|
||||
``` bash
|
||||
curl -X POST -H "X-API-Key: <YOUR_API_KEY>" -d '{"shortlink":"<shortlink>", "longlink":"<longlink>"}' http://localhost:4567/api/new
|
||||
```
|
||||
Send an empty `<shortlink>` if you want it to be auto-generated. The server will reply with the generated shortlink.
|
||||
|
||||
To get information about a single shortlink:
|
||||
``` bash
|
||||
curl -H "X-API-Key: <YOUR_API_KEY>" -d '<shortlink>' http://localhost:4567/api/expand
|
||||
```
|
||||
(This route is not accessible using cookie validation.)
|
||||
|
||||
To get a list of all the currently available links:
|
||||
``` bash
|
||||
curl -H "X-API-Key: <YOUR_API_KEY>" http://localhost:4567/api/all
|
||||
```
|
||||
|
||||
To delete a link:
|
||||
``` bash
|
||||
curl -X DELETE -H "X-API-Key: <YOUR_API_KEY>" http://localhost:4567/api/del/<shortlink>
|
||||
```
|
||||
Where `<shortlink>` is name of the shortened link you would like to delete. For example, if the shortened link is
|
||||
`http://localhost:4567/example`, `<shortlink>` would be `example`.
|
||||
|
||||
The server will output when the instance is accessed over API, when an incorrect API key is received, etc.
|
||||
|
||||
### Cookie validation
|
||||
If you have set up a password, first do the following to get an authentication cookie and store it in a file.
|
||||
```bash
|
||||
curl -X POST -d "<your-password>" -c cookie.txt http://localhost:4567/api/login
|
||||
```
|
||||
You should receive "Correct password!" if the provided password was correct. For any subsequent
|
||||
request, please add `-b cookie.txt` to provide authentication.
|
||||
|
||||
To add a link, do
|
||||
```bash
|
||||
curl -X POST -d '{"shortlink":"<shortlink>", "longlink":"<longlink>"}' http://localhost:4567/api/new
|
||||
```
|
||||
Send an empty `<shortlink>` if you want it to be auto-generated. The server will reply with the generated shortlink.
|
||||
|
||||
To get a list of all the currently available links as `json`, do
|
||||
```bash
|
||||
curl http://localhost:4567/api/all
|
||||
```
|
||||
|
||||
To delete a link, do
|
||||
```bash
|
||||
curl -X DELETE http://localhost:4567/api/del/<shortlink>
|
||||
```
|
||||
The server will send a confirmation.
|
||||
|
||||
## Disable authentication
|
||||
As requested in #5, it is possible to completely disable the authentication.
|
||||
This if not recommended, as it will allow anyone to create new links and delete
|
||||
If you do not define a password environment variable when starting the docker image, authentication
|
||||
will be disabled.
|
||||
|
||||
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
|
||||
pointing to illegal content. Since there are no logs, it's impossible to prove
|
||||
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
|
||||
- This is a fork of [this project](https://gitlab.com/draganczukp/simply-shorten).
|
||||
- It started as a fork of [`simply-shorten`](https://gitlab.com/draganczukp/simply-shorten).
|
||||
- There's an (unofficial) extension maintained by for shortening URLs easily using Chhoto URL.
|
||||
[You can take a look at it here.](https://github.com/SolninjaA/Chhoto-URL-Extension)
|
||||
- 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).
|
||||
|
|
2100
actix/Cargo.lock
generated
Normal file
40
actix/Cargo.toml
Normal file
|
@ -0,0 +1,40 @@
|
|||
# SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
[package]
|
||||
name = "chhoto-url"
|
||||
version = "5.6.3"
|
||||
edition = "2021"
|
||||
authors = ["Sayantan Santra <sayantan[dot]santra689[at]gmail[dot]com"]
|
||||
license = "mit"
|
||||
description = "A simple selfhosted URL shortener with no unnecessary features."
|
||||
homepage = "https://github.com/SinTan1729/chhoto-url"
|
||||
documentation = "https://github.com/SinTan1729/chhoto-url"
|
||||
repository = "https://github.com/SinTan1729/chhoto-url"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"docker",
|
||||
"rust",
|
||||
"self-hosted",
|
||||
"url-shortener",
|
||||
"webapp",
|
||||
"shortener",
|
||||
"link-shortener",
|
||||
"actix-web",
|
||||
]
|
||||
categories = ["web-programming"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.5.1"
|
||||
actix-files = "0.6.5"
|
||||
rusqlite = { version = "0.34.0", features = ["bundled"] }
|
||||
regex = "1.10.3"
|
||||
rand = "0.9.0"
|
||||
passwords = "3.1.16"
|
||||
actix-session = { version = "0.10.0", features = ["cookie-session"] }
|
||||
env_logger = "0.11.1"
|
||||
nanoid = "0.4.0"
|
||||
serde_json = "1.0.115"
|
||||
serde = { version = "1.0.197", features = [ "derive" ] }
|
100
actix/src/auth.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use actix_session::Session;
|
||||
use actix_web::HttpRequest;
|
||||
use std::{env, time::SystemTime};
|
||||
|
||||
// API key generation and scoring
|
||||
use passwords::{analyzer, scorer, PasswordGenerator};
|
||||
|
||||
// Validate API key
|
||||
pub fn validate_key(key: String) -> bool {
|
||||
if let Ok(api_key) = env::var("api_key") {
|
||||
if api_key != key {
|
||||
eprintln!("Incorrect API key was provided when connecting to Chhoto URL.");
|
||||
false
|
||||
} else {
|
||||
eprintln!("Server accessed with API key.");
|
||||
true
|
||||
}
|
||||
} else {
|
||||
eprintln!("API was accessed with API key validation but no API key was specified. Set the 'api_key' environment variable.");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Generate an API key if the user doesn't specify a secure key
|
||||
// Called in main.rs
|
||||
pub fn gen_key() -> String {
|
||||
let key = PasswordGenerator {
|
||||
length: 128,
|
||||
numbers: true,
|
||||
lowercase_letters: true,
|
||||
uppercase_letters: true,
|
||||
symbols: false,
|
||||
spaces: false,
|
||||
exclude_similar_characters: false,
|
||||
strict: true,
|
||||
};
|
||||
key.generate_one().unwrap()
|
||||
}
|
||||
|
||||
// Check if the API key header exists
|
||||
pub fn api_header(req: &HttpRequest) -> Option<&str> {
|
||||
req.headers().get("X-API-Key")?.to_str().ok()
|
||||
}
|
||||
|
||||
// Determine whether the inputted API key is sufficiently secure
|
||||
pub fn is_key_secure() -> bool {
|
||||
let score = scorer::score(&analyzer::analyze(env::var("api_key").unwrap()));
|
||||
score >= 90.0
|
||||
}
|
||||
|
||||
// Validate a given password
|
||||
pub fn validate(session: Session) -> bool {
|
||||
// If there's no password provided, just return true
|
||||
if env::var("password")
|
||||
.ok()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.is_none()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Ok(token) = session.get::<String>("chhoto-url-auth") {
|
||||
check(token)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Check a token cryptographically
|
||||
fn check(token: Option<String>) -> bool {
|
||||
if let Some(token_body) = token {
|
||||
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();
|
||||
token_text == "chhoto-url-auth" && time_now < token_time + 1209600 // There are 1209600 seconds in 14 days
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new cryptographic token
|
||||
pub fn gen_token() -> String {
|
||||
let token_text = String::from("chhoto-url-auth");
|
||||
let time = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("Time went backwards!")
|
||||
.as_secs();
|
||||
format!("{token_text};{time}")
|
||||
}
|
103
actix/src/database.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
// Struct for encoding a DB row
|
||||
#[derive(Serialize)]
|
||||
pub struct DBRow {
|
||||
shortlink: String,
|
||||
longlink: String,
|
||||
hits: i64,
|
||||
}
|
||||
|
||||
// Find a single URL
|
||||
pub fn find_url(shortlink: &str, db: &Connection, needhits: bool) -> (Option<String>, Option<i64>) {
|
||||
let query = if needhits {
|
||||
"SELECT long_url,hits FROM urls WHERE short_url = ?1"
|
||||
} else {
|
||||
"SELECT long_url FROM urls WHERE short_url = ?1"
|
||||
};
|
||||
let mut statement = db
|
||||
.prepare_cached(query)
|
||||
.expect("Error preparing SQL statement for find_url.");
|
||||
|
||||
let longlink = statement
|
||||
.query_row([shortlink], |row| row.get("long_url"))
|
||||
.ok();
|
||||
let hits = statement.query_row([shortlink], |row| row.get("hits")).ok();
|
||||
(longlink, hits)
|
||||
}
|
||||
|
||||
// Get all URLs in DB
|
||||
pub fn getall(db: &Connection) -> Vec<DBRow> {
|
||||
let mut statement = db
|
||||
.prepare_cached("SELECT * FROM urls ORDER BY id ASC")
|
||||
.expect("Error preparing SQL statement for getall.");
|
||||
|
||||
let mut data = statement
|
||||
.query([])
|
||||
.expect("Error executing query for getall.");
|
||||
|
||||
let mut links: Vec<DBRow> = Vec::new();
|
||||
while let Some(row) = data.next().expect("Error reading fetched rows.") {
|
||||
let row_struct = DBRow {
|
||||
shortlink: row
|
||||
.get("short_url")
|
||||
.expect("Error reading shortlink from row."),
|
||||
longlink: row
|
||||
.get("long_url")
|
||||
.expect("Error reading shortlink from row."),
|
||||
hits: row.get("hits").expect("Error reading shortlink from row."),
|
||||
};
|
||||
links.push(row_struct);
|
||||
}
|
||||
|
||||
links
|
||||
}
|
||||
|
||||
// Add a hit when site is visited
|
||||
pub fn add_hit(shortlink: &str, db: &Connection) {
|
||||
db.execute(
|
||||
"UPDATE urls SET hits = hits + 1 WHERE short_url = ?1",
|
||||
[shortlink],
|
||||
)
|
||||
.expect("Error updating hit count.");
|
||||
}
|
||||
|
||||
// Insert a new link
|
||||
pub fn add_link(shortlink: String, longlink: String, db: &Connection) -> bool {
|
||||
db.execute(
|
||||
"INSERT INTO urls (long_url, short_url, hits) VALUES (?1, ?2, ?3)",
|
||||
(longlink, shortlink, 0),
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
// Delete and existing link
|
||||
pub fn delete_link(shortlink: String, db: &Connection) -> bool {
|
||||
if let Ok(delta) = db.execute("DELETE FROM urls WHERE short_url = ?1", [shortlink]) {
|
||||
delta > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Open the DB, and create schema if missing
|
||||
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
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.expect("Unable to initialize empty database.");
|
||||
|
||||
db
|
||||
}
|
125
actix/src/main.rs
Normal file
|
@ -0,0 +1,125 @@
|
|||
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use actix_files::Files;
|
||||
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
|
||||
use actix_web::{cookie::Key, middleware, web, App, HttpServer};
|
||||
use rusqlite::Connection;
|
||||
use std::{env, io::Result};
|
||||
|
||||
// Import modules
|
||||
mod auth;
|
||||
mod database;
|
||||
mod services;
|
||||
mod utils;
|
||||
|
||||
// This struct represents state
|
||||
struct AppState {
|
||||
db: Connection,
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("warn"));
|
||||
|
||||
// Generate session key in runtime so that restart invalidates older logins
|
||||
let secret_key = Key::generate();
|
||||
|
||||
let db_location = env::var("db_url")
|
||||
.ok()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or(String::from("urls.sqlite"));
|
||||
|
||||
// Get the port environment variable
|
||||
let port = env::var("port")
|
||||
.unwrap_or(String::from("4567"))
|
||||
.parse::<u16>()
|
||||
.expect("Supplied port is not an integer");
|
||||
|
||||
let cache_control_header = env::var("cache_control_header")
|
||||
.ok()
|
||||
.filter(|s| !s.trim().is_empty());
|
||||
|
||||
let disable_frontend = env::var("disable_frontend").is_ok_and(|s| s.trim() == "True");
|
||||
|
||||
// If an API key is set, check the security
|
||||
if let Ok(key) = env::var("api_key") {
|
||||
if !auth::is_key_secure() {
|
||||
eprintln!("WARN: API key is insecure! Please change it. Current key is: {}. Generated secure key which you may use: {}", key, auth::gen_key())
|
||||
} else {
|
||||
eprintln!("Secure API key was provided.")
|
||||
}
|
||||
}
|
||||
|
||||
// If the site_url env variable exists
|
||||
if let Some(site_url) = env::var("site_url").ok().filter(|s| !s.trim().is_empty()) {
|
||||
// Get first and last characters of the site_url
|
||||
let mut chars = site_url.chars();
|
||||
let first = chars.next();
|
||||
let last = chars.next_back();
|
||||
let url = chars.as_str();
|
||||
// If the site_url is encapsulated by quotes (i.e. invalid)
|
||||
if first == Option::from('"') || first == Option::from('\'') && first == last {
|
||||
// Set the site_url without the quotes
|
||||
env::set_var("site_url", url);
|
||||
eprintln!("WARN: The site_url environment variable is encapsulated by quotes. Automatically adjusting to {}", url);
|
||||
|
||||
// Tell the user what URI the server will respond with
|
||||
eprintln!("INFO: Public URI is: {url}:{port}.")
|
||||
} else {
|
||||
// No issues
|
||||
eprintln!("INFO: Configured Site URL is: {site_url}.");
|
||||
|
||||
// Tell the user what URI the server will respond with
|
||||
eprintln!("INFO: Public URI is: {site_url}:{port}.")
|
||||
}
|
||||
} else {
|
||||
// Site URL is not configured
|
||||
eprintln!("WARN: The site_url environment variable is not configured. Defaulting to http://localhost");
|
||||
eprintln!("INFO: Public URI is: http://localhost:{port}.")
|
||||
}
|
||||
|
||||
// Tell the user that the server has started, and where it is listening to, rather than simply outputting nothing
|
||||
eprintln!("Server has started at 0.0.0.0 on port {port}.");
|
||||
|
||||
// Actually start the server
|
||||
HttpServer::new(move || {
|
||||
let mut app = App::new()
|
||||
.wrap(middleware::Logger::default())
|
||||
.wrap(middleware::Compress::default())
|
||||
.wrap(
|
||||
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
|
||||
.cookie_same_site(actix_web::cookie::SameSite::Strict)
|
||||
.cookie_secure(false)
|
||||
.build(),
|
||||
)
|
||||
// Maintain a single instance of database throughout
|
||||
.app_data(web::Data::new(AppState {
|
||||
db: database::open_db(db_location.clone()),
|
||||
}))
|
||||
.wrap(if let Some(header) = &cache_control_header {
|
||||
middleware::DefaultHeaders::new().add(("Cache-Control", header.to_owned()))
|
||||
} else {
|
||||
middleware::DefaultHeaders::new()
|
||||
})
|
||||
.service(services::link_handler)
|
||||
.service(services::getall)
|
||||
.service(services::siteurl)
|
||||
.service(services::version)
|
||||
.service(services::add_link)
|
||||
.service(services::delete_link)
|
||||
.service(services::login)
|
||||
.service(services::logout)
|
||||
.service(services::expand);
|
||||
|
||||
if !disable_frontend {
|
||||
app = app.service(Files::new("/", "./resources/").index_file("index.html"));
|
||||
}
|
||||
|
||||
app.default_service(actix_web::web::get().to(services::error404))
|
||||
})
|
||||
// Hardcode the port the server listens to. Allows for more intuitive Docker Compose port management
|
||||
.bind(("0.0.0.0", port))?
|
||||
.run()
|
||||
.await
|
||||
}
|
322
actix/src/services.rs
Normal file
|
@ -0,0 +1,322 @@
|
|||
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use actix_files::NamedFile;
|
||||
use actix_session::Session;
|
||||
use actix_web::{
|
||||
delete, get,
|
||||
http::StatusCode,
|
||||
post,
|
||||
web::{self, Redirect},
|
||||
Either, HttpRequest, HttpResponse, Responder,
|
||||
};
|
||||
use std::env;
|
||||
// Serialize JSON data
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::auth;
|
||||
use crate::database;
|
||||
use crate::utils;
|
||||
use crate::AppState;
|
||||
|
||||
// Store the version number
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
// Define JSON struct for returning JSON data
|
||||
#[derive(Serialize)]
|
||||
struct Response {
|
||||
success: bool,
|
||||
error: bool,
|
||||
reason: String,
|
||||
}
|
||||
|
||||
// Needed to return the short URL to make it easier for programs leveraging the API
|
||||
#[derive(Serialize)]
|
||||
struct CreatedURL {
|
||||
success: bool,
|
||||
error: bool,
|
||||
shorturl: String,
|
||||
}
|
||||
|
||||
// Struct for returning information about a shortlink
|
||||
#[derive(Serialize)]
|
||||
struct LinkInfo {
|
||||
success: bool,
|
||||
error: bool,
|
||||
longurl: String,
|
||||
hits: i64,
|
||||
}
|
||||
|
||||
// Define the routes
|
||||
|
||||
// Add new links
|
||||
#[post("/api/new")]
|
||||
pub async fn add_link(
|
||||
req: String,
|
||||
data: web::Data<AppState>,
|
||||
session: Session,
|
||||
http: HttpRequest,
|
||||
) -> HttpResponse {
|
||||
// Call is_api_ok() function, pass HttpRequest
|
||||
let result = utils::is_api_ok(http);
|
||||
// If success, add new link
|
||||
if result.success {
|
||||
let out = utils::add_link(req, &data.db);
|
||||
if out.0 {
|
||||
let port = env::var("port")
|
||||
.unwrap_or(String::from("4567"))
|
||||
.parse::<u16>()
|
||||
.expect("Supplied port is not an integer");
|
||||
let mut url = format!(
|
||||
"{}:{}",
|
||||
env::var("site_url")
|
||||
.ok()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or(String::from("http://localhost")),
|
||||
port
|
||||
);
|
||||
// If the port is 80, remove the port from the returned URL (better for copying and pasting)
|
||||
// Return http://
|
||||
if port == 80 {
|
||||
url = env::var("site_url")
|
||||
.ok()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or(String::from("http://localhost"));
|
||||
}
|
||||
// If the port is 443, remove the port from the returned URL (better for copying and pasting)
|
||||
// Return https://
|
||||
if port == 443 {
|
||||
url = env::var("site_url")
|
||||
.ok()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or(String::from("https://localhost"));
|
||||
}
|
||||
let response = CreatedURL {
|
||||
success: true,
|
||||
error: false,
|
||||
shorturl: format!("{}/{}", url, out.1),
|
||||
};
|
||||
HttpResponse::Created().json(response)
|
||||
} else {
|
||||
let response = Response {
|
||||
success: false,
|
||||
error: true,
|
||||
reason: out.1,
|
||||
};
|
||||
HttpResponse::Conflict().json(response)
|
||||
}
|
||||
} else if result.error {
|
||||
HttpResponse::Unauthorized().json(result)
|
||||
// If password authentication or public mode is used - keeps backwards compatibility
|
||||
} else if env::var("public_mode") == Ok(String::from("Enable")) || auth::validate(session) {
|
||||
let out = utils::add_link(req, &data.db);
|
||||
if out.0 {
|
||||
HttpResponse::Created().body(out.1)
|
||||
} else {
|
||||
HttpResponse::Conflict().body(out.1)
|
||||
}
|
||||
} else {
|
||||
HttpResponse::Unauthorized().body("Not logged in!")
|
||||
}
|
||||
}
|
||||
|
||||
// Return all active links
|
||||
#[get("/api/all")]
|
||||
pub async fn getall(
|
||||
data: web::Data<AppState>,
|
||||
session: Session,
|
||||
http: HttpRequest,
|
||||
) -> HttpResponse {
|
||||
// Call is_api_ok() function, pass HttpRequest
|
||||
let result = utils::is_api_ok(http);
|
||||
// If success, return all links
|
||||
if result.success {
|
||||
HttpResponse::Ok().body(utils::getall(&data.db))
|
||||
} else if result.error {
|
||||
HttpResponse::Unauthorized().json(result)
|
||||
// If password authentication is used - keeps backwards compatibility
|
||||
} else if auth::validate(session) {
|
||||
HttpResponse::Ok().body(utils::getall(&data.db))
|
||||
} else {
|
||||
let body = if env::var("public_mode") == Ok(String::from("Enable")) {
|
||||
"Using public mode."
|
||||
} else {
|
||||
"Not logged in!"
|
||||
};
|
||||
HttpResponse::Unauthorized().body(body)
|
||||
}
|
||||
}
|
||||
|
||||
// Get information about a single shortlink
|
||||
#[post("/api/expand")]
|
||||
pub async fn expand(req: String, data: web::Data<AppState>, http: HttpRequest) -> HttpResponse {
|
||||
let result = utils::is_api_ok(http);
|
||||
if result.success {
|
||||
let linkinfo = utils::get_longurl(req, &data.db, true);
|
||||
if let Some(longlink) = linkinfo.0 {
|
||||
let body = LinkInfo {
|
||||
success: true,
|
||||
error: false,
|
||||
longurl: longlink,
|
||||
hits: linkinfo
|
||||
.1
|
||||
.expect("Error getting hit count for existing shortlink."),
|
||||
};
|
||||
HttpResponse::Ok().json(body)
|
||||
} else {
|
||||
let body = Response {
|
||||
success: false,
|
||||
error: true,
|
||||
reason: "The shortlink does not exist on the server.".to_string(),
|
||||
};
|
||||
HttpResponse::Unauthorized().json(body)
|
||||
}
|
||||
} else {
|
||||
HttpResponse::Unauthorized().json(result)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the site URL
|
||||
#[get("/api/siteurl")]
|
||||
pub async fn siteurl() -> HttpResponse {
|
||||
let site_url = env::var("site_url")
|
||||
.ok()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or(String::from("unset"));
|
||||
HttpResponse::Ok().body(site_url)
|
||||
}
|
||||
|
||||
// Get the version number
|
||||
#[get("/api/version")]
|
||||
pub async fn version() -> HttpResponse {
|
||||
HttpResponse::Ok().body(VERSION)
|
||||
}
|
||||
|
||||
// 404 error page
|
||||
pub async fn error404() -> impl Responder {
|
||||
NamedFile::open_async("./resources/static/404.html")
|
||||
.await
|
||||
.customize()
|
||||
.with_status(StatusCode::NOT_FOUND)
|
||||
}
|
||||
|
||||
// Handle a given shortlink
|
||||
#[get("/{shortlink}")]
|
||||
pub async fn link_handler(
|
||||
shortlink: web::Path<String>,
|
||||
data: web::Data<AppState>,
|
||||
) -> impl Responder {
|
||||
let shortlink_str = shortlink.to_string();
|
||||
if let Some(longlink) = utils::get_longurl(shortlink_str, &data.db, false).0 {
|
||||
let redirect_method = env::var("redirect_method").unwrap_or(String::from("PERMANENT"));
|
||||
database::add_hit(shortlink.as_str(), &data.db);
|
||||
if redirect_method == "TEMPORARY" {
|
||||
Either::Left(Redirect::to(longlink))
|
||||
} else {
|
||||
// Defaults to permanent redirection
|
||||
Either::Left(Redirect::to(longlink).permanent())
|
||||
}
|
||||
} else {
|
||||
Either::Right(
|
||||
NamedFile::open_async("./resources/static/404.html")
|
||||
.await
|
||||
.customize()
|
||||
.with_status(StatusCode::NOT_FOUND),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle login
|
||||
#[post("/api/login")]
|
||||
pub async fn login(req: String, session: Session) -> HttpResponse {
|
||||
// Keep this function backwards compatible
|
||||
if env::var("api_key").is_ok() {
|
||||
if let Ok(password) = env::var("password") {
|
||||
if password != req {
|
||||
eprintln!("Failed login attempt!");
|
||||
let response = Response {
|
||||
success: false,
|
||||
error: true,
|
||||
reason: "Wrong password!".to_string(),
|
||||
};
|
||||
return HttpResponse::Unauthorized().json(response);
|
||||
}
|
||||
}
|
||||
// Return Ok if no password was set on the server side
|
||||
session
|
||||
.insert("chhoto-url-auth", auth::gen_token())
|
||||
.expect("Error inserting auth token.");
|
||||
|
||||
let response = Response {
|
||||
success: true,
|
||||
error: false,
|
||||
reason: "Correct password!".to_string(),
|
||||
};
|
||||
HttpResponse::Ok().json(response)
|
||||
} else {
|
||||
if let Ok(password) = env::var("password") {
|
||||
if password != req {
|
||||
eprintln!("Failed login attempt!");
|
||||
return HttpResponse::Unauthorized().body("Wrong password!");
|
||||
}
|
||||
}
|
||||
// Return Ok if no password was set on the server side
|
||||
session
|
||||
.insert("chhoto-url-auth", auth::gen_token())
|
||||
.expect("Error inserting auth token.");
|
||||
|
||||
HttpResponse::Ok().body("Correct password!")
|
||||
}
|
||||
}
|
||||
|
||||
// Handle logout
|
||||
// There's no reason to be calling this route with an API key, so it is not necessary to check if the api_key env variable is set.
|
||||
#[delete("/api/logout")]
|
||||
pub async fn logout(session: Session) -> HttpResponse {
|
||||
if session.remove("chhoto-url-auth").is_some() {
|
||||
HttpResponse::Ok().body("Logged out!")
|
||||
} else {
|
||||
HttpResponse::Unauthorized().body("You don't seem to be logged in.")
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a given shortlink
|
||||
#[delete("/api/del/{shortlink}")]
|
||||
pub async fn delete_link(
|
||||
shortlink: web::Path<String>,
|
||||
data: web::Data<AppState>,
|
||||
session: Session,
|
||||
http: HttpRequest,
|
||||
) -> HttpResponse {
|
||||
// Call is_api_ok() function, pass HttpRequest
|
||||
let result = utils::is_api_ok(http);
|
||||
// If success, delete shortlink
|
||||
if result.success {
|
||||
if utils::delete_link(shortlink.to_string(), &data.db) {
|
||||
let response = Response {
|
||||
success: true,
|
||||
error: false,
|
||||
reason: format!("Deleted {}", shortlink),
|
||||
};
|
||||
HttpResponse::Ok().json(response)
|
||||
} else {
|
||||
let response = Response {
|
||||
success: false,
|
||||
error: true,
|
||||
reason: "The short link was not found, and could not be deleted.".to_string(),
|
||||
};
|
||||
HttpResponse::NotFound().json(response)
|
||||
}
|
||||
} else if result.error {
|
||||
HttpResponse::Unauthorized().json(result)
|
||||
// If "pass" is true - keeps backwards compatibility
|
||||
} else if auth::validate(session) {
|
||||
if utils::delete_link(shortlink.to_string(), &data.db) {
|
||||
HttpResponse::Ok().body(format!("Deleted {shortlink}"))
|
||||
} else {
|
||||
HttpResponse::NotFound().body("Not found!")
|
||||
}
|
||||
} else {
|
||||
HttpResponse::Unauthorized().body("Not logged in!")
|
||||
}
|
||||
}
|
200
actix/src/utils.rs
Normal file
|
@ -0,0 +1,200 @@
|
|||
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{auth, database};
|
||||
use actix_web::HttpRequest;
|
||||
use nanoid::nanoid;
|
||||
use rand::seq::IndexedRandom;
|
||||
use regex::Regex;
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
|
||||
// Struct for reading link pairs sent during API call
|
||||
#[derive(Deserialize)]
|
||||
struct URLPair {
|
||||
shortlink: String,
|
||||
longlink: String,
|
||||
}
|
||||
|
||||
// Define JSON struct for response
|
||||
#[derive(Serialize)]
|
||||
pub struct Response {
|
||||
pub(crate) success: bool,
|
||||
pub(crate) error: bool,
|
||||
reason: String,
|
||||
pass: bool,
|
||||
}
|
||||
|
||||
// If the api_key environment variable exists
|
||||
pub fn is_api_ok(http: HttpRequest) -> Response {
|
||||
// If the api_key environment variable exists
|
||||
if env::var("api_key").is_ok() {
|
||||
// If the header exists
|
||||
if let Some(header) = auth::api_header(&http) {
|
||||
// If the header is correct
|
||||
if auth::validate_key(header.to_string()) {
|
||||
Response {
|
||||
success: true,
|
||||
error: false,
|
||||
reason: "Correct API key".to_string(),
|
||||
pass: false,
|
||||
}
|
||||
} else {
|
||||
Response {
|
||||
success: false,
|
||||
error: true,
|
||||
reason: "Incorrect API key".to_string(),
|
||||
pass: false,
|
||||
}
|
||||
}
|
||||
// The header may not exist when the user logs in through the web interface, so allow a request with no header.
|
||||
// Further authentication checks will be conducted in services.rs
|
||||
} else {
|
||||
// Due to the implementation of this result in services.rs, this JSON object will not be outputted.
|
||||
Response {
|
||||
success: false,
|
||||
error: false,
|
||||
reason: "X-API-Key header was not found".to_string(),
|
||||
pass: true,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the API key isn't set, but an API Key header is provided
|
||||
if auth::api_header(&http).is_some() {
|
||||
Response {
|
||||
success: false,
|
||||
error: true,
|
||||
reason: "An API key was provided, but the 'api_key' environment variable is not configured in the Chhoto URL instance".to_string(),
|
||||
pass: false
|
||||
}
|
||||
} else {
|
||||
Response {
|
||||
success: false,
|
||||
error: false,
|
||||
reason: "".to_string(),
|
||||
pass: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request the DB for searching an URL
|
||||
pub fn get_longurl(shortlink: String, db: &Connection, needhits: bool) -> (Option<String>, Option<i64>) {
|
||||
if validate_link(&shortlink) {
|
||||
database::find_url(shortlink.as_str(), db, needhits)
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
|
||||
// Only have a-z, 0-9, - and _ as valid characters in a shortlink
|
||||
fn validate_link(link: &str) -> bool {
|
||||
let re = Regex::new("^[a-z0-9-_]+$").expect("Regex generation failed.");
|
||||
re.is_match(link)
|
||||
}
|
||||
|
||||
// Request the DB for all URLs
|
||||
pub fn getall(db: &Connection) -> String {
|
||||
let links = database::getall(db);
|
||||
serde_json::to_string(&links).expect("Failure during creation of json from db.")
|
||||
}
|
||||
|
||||
// Make checks and then request the DB to add a new URL entry
|
||||
pub fn add_link(req: String, db: &Connection) -> (bool, String) {
|
||||
let mut chunks: URLPair;
|
||||
if let Ok(json) = serde_json::from_str(&req) {
|
||||
chunks = json;
|
||||
} else {
|
||||
// shorturl should always be supplied, even if empty
|
||||
return (false, String::from("Invalid request!"));
|
||||
}
|
||||
|
||||
let style = env::var("slug_style").unwrap_or(String::from("Pair"));
|
||||
let mut len = env::var("slug_length")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(8);
|
||||
if len < 4 {
|
||||
len = 4;
|
||||
}
|
||||
|
||||
if chunks.shortlink.is_empty() {
|
||||
chunks.shortlink = gen_link(style, len);
|
||||
}
|
||||
|
||||
if validate_link(chunks.shortlink.as_str())
|
||||
&& get_longurl(chunks.shortlink.clone(), db, false).0.is_none()
|
||||
{
|
||||
(
|
||||
database::add_link(chunks.shortlink.clone(), chunks.longlink, db),
|
||||
chunks.shortlink,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
false,
|
||||
String::from("Short URL not valid or already in use!"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if link, and request DB to delete it if exists
|
||||
pub fn delete_link(shortlink: String, db: &Connection) -> bool {
|
||||
if validate_link(shortlink.as_str()) {
|
||||
database::delete_link(shortlink, db)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a random link using either adjective-name pair (default) of a slug or a-z, 0-9
|
||||
fn gen_link(style: String, len: usize) -> 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",
|
||||
"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",
|
||||
"great", "happy", "hardcore", "heuristic", "hopeful", "hungry", "infallible", "inspiring", "intelligent", "interesting", "jolly",
|
||||
"jovial", "keen", "kind", "laughing", "loving", "lucid", "magical", "modest", "musing", "mystifying", "naughty", "nervous", "nice",
|
||||
"nifty", "nostalgic", "objective", "optimistic", "peaceful", "pedantic", "pensive", "practical", "priceless", "quirky", "quizzical",
|
||||
"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",
|
||||
"youthful", "zealous", "zen"];
|
||||
#[rustfmt::skip]
|
||||
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",
|
||||
"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",
|
||||
"cray", "curie", "curran", "darwin", "davinci", "dewdney", "dhawan", "diffie", "dijkstra", "dirac", "driscoll", "dubinsky", "easley", "edison",
|
||||
"einstein", "elbakyan", "elgamal", "elion", "ellis", "engelbart", "euclid", "euler", "faraday", "feistel", "fermat", "fermi", "feynman", "franklin",
|
||||
"gagarin", "galileo", "galois", "ganguly", "gates", "gauss", "germain", "goldberg", "goldstine", "goldwasser", "golick", "goodall", "gould", "greider",
|
||||
"grothendieck", "haibt", "hamilton", "hardy", "haslett", "hawking", "heisenberg", "hellman", "hermann", "herschel", "hertz", "heyrovsky", "hodgkin",
|
||||
"hofstadter", "hoover", "hopper", "hugle", "hypatia", "ishizaka", "jackson", "jang", "jemison", "jennings", "jepsen", "johnson", "joliot", "jones",
|
||||
"kalam", "kapitsa", "kare", "keldysh", "keller", "kepler", "khayyam", "khorana", "kilby", "kirch", "knuth", "kowalevski", "lalande", "lamarr",
|
||||
"lamport", "leakey", "leavitt", "lederberg", "lehmann", "lewin", "lichterman", "liskov", "lovelace", "lumiere", "mahavira", "margulis", "matsumoto",
|
||||
"maxwell", "mayer", "mccarthy", "mcclintock", "mclaren", "mclean", "mcnulty", "meitner", "mendel", "mendeleev", "meninsky", "merkle", "mestorf",
|
||||
"mirzakhani", "montalcini", "moore", "morse", "moser", "murdock", "napier", "nash", "neumann", "newton", "nightingale", "nobel", "noether", "northcutt",
|
||||
"noyce", "panini", "pare", "pascal", "pasteur", "payne", "perlman", "pike", "poincare", "poitras", "proskuriakova", "ptolemy", "raman", "ramanujan",
|
||||
"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",
|
||||
"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"];
|
||||
|
||||
#[rustfmt::skip]
|
||||
static CHARS: [char; 36] = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
|
||||
'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
|
||||
if style == "UID" {
|
||||
nanoid!(len, &CHARS)
|
||||
} else {
|
||||
format!(
|
||||
"{0}-{1}",
|
||||
ADJECTIVES
|
||||
.choose(&mut rand::rng())
|
||||
.expect("Error choosing random adjective."),
|
||||
NAMES
|
||||
.choose(&mut rand::rng())
|
||||
.expect("Error choosing random name.")
|
||||
)
|
||||
}
|
||||
}
|
33
build.gradle
|
@ -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()
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes "Main-Class": "tk.sintan1729.url.App"
|
||||
}
|
||||
|
||||
from {
|
||||
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "com.sparkjava:spark-core:2.8.0"
|
||||
compile 'com.qmetric:spark-authentication:1.4'
|
||||
compile group: 'org.xerial', name: 'sqlite-jdbc', version: '3.30.1'
|
||||
|
||||
}
|
||||
|
||||
application {
|
||||
mainClassName = 'tk.sintan1729.url.App'
|
||||
}
|
75
compose.yaml
Normal file
|
@ -0,0 +1,75 @@
|
|||
# SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
services:
|
||||
chhoto-url:
|
||||
image: sintan1729/chhoto-url:latest
|
||||
restart: unless-stopped
|
||||
container_name: chhoto-url
|
||||
# You may enable the next two options if you want, but it may break the program if the db is bind
|
||||
# mounted from the system. It does add extra security, but I don't know enough about docker
|
||||
# to help in case it breaks something.
|
||||
# read_only: true
|
||||
# cap_drop:
|
||||
# - ALL
|
||||
ports:
|
||||
# If you changed the "port" environment variable, adjust accordingly
|
||||
# The number AFTER the colon should match the "port" variable and the number
|
||||
# before the colon is the port where you would access the container from outside.
|
||||
- 4567:4567
|
||||
environment:
|
||||
# Change if you want to mount the database somewhere else.
|
||||
# In this case, you can get rid of the db volume below
|
||||
# and instead do a mount manually by specifying the location.
|
||||
# Make sure that you create an empty file with the correct name
|
||||
# before starting the container if you do make any changes.
|
||||
# (In fact, I'd suggest that you do that so that you can keep
|
||||
# a copy of your database.)
|
||||
- db_url=/db/urls.sqlite
|
||||
|
||||
# Change this if your server URL is not "http://localhost"
|
||||
# This must not be surrounded by quotes. For example:
|
||||
# site_url="https://www.example.com" incorrect
|
||||
# site_url=https://www.example.com correct
|
||||
# This is important to ensure Chhoto URL outputs the shortened link with the correct URL.
|
||||
# - site_url=https://www.example.com
|
||||
|
||||
# Change this if you are running Chhoto URL on a port which is not 4567.
|
||||
# This is important to ensure Chhoto URL outputs the shortened link with the correct port.
|
||||
# - port=4567
|
||||
|
||||
- password=TopSecretPass
|
||||
|
||||
# This needs to be set in order to use programs that use the JSON interface of Chhoto URL.
|
||||
# You will get a warning if this is insecure, and a generated value will be output
|
||||
# You may use that value if you can't think of a secure key
|
||||
# - api_key=SECURE_API_KEY
|
||||
|
||||
# Pass the redirect method, if needed. TEMPORARY and PERMANENT
|
||||
# are accepted values, defaults to PERMANENT.
|
||||
# - redirect_method=TEMPORARY
|
||||
|
||||
# By default, the auto-generated pairs are adjective-name pairs.
|
||||
# If you want UIDs, please change slug_style to UID.
|
||||
# Supported values for slug_style are Pair and UID.
|
||||
# The length is 8 by default, and a minimum of 4 is allowed.
|
||||
# - slug_style=Pair
|
||||
# - slug_length=8
|
||||
|
||||
# In case you want to provide public access to adding links (and not
|
||||
# delete, or listing), change the following option to Enable.
|
||||
# - public_mode=Disable
|
||||
# In case you want to completely disable the frontend, change the following
|
||||
# to True.
|
||||
# - disable_frontend=False
|
||||
|
||||
# By default, the server sends no Cache-Control headers. You can supply a
|
||||
# comma separated list of valid header as per RFC 7234 §5.2 to send those
|
||||
# headers instead.
|
||||
# - cache_control_header=no-cache, private
|
||||
volumes:
|
||||
- db:/db
|
||||
|
||||
volumes:
|
||||
db:
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
version: "3"
|
||||
services:
|
||||
url:
|
||||
image: sintan1729/simply-shorten:latest
|
||||
restart: unless-stopped
|
||||
container_name: simply-shorten
|
||||
ports:
|
||||
- 4567:4567
|
||||
environment:
|
||||
# Change if you want to mount the database somewhere else
|
||||
# In this case, you can get rid of the db volume below
|
||||
# - db_url=/urls.sqlite
|
||||
# Change it in case you want to set the website name
|
||||
# displayed in front of the shorturls, defaults to
|
||||
# the hostname you're accessing it from
|
||||
# - site_url=https://www.example.com
|
||||
- username=admin
|
||||
- password=$3CuReP4S$W0rD
|
||||
volumes:
|
||||
- db:/urls.sqlite
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
volumes:
|
||||
db:
|
||||
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
BIN
favicon.svgz
Normal file
BIN
favicon.xcf
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +0,0 @@
|
|||
#Fri Nov 04 01:07:18 CDT 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
183
gradlew
vendored
|
@ -1,183 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or 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 UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$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 "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 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
|
||||
;;
|
||||
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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
100
gradlew.bat
vendored
|
@ -1,100 +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 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%" == "0" goto init
|
||||
|
||||
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 init
|
||||
|
||||
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
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
: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 %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="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!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
24
helm-chart/Chart.yaml
Normal file
|
@ -0,0 +1,24 @@
|
|||
apiVersion: v2
|
||||
name: chhoto-url
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
23
helm-chart/templates/ingress.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: chhoto-url
|
||||
annotations:
|
||||
cert-manager.io/issuer: "letsencrypt"
|
||||
acme.cert-manager.io/http01-edit-in-place: "true"
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.fqdn }}
|
||||
secretName: my-tls
|
||||
rules:
|
||||
- host: {{ .Values.fqdn }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: chhoto-url
|
||||
port:
|
||||
number: 80
|
18
helm-chart/templates/issuer.yml
Normal file
|
@ -0,0 +1,18 @@
|
|||
apiVersion: cert-manager.io/v1
|
||||
kind: Issuer
|
||||
metadata:
|
||||
name: letsencrypt
|
||||
spec:
|
||||
acme:
|
||||
# The ACME server URL
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
# Email address used for ACME registration
|
||||
email: {{ .Values.letsencryptmail }}
|
||||
# Name of a secret used to store the ACME account private key
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt
|
||||
# Enable the HTTP-01 challenge provider
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
ingressClassName: nginx
|
13
helm-chart/templates/pv.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: chhoto-pv
|
||||
labels:
|
||||
app: chhoto-url
|
||||
spec:
|
||||
capacity:
|
||||
storage: 100Mi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
hostPath:
|
||||
path: {{ .Values.persistence.hostPath.path }}
|
10
helm-chart/templates/secret.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: secret
|
||||
type: Opaque
|
||||
data:
|
||||
password: {{ .Values.password }}
|
||||
{{- if .Values.api_key }}
|
||||
api_key: {{ .Values.api_key }}
|
||||
{{- end }}
|
61
helm-chart/templates/sts.yml
Normal file
|
@ -0,0 +1,61 @@
|
|||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: chhoto-url
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: chhoto-url
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: chhoto-url
|
||||
spec:
|
||||
containers:
|
||||
- name: chhoto-url
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
ports:
|
||||
- containerPort: 4567
|
||||
env:
|
||||
- name: password
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: secret
|
||||
key: password
|
||||
{{- if .Values.api_key }}
|
||||
- name: api_key
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: secret
|
||||
key: api_key
|
||||
{{- end }}
|
||||
- name: db_url
|
||||
value: /db/urls.sqlite
|
||||
- name: site_url
|
||||
value: "{{ .Values.protocol }}://{{ .Values.fqdn }}"
|
||||
- name: redirect_method
|
||||
value: {{ .Values.redirect_method }}
|
||||
- name: slug_style
|
||||
value: {{ .Values.slug_style }}
|
||||
- name: slug_length
|
||||
value: "{{ .Values.slug_length }}"
|
||||
- name: public_mode
|
||||
value: {{ .Values.public_mode }}
|
||||
- name: disable_frontend
|
||||
value: {{ .Values.disable_frontend }}
|
||||
{{- if .Values.cache_control_header }}
|
||||
- name: cache_control_header
|
||||
value: {{ .Values.cache_control_header }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /db
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
resources:
|
||||
requests:
|
||||
storage: 100Mi
|
14
helm-chart/templates/svc.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: chhoto-url
|
||||
labels:
|
||||
app: chhoto-url
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 4567
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: chhoto-url
|
28
helm-chart/values.yaml
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Default values for chhoto-url.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
image:
|
||||
repository: sintan1729/chhoto-url
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "5.4.6"
|
||||
|
||||
# please use a better password in your values and base64 encode it
|
||||
password: cGFzc3dvcmQ=
|
||||
# if used, needs to be base64 encoded as well
|
||||
# api_key: U0VDVVJFX0FQSV9LRVk=
|
||||
|
||||
persistence:
|
||||
hostPath:
|
||||
path: /mnt/data/chhoto-data
|
||||
|
||||
redirect_method: PERMANENT
|
||||
slug_style: Pair
|
||||
slug_length: 8
|
||||
public_mode: Disable
|
||||
disable_frontend: False
|
||||
# cache_control_header: "no-cache, private"
|
||||
|
||||
protocol: https
|
||||
fqdn: your.short.link.url.com
|
||||
letsencryptmail: your.mail@address.com
|
BIN
resources/assets/Montserrat-VF.woff2
Normal file
BIN
resources/assets/favicon-196.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
resources/assets/favicon-32.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
resources/assets/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
37
resources/assets/favicon.svg
Normal file
After Width: | Height: | Size: 14 KiB |
87
resources/index.html
Normal file
|
@ -0,0 +1,87 @@
|
|||
<!-- SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com> -->
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
|
||||
<title>Chhoto URL</title>
|
||||
<meta name="description" content="A simple selfhosted URL shortener with no unnecessary features." />
|
||||
<meta name="keywords" content="url shortener, link shortener, self hosted, open source" />
|
||||
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" sizes="any" />
|
||||
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="assets/favicon-32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="assets/favicon-196.png" sizes="196x196" />
|
||||
|
||||
<script src="static/script.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.1/build/pure-min.css"
|
||||
integrity="sha384-oAOxQR6DkCoMliIh8yFnu25d7Eq/PHS21PClpwjOTeU2jRSq11vu66rf90/cZr47" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" type="text/css" target="_blank" href="static/styles.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container" id="container">
|
||||
<form class="pure-form pure-form-aligned" name="new-url-form">
|
||||
<fieldset>
|
||||
<legend id="logo"><img src="assets/favicon-32.png" width="26px" alt="logo"> Chhoto URL</legend>
|
||||
<div class="pure-control-group">
|
||||
<label for="longUrl">Long URL</label>
|
||||
<input type="url" name="longUrl" id="longUrl" placeholder="Please enter a valid URL"
|
||||
onblur="addProtocol(this)" required />
|
||||
</div>
|
||||
<div class=" pure-control-group">
|
||||
<label for="shortUrl">Short URL (optional)</label>
|
||||
<input type="text" name="shortUrl" id="shortUrl" placeholder="Only a-z, 0-9, - and _ are allowed"
|
||||
pattern="[a-z0-9\-_]+" title="Only a-z, 0-9, - and _ are allowed" autocapitalize="off"/>
|
||||
</div>
|
||||
<div class="pure-controls" id="controls">
|
||||
<button class="pure-button pure-button-primary">Shorten!</button>
|
||||
<p id="alert-box"> </p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<p id="loading-text">Loading links table...</p>
|
||||
<table class="pure-table" id="table-box" hidden>
|
||||
<caption>Active links</caption>
|
||||
<br />
|
||||
<thead>
|
||||
<tr>
|
||||
<td id="short-url-header">Short URL (click to copy)</td>
|
||||
<td>Long URL</td>
|
||||
<td name="hitsColumn">Hits</td>
|
||||
<td name="deleteBtn">×</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="url-table">
|
||||
<!-- The links would be inserted here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div name="links-div">
|
||||
<a id="admin-button" href="javascript:getLogin()" hidden>login</a>
|
||||
|
||||
<a id="version-number" href="https://github.com/SinTan1729/chhoto-url" target="_blank" rel="noopener noreferrer"
|
||||
hidden>Source Code</a>
|
||||
<!-- The version number would be inserted here -->
|
||||
</div>
|
||||
|
||||
<dialog id="login-dialog">
|
||||
<form class="pure-form" name="login-form">
|
||||
<p>Please enter password to access this website</p>
|
||||
<input type="password" id="password" />
|
||||
<button class="pure-button pure-button-primary" value="default">Log in</button>
|
||||
<p id="wrong-pass" hidden>Wrong password!</p>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
42
resources/static/404.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
<!-- SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com> -->
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Error 404</title>
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
|
||||
</head>
|
||||
|
||||
<style>
|
||||
body {
|
||||
text-align: center;
|
||||
}
|
||||
#quote {
|
||||
text-indent: 4em;
|
||||
}
|
||||
|
||||
/* Settings for mobile devices */
|
||||
@media (pointer:none),
|
||||
(pointer:coarse) {
|
||||
body {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<h1>Error 404!</h1>
|
||||
<div style="display: inline-block; text-align:left;">
|
||||
<p>You step in the stream,</p>
|
||||
<p>But the water has moved on.</p>
|
||||
<p>The page is not here.</p>
|
||||
<p id="quote"> — Cass Whittington</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
268
resources/static/script.js
Normal file
|
@ -0,0 +1,268 @@
|
|||
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const prepSubdir = (link) => {
|
||||
let thisPage = new URL(window.location.href);
|
||||
let subdir = thisPage.pathname;
|
||||
let out = (subdir + link).replace('//', '/');
|
||||
console.log(out);
|
||||
return (subdir + link).replace('//', '/');
|
||||
}
|
||||
|
||||
const getSiteUrl = async () => {
|
||||
let url = await fetch(prepSubdir("/api/siteurl"))
|
||||
.then(res => res.text());
|
||||
if (url == "unset") {
|
||||
return window.location.host.replace(/\/$/, '');
|
||||
}
|
||||
else {
|
||||
return url.replace(/\/$/, '').replace(/^"/, '').replace(/"$/, '');
|
||||
}
|
||||
}
|
||||
|
||||
const getVersion = async () => {
|
||||
let ver = await fetch(prepSubdir("/api/version"))
|
||||
.then(res => res.text());
|
||||
return ver;
|
||||
}
|
||||
|
||||
const showVersion = async () => {
|
||||
let version = await getVersion();
|
||||
link = document.getElementById("version-number");
|
||||
link.innerText = "v" + version;
|
||||
link.href = "https://github.com/SinTan1729/chhoto-url/releases/tag/" + version;
|
||||
link.hidden = false;
|
||||
}
|
||||
|
||||
const getLogin = async () => {
|
||||
document.getElementById("container").style.filter = "blur(2px)";
|
||||
document.getElementById("login-dialog").showModal();
|
||||
document.getElementById("password").focus();
|
||||
}
|
||||
|
||||
const refreshData = async () => {
|
||||
let res = await fetch(prepSubdir("/api/all"));
|
||||
if (!res.ok) {
|
||||
let errorMsg = await res.text();
|
||||
document.getElementById("url-table").innerHTML = '';
|
||||
console.log(errorMsg);
|
||||
if (errorMsg == "Using public mode.") {
|
||||
document.getElementById("admin-button").hidden = false;
|
||||
loading_text = document.getElementById("loading-text");
|
||||
loading_text.hidden = true;
|
||||
showVersion();
|
||||
} else {
|
||||
getLogin();
|
||||
}
|
||||
} else {
|
||||
let data = await res.json();
|
||||
displayData(data.reverse());
|
||||
}
|
||||
}
|
||||
|
||||
const displayData = async (data) => {
|
||||
showVersion();
|
||||
let site = await getSiteUrl();
|
||||
admin_button = document.getElementById("admin-button");
|
||||
admin_button.innerText = "logout";
|
||||
admin_button.href = "javascript:logOut()";
|
||||
admin_button.hidden = false;
|
||||
|
||||
table_box = document.getElementById("table-box");
|
||||
loading_text = document.getElementById("loading-text");
|
||||
const table = document.getElementById("url-table");
|
||||
|
||||
if (data.length == 0) {
|
||||
table_box.hidden = true;
|
||||
loading_text.innerHTML = "No active links.";
|
||||
loading_text.hidden = false;
|
||||
}
|
||||
else {
|
||||
loading_text.hidden = true;
|
||||
if (!window.isSecureContext) {
|
||||
const shortUrlHeader = document.getElementById("short-url-header");
|
||||
shortUrlHeader.innerHTML = "Short URL<br>(right click and copy)";
|
||||
}
|
||||
table_box.hidden = false;
|
||||
table.innerHTML = '';
|
||||
data.forEach(tr => table.appendChild(TR(tr, site)));
|
||||
}
|
||||
}
|
||||
|
||||
const showAlert = async (text, col) => {
|
||||
document.getElementById("alert-box")?.remove();
|
||||
const controls = document.getElementById("controls");
|
||||
const alertBox = document.createElement("p");
|
||||
alertBox.id = "alert-box";
|
||||
alertBox.style.color = col;
|
||||
alertBox.innerHTML = text;
|
||||
controls.appendChild(alertBox);
|
||||
}
|
||||
|
||||
const TR = (row, site) => {
|
||||
const tr = document.createElement("tr");
|
||||
const longTD = TD(A_LONG(row["longlink"]), "Long URL");
|
||||
var shortTD = null;
|
||||
var isSafari = /Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor);
|
||||
// For now, we disable copying on WebKit due to a possible bug. Manual copying is enabled instead.
|
||||
// Take a look at https://github.com/SinTan1729/chhoto-url/issues/36
|
||||
if (window.isSecureContext && !(isSafari)) {
|
||||
shortTD = TD(A_SHORT(row["shortlink"], site), "Short URL");
|
||||
}
|
||||
else {
|
||||
shortTD = TD(A_SHORT_INSECURE(row["shortlink"], site), "Short URL");
|
||||
}
|
||||
let hitsTD = TD(row["hits"]);
|
||||
hitsTD.setAttribute("label", "Hits");
|
||||
hitsTD.setAttribute("name", "hitsColumn");
|
||||
const btn = deleteButton(row["shortlink"]);
|
||||
|
||||
tr.appendChild(shortTD);
|
||||
tr.appendChild(longTD);
|
||||
tr.appendChild(hitsTD);
|
||||
tr.appendChild(btn);
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
const copyShortUrl = async (link) => {
|
||||
const site = await getSiteUrl();
|
||||
try {
|
||||
navigator.clipboard.writeText(`${site}/${link}`);
|
||||
showAlert(`Short URL ${link} was copied to clipboard!`, "light-dark(green, #72ff72)");
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
showAlert(`Could not copy short URL to clipboard, please do it manually: <a href=${site}/${link}>${site}/${link}</a>`, "light-dark(red, #ff1a1a)");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const addProtocol = (input) => {
|
||||
var url = input.value.trim();
|
||||
if (url != "" && !~url.indexOf("://") && !~url.indexOf("magnet:")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
input.value = url;
|
||||
return input;
|
||||
}
|
||||
|
||||
const A_LONG = (s) => `<a href='${s}'>${s}</a>`;
|
||||
const A_SHORT = (s, t) => `<a href="javascript:copyShortUrl('${s}');">${s}</a>`;
|
||||
const A_SHORT_INSECURE = (s, t) => `<a href="${t}/${s}">${s}</a>`;
|
||||
|
||||
const deleteButton = (shortUrl) => {
|
||||
const td = document.createElement("td");
|
||||
const div = document.createElement("div");
|
||||
const btn = document.createElement("button");
|
||||
|
||||
btn.innerHTML = "×";
|
||||
|
||||
btn.onclick = e => {
|
||||
e.preventDefault();
|
||||
if (confirm("Do you want to delete the entry " + shortUrl + "?")) {
|
||||
document.getElementById("alert-box")?.remove();
|
||||
showAlert(" ", "black");
|
||||
fetch(prepSubdir(`/api/del/${shortUrl}`), {
|
||||
method: "DELETE"
|
||||
}).then(res => {
|
||||
if (res.ok) {
|
||||
console.log("Deleted " + shortUrl);
|
||||
} else {
|
||||
console.log("Unable to delete " + shortUrl);
|
||||
}
|
||||
refreshData();
|
||||
});
|
||||
}
|
||||
};
|
||||
td.setAttribute("name", "deleteBtn");
|
||||
td.setAttribute("label", "Delete");
|
||||
div.appendChild(btn);
|
||||
td.appendChild(div);
|
||||
return td;
|
||||
}
|
||||
|
||||
const TD = (s, u) => {
|
||||
const td = document.createElement("td");
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = s;
|
||||
td.appendChild(div);
|
||||
td.setAttribute("label", u);
|
||||
return td;
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
const form = document.forms.namedItem("new-url-form");
|
||||
const data = {
|
||||
"longlink": form.elements["longUrl"].value,
|
||||
"shortlink": form.elements["shortUrl"].value,
|
||||
};
|
||||
|
||||
const url = prepSubdir("/api/new");
|
||||
let ok = false;
|
||||
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(res => {
|
||||
ok = res.ok;
|
||||
return res.text();
|
||||
})
|
||||
.then(text => {
|
||||
if (!ok) {
|
||||
showAlert(text, "light-dark(red, #ff1a1a)");
|
||||
}
|
||||
else {
|
||||
copyShortUrl(text);
|
||||
longUrl.value = "";
|
||||
shortUrl.value = "";
|
||||
refreshData();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const submitLogin = () => {
|
||||
const password = document.getElementById("password");
|
||||
fetch(prepSubdir("/api/login"), {
|
||||
method: "POST",
|
||||
body: password.value
|
||||
}).then(res => {
|
||||
if (res.ok) {
|
||||
document.getElementById("container").style.filter = "blur(0px)"
|
||||
document.getElementById("login-dialog").close();
|
||||
password.value = '';
|
||||
document.getElementById("wrong-pass").hidden = true;
|
||||
refreshData();
|
||||
} else {
|
||||
document.getElementById("wrong-pass").hidden = false;
|
||||
password.focus();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const logOut = async () => {
|
||||
let reply = await fetch(prepSubdir("/api/logout"), {method: "DELETE"}).then(res => res.text());
|
||||
console.log(reply);
|
||||
document.getElementById("table-box").hidden = true;
|
||||
document.getElementById("loading-text").hidden = false;
|
||||
refreshData();
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await refreshData();
|
||||
|
||||
const form = document.forms.namedItem("new-url-form");
|
||||
form.onsubmit = e => {
|
||||
e.preventDefault();
|
||||
submitForm();
|
||||
}
|
||||
|
||||
const login_form = document.forms.namedItem("login-form");
|
||||
login_form.onsubmit = e => {
|
||||
e.preventDefault();
|
||||
submitLogin();
|
||||
}
|
||||
})()
|
183
resources/static/styles.css
Normal file
|
@ -0,0 +1,183 @@
|
|||
/* SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com> */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
@font-face {
|
||||
font-family: Montserrat;
|
||||
src: url('/assets/Montserrat-VF.woff2');
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
body {
|
||||
color: light-dark(black, #e8e6e3);
|
||||
background-color: light-dark(white, #181a1b);
|
||||
}
|
||||
|
||||
.pure-button {
|
||||
background-color: light-dark(#0078e7, #0060b9);
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: light-dark(#cccccc, #3e4446) !important;
|
||||
box-shadow: light-dark(#dddddd, #2b2f31) 0px 1px 3px inset !important;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: light-dark(#757575, #636061);
|
||||
}
|
||||
|
||||
legend {
|
||||
color: light-dark(#333333, #c8c3bc) !important;
|
||||
border-bottom-color: light-dark(#e5e5e5 ,#373c3e) !important;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: Montserrat;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: light-dark(blue, #3391ff);
|
||||
}
|
||||
|
||||
table tr td div {
|
||||
max-height: 75px;
|
||||
line-height: 25px;
|
||||
word-wrap: break-word;
|
||||
max-width: 575px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pure-table {
|
||||
border-color: light-dark(black, #867d6e);
|
||||
}
|
||||
|
||||
.pure-table caption {
|
||||
color: light-dark(black, #e8e6e3);
|
||||
}
|
||||
|
||||
.pure-table thead {
|
||||
color: light-dark(black, #e8e6e3);
|
||||
background-color: light-dark(#e0e0e0, #2a2d2f);
|
||||
}
|
||||
|
||||
.pure-table td {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
td[name="hitsColumn"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td[name="deleteBtn"] div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
td[name="deleteBtn"] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td[name="deleteBtn"] div button {
|
||||
border-radius: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-style: solid;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
form input[name="shortUrl"]::placeholder {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
div[name="links-div"] {
|
||||
position: absolute;
|
||||
right: 0.5%;
|
||||
top: 0.5%;
|
||||
}
|
||||
|
||||
.pure-table {
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
.pure-table caption {
|
||||
font-size: 22px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
#logo {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
#password {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#login-dialog {
|
||||
border-radius: 10px;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
#login-dialog form {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#wrong-pass {
|
||||
color: light-dark(red, #ff1a1a);
|
||||
}
|
||||
|
||||
/* Settings for mobile devices */
|
||||
@media (pointer:none),
|
||||
(pointer:coarse) {
|
||||
.container {
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.pure-control-group input {
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
table tr {
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
|
||||
table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table td {
|
||||
display: flex;
|
||||
justify-content: left !important;
|
||||
width: 98vw;
|
||||
padding: .5em .1em !important;
|
||||
}
|
||||
|
||||
table td::before {
|
||||
content: attr(label);
|
||||
font-weight: bold;
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pure-table caption {
|
||||
padding-top: 0px;
|
||||
}
|
||||
}
|
BIN
screenshot-desktop.webp
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
screenshot-mobile.webp
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
screenshot.png
Before Width: | Height: | Size: 44 KiB |
|
@ -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'
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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.status(404);
|
||||
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 "";
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
package tk.sintan1729.url;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Utils {
|
||||
private static final Random random = new Random(System.currentTimeMillis());
|
||||
|
||||
private static final String SHORT_URL_PATTERN = "[a-z0-9-_]+";
|
||||
private static final Pattern PATTERN = Pattern.compile(SHORT_URL_PATTERN);
|
||||
|
||||
// The following lists are modified versions of the strings in
|
||||
// https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go
|
||||
|
||||
private static final String[] adjective = new String[] {"admiring", "adoring", "affectionate", "agitated", "amazing", "angry", "awesome", "beautiful",
|
||||
"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",
|
||||
"exciting", "fervent", "festive", "flamboyant", "focused", "friendly", "frosty", "funny", "gallant", "gifted", "goofy", "gracious",
|
||||
"great", "happy", "hardcore", "heuristic", "hopeful", "hungry", "infallible", "inspiring", "intelligent", "interesting", "jolly",
|
||||
"jovial", "keen", "kind", "laughing", "loving", "lucid", "magical", "modest", "musing", "mystifying", "naughty", "nervous", "nice",
|
||||
"nifty", "nostalgic", "objective", "optimistic", "peaceful", "pedantic", "pensive", "practical", "priceless", "quirky", "quizzical",
|
||||
"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",
|
||||
"youthful", "zealous", "zen"};
|
||||
|
||||
private static final String[] name = new String[] {"agnesi", "albattani", "allen", "almeida", "antonelli", "archimedes", "ardinghelli", "aryabhata", "austin",
|
||||
"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",
|
||||
"carver", "cauchy", "cerf", "chandrasekhar", "chaplygin", "chatelet", "chatterjee", "chaum", "chebyshev", "clarke", "cohen", "colden", "cori",
|
||||
"cray", "curie", "curran", "darwin", "davinci", "dewdney", "dhawan", "diffie", "dijkstra", "dirac", "driscoll", "dubinsky", "easley", "edison",
|
||||
"einstein", "elbakyan", "elgamal", "elion", "ellis", "engelbart", "euclid", "euler", "faraday", "feistel", "fermat", "fermi", "feynman", "franklin",
|
||||
"gagarin", "galileo", "galois", "ganguly", "gates", "gauss", "germain", "goldberg", "goldstine", "goldwasser", "golick", "goodall", "gould", "greider",
|
||||
"grothendieck", "haibt", "hamilton", "hardy", "haslett", "hawking", "heisenberg", "hellman", "hermann", "herschel", "hertz", "heyrovsky", "hodgkin",
|
||||
"hofstadter", "hoover", "hopper", "hugle", "hypatia", "ishizaka", "jackson", "jang", "jemison", "jennings", "jepsen", "johnson", "joliot", "jones",
|
||||
"kalam", "kapitsa", "kare", "keldysh", "keller", "kepler", "khayyam", "khorana", "kilby", "kirch", "knuth", "kowalevski", "lalande", "lamarr",
|
||||
"lamport", "leakey", "leavitt", "lederberg", "lehmann", "lewin", "lichterman", "liskov", "lovelace", "lumiere", "mahavira", "margulis", "matsumoto",
|
||||
"maxwell", "mayer", "mccarthy", "mcclintock", "mclaren", "mclean", "mcnulty", "meitner", "mendel", "mendeleev", "meninsky", "merkle", "mestorf",
|
||||
"mirzakhani", "montalcini", "moore", "morse", "moser", "murdock", "napier", "nash", "neumann", "newton", "nightingale", "nobel", "noether", "northcutt",
|
||||
"noyce", "panini", "pare", "pascal", "pasteur", "payne", "perlman", "pike", "poincare", "poitras", "proskuriakova", "ptolemy", "raman", "ramanujan",
|
||||
"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",
|
||||
"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"};
|
||||
|
||||
public static boolean validate(String shortUrl) {
|
||||
return PATTERN.matcher(shortUrl)
|
||||
.matches();
|
||||
}
|
||||
|
||||
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)];
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 9.1 KiB |
|
@ -1,102 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Simply Shorten</title>
|
||||
<link rel="icon" type="image/png" href="assets/favicon-32.png" sizes="32x32">
|
||||
<link rel="shortcut icon" type="image/png" href="assets/favicon-196.png" sizes="196x196">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="assets/favicon.ico">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.1/build/pure-min.css"
|
||||
integrity="sha384-oAOxQR6DkCoMliIh8yFnu25d7Eq/PHS21PClpwjOTeU2jRSq11vu66rf90/cZr47" crossorigin="anonymous">
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 950px;
|
||||
margin: 20px auto auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table tr td div {
|
||||
max-height: 75px;
|
||||
line-height: 25px;
|
||||
word-wrap: break-word;
|
||||
max-width: 575px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
td[name="deleteBtn"] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td[name="deleteBtn"] button {
|
||||
border-radius: 50%;
|
||||
border-style: solid;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
form input[name="shortUrl"] {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
form input[name="shortUrl"]::placeholder {
|
||||
text-transform: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<form class="pure-form pure-form-aligned" name="new-url-form">
|
||||
<fieldset>
|
||||
<legend style="font-size: 32px;"><img src="assets/favicon-32.png" width="26px" alt="logo"> Simply
|
||||
Shorten</legend>
|
||||
<div class="pure-control-group">
|
||||
<label for="longUrl">Long URL</label>
|
||||
<input type="url" name="longUrl" id="longUrl" placeholder="Please enter a valid URL" required />
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
<label for="shortUrl">Short URL (optional)</label>
|
||||
<input type="text" name="shortUrl" id="shortUrl" placeholder="Only a-z, 0-9, - and _ are allowed"
|
||||
pattern="[A-Za-z0-9_-]+" />
|
||||
</div>
|
||||
<div class="pure-controls">
|
||||
<button class="pure-button pure-button-primary">Shorten!</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<table class="pure-table" style="visibility: hidden;">
|
||||
<caption style="font-size: 22px; text-align: left; font-style: normal;">Active links</caption>
|
||||
<br>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Short URL (click to copy)</td>
|
||||
<td>Long URL</td>
|
||||
<td>Hits</td>
|
||||
<td name="deleteBtn">×</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="url-table">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,141 +0,0 @@
|
|||
const getSiteUrl = async () => await fetch("/api/site")
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
if (text == "unset") {
|
||||
return window.location.host;
|
||||
}
|
||||
else {
|
||||
return text;
|
||||
}
|
||||
});
|
||||
|
||||
const refreshData = async () => {
|
||||
let data = await fetch("/api/all").then(res => res.text());
|
||||
data = data
|
||||
.split("\n")
|
||||
.filter(line => line !== "")
|
||||
.map(line => line.split(","))
|
||||
.map(arr => ({
|
||||
short: arr[0],
|
||||
long: arr[1],
|
||||
hits: arr[2]
|
||||
}));
|
||||
|
||||
displayData(data);
|
||||
};
|
||||
|
||||
const displayData = async (data) => {
|
||||
let site = await getSiteUrl();
|
||||
site = site.replace(/(^\w+:|^)\/\//, '');
|
||||
table_box = document.querySelector(".pure-table");
|
||||
if (data.length == 0) {
|
||||
table_box.style.visibility = "hidden";
|
||||
}
|
||||
else {
|
||||
const table = document.querySelector("#url-table");
|
||||
table_box.style.visibility = "visible";
|
||||
table.innerHTML = ''; // Clear
|
||||
data.forEach(tr => table.appendChild(TR(tr, site)));
|
||||
}
|
||||
};
|
||||
|
||||
const addAlertBox = async (text, col) => {
|
||||
document.getElementById("alertBox")?.remove();
|
||||
const controls = document.querySelector(".pure-controls");
|
||||
const alertBox = document.createElement("p");
|
||||
alertBox.setAttribute("id", "alertBox");
|
||||
alertBox.setAttribute("style", `color:${col}`);
|
||||
alertBox.innerHTML = text;
|
||||
controls.appendChild(alertBox);
|
||||
};
|
||||
|
||||
const TR = (row, site) => {
|
||||
const tr = document.createElement("tr");
|
||||
const longTD = TD(A(row.long));
|
||||
const shortTD = TD(A_INT(row.short, site));
|
||||
const hitsTD = TD(row.hits);
|
||||
const btn = deleteButton(row.short);
|
||||
|
||||
tr.appendChild(shortTD);
|
||||
tr.appendChild(longTD);
|
||||
tr.appendChild(hitsTD);
|
||||
tr.appendChild(btn);
|
||||
|
||||
return tr;
|
||||
};
|
||||
|
||||
const copyShortUrl = async (link) => {
|
||||
const site = await getSiteUrl();
|
||||
navigator.clipboard.writeText(`${site}/${link}`);
|
||||
addAlertBox(`Short URL ${link} copied to clipboard!`, "green");
|
||||
};
|
||||
|
||||
const A = (s) => `<a href='${s}'>${s}</a>`;
|
||||
const A_INT = (s, t) => `<a href="javascript:copyShortUrl('${s}');">${t}/${s}</a>`;
|
||||
|
||||
const deleteButton = (shortUrl) => {
|
||||
const td = document.createElement("td");
|
||||
const btn = document.createElement("button");
|
||||
|
||||
btn.innerHTML = "×";
|
||||
|
||||
btn.onclick = e => {
|
||||
e.preventDefault();
|
||||
if (confirm("Do you want to delete the entry " + shortUrl + "?")) {
|
||||
document.getElementById("alertBox")?.remove();
|
||||
fetch(`/api/${shortUrl}`, {
|
||||
method: "DELETE"
|
||||
}).then(_ => refreshData());
|
||||
}
|
||||
};
|
||||
td.setAttribute("name", "deleteBtn");
|
||||
td.appendChild(btn);
|
||||
return td;
|
||||
};
|
||||
|
||||
const TD = (s) => {
|
||||
const td = document.createElement("td");
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = s;
|
||||
td.appendChild(div);
|
||||
return td;
|
||||
};
|
||||
|
||||
const submitForm = () => {
|
||||
const form = document.forms.namedItem("new-url-form");
|
||||
const longUrl = form.elements["longUrl"];
|
||||
const shortUrl = form.elements["shortUrl"];
|
||||
|
||||
const url = `/api/new`;
|
||||
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
body: `${longUrl.value};${shortUrl.value}`
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
addAlertBox("Short URL not valid or already in use!", "red");
|
||||
return "error";
|
||||
}
|
||||
else {
|
||||
return res.text();
|
||||
}
|
||||
}).then(text => {
|
||||
if (text != "error") {
|
||||
copyShortUrl(text);
|
||||
longUrl.value = "";
|
||||
shortUrl.value = "";
|
||||
refreshData();
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await refreshData();
|
||||
const form = document.forms.namedItem("new-url-form");
|
||||
form.onsubmit = e => {
|
||||
e.preventDefault();
|
||||
submitForm();
|
||||
}
|
||||
})();
|