1
0
Fork 0
mirror of https://github.com/SinTan1729/chhoto-url synced 2025-04-19 11:20:00 -05:00

Compare commits

..

331 commits
2.1 ... main

Author SHA1 Message Date
Sayantan Santra
d198135144
Merge pull request #48 from ronnyworm/add-helm-chart
add helm chart and add instructions for its usage in README
2025-04-10 15:03:07 -05:00
b838a6e027
new: Added disable_frontend to variables in helm-chart 2025-04-10 15:01:50 -05:00
Ronny Worm
0897b6b63b add other options from docker-compose 2025-04-10 21:46:39 +02:00
97b56c40ae
docs: Added instructions for disabling the frontend 2025-04-10 14:17:22 -05:00
2c8f47c0cb
new: Ability to disable frontend 2025-04-10 12:45:49 -05:00
828019998e
build: Bumped version to 5.6.3 2025-04-07 22:24:49 -05:00
49d910fb3c
build: Updated deps to mitigate a tokio security issue 2025-04-07 22:24:10 -05:00
c521ad1120
docs: Added some more options and info in the compose file 2025-04-02 17:17:59 -05:00
Ronny Worm
63020b2c24 add helm chart and add intructions for its usage in README 2025-03-23 21:55:40 +01:00
Sayantan Santra
d42a738861
docs: Updated README.md 2025-03-17 18:23:27 -05:00
e3eaf5aba8
docs: Updated screenshots 2025-03-17 18:22:25 -05:00
3b48ce7b5e
chg: Simplify how wrong password text is shown 2025-03-05 15:56:12 -06:00
5363a1b056
docs: Added info about dark mode 2025-03-05 15:22:58 -06:00
0d58e626a4
fix: Hide the disabled wrong password text area in dialog 2025-03-04 00:11:34 -06:00
e8faf660f4
build: Bumped version to 5.6.2 2025-03-03 18:45:08 -06:00
67695da86b
fix: Use changed methods for rand 2025-03-03 18:44:42 -06:00
d50c183c9c
build: Updated deps 2025-03-03 18:37:49 -06:00
90b04b1f21
fix: Link colors for dark mode 2025-03-03 18:34:57 -06:00
babf3d8911
new: Automatic dark mode support 2025-03-03 18:10:53 -06:00
1ae00eb3a8
chg: Some cosmetic changes to login dialog 2025-03-03 14:16:38 -06:00
6f419c7b3d
new: Enforce ordering of data
Closes #46
Data is returned in order of id, which should match the order it was
inserted in. In the WebUI, the entries are shown in reverse, so the
latest link is at the top.
2025-03-03 12:27:59 -06:00
c557b8b262
docs: Change to github link for extension 2025-01-30 01:03:06 -06:00
a63222a71a
docs: Add a few words 2025-01-30 00:59:05 -06:00
86cea6278f
docs: Added mention of extension 2025-01-28 01:39:44 -06:00
f283991740
build: Bumped version to 5.6.1 2025-01-17 23:30:43 -06:00
Sayantan Santra
1775f71347
Merge pull request #42 from SolninjaA/main
Correctly output created link
2025-01-17 23:28:30 -06:00
0b1224f8e5
docs: Improve clarification of the port variables 2025-01-17 23:27:33 -06:00
Solninja A
1047763285 chg: Bind server to port env variable 2025-01-18 12:52:19 +10:00
Solninja A
fc785c3eef Re-comment the API key in compose.yaml 2025-01-14 18:16:37 +10:00
Solninja A
17d0df943b Correctly output created link 2025-01-14 17:20:41 +10:00
SolomonTechnology
7b52bd60da
Rewording 2025-01-14 00:33:01 +10:00
SolomonTechnology
db8417d919
Improve documentation for the "port" and "site_url" env variables 2025-01-14 00:25:49 +10:00
af1685bb70
build: Bumped version to 5.6.0 2025-01-09 00:34:22 +05:30
Sayantan Santra
a5621acfe4
Merge pull request #40 from SinTan1729/get-longlink
Get longlink
2025-01-09 00:30:58 +05:30
1be89db43b
docs: Add info about expand route, and put API as preferred method 2025-01-09 00:27:05 +05:30
a60853fd21
fix: Only pull hits when needed 2025-01-09 00:21:05 +05:30
2b9fafe440
new: Got the expand API path working
It replies with info for a single shortlink. May be useful for
applications using json interface.
2025-01-08 20:09:24 +05:30
f952cb88a0
build: Bumped version to 5.5.0 2025-01-06 11:59:41 +05:30
9eec252fe2
build: Updated deps 2025-01-06 11:54:49 +05:30
Sayantan Santra
f8f4dae457
Merge pull request #39 from SolninjaA/main
Improvements of the API system
2025-01-06 11:52:48 +05:30
16bc211f9f
fix: Confirm when secure API key is provided 2025-01-06 11:48:18 +05:30
cca5bcfa1a
docs: Add example command to generate API key 2025-01-06 11:47:01 +05:30
cba667ded8
chg: Small cosmetic change 2025-01-06 11:40:20 +05:30
1d9a8c202d
build: Add API_KEY variable in Makefile 2025-01-06 11:17:10 +05:30
eb4f05a87b
fix: Disregard empty Site URL 2025-01-06 11:11:09 +05:30
5183279cab
docs: Small changes to the README 2025-01-05 16:25:08 +05:30
f1c1642976
chg: Small semantic changes 2025-01-05 16:20:38 +05:30
Solninja A
eed3c2292a Cleaned up code 2025-01-03 00:28:51 +10:00
Solninja A
4fb8d0cb5c Edited the API Key header to comply with OpenAPI v3 specs 2025-01-03 00:25:55 +10:00
Solninja A
9a0cdec646 Improved API error codes 2025-01-01 19:08:35 +10:00
Solninja A
818dadb84f Made code more Rust-like 2025-01-01 17:34:09 +10:00
Solninja A
247cfb0476 Fixed compose.yaml 2024-12-31 20:32:46 +10:00
Solninja A
6347a89725 Minor code clean up 2024-12-31 20:30:55 +10:00
Solninja A
9ddf043c17 Fix typos, etc 2024-12-31 20:17:13 +10:00
Solninja A
a1f8700664 Change README.md 2024-12-31 20:15:06 +10:00
Solninja A
aab7a9b3d1 Change README.md and remove unneeded dependencies 2024-12-31 20:13:37 +10:00
Solninja A
1ef5d539d5 Improve API error handling 2024-12-31 19:54:22 +10:00
Solninja A
5c2886f651 Changes the API to use JSON data and results 2024-12-31 19:11:47 +10:00
Solninja A
2c56c68637 Improves API functionality 2024-12-31 16:19:20 +10:00
756d675f06
fix: Capitalization, fixes #37 2024-12-30 18:41:48 +05:30
e6eed2dd70
build: Bumped version to 5.4.6 2024-11-07 19:35:42 -06:00
37a5300015
fix: Disable copying to clipboard on WebKit, fixes #36
This disables clipboard copying and lets the user
manually copy the links.
2024-11-07 19:33:34 -06:00
66d94634d9
build: Bumped version to 5.4.5 2024-11-06 22:11:36 -06:00
03f5529c30
build: Updated deps 2024-11-06 22:11:05 -06:00
f772475d96
fix: Do not autocapitalize shorturl on mobile devices 2024-11-06 21:57:56 -06:00
8b8ceca313
chg: Remove lowercasing of shorturl from the CSS, fixes #35
This makes the behavior more uniform across different banned characters.
2024-11-03 01:17:53 -05:00
201d0b319f
chg: Move the font to assets 2024-10-25 14:47:15 -05:00
733ef6ea67
docs: Added note about Dark Reader 2024-10-06 20:38:05 -05:00
cf5909c888
fix: Use a simpler password to make the shell happy 2024-10-05 00:26:38 -05:00
dcb3144b22
chg: Added a better compose file 2024-10-05 00:24:16 -05:00
e0c61bdb93
build: Bumped version to 5.4.4 2024-10-03 00:02:48 -05:00
06f7a33d5d
fix: Do not consider empty password 2024-10-02 23:52:23 -05:00
514e905299
chg: Updated instructions in the compose file 2024-10-02 23:46:56 -05:00
3688692c7a
chg: Default db location 2024-10-02 23:46:35 -05:00
a7cf0cdf30
build: Bumped version to 5.4.3 2024-09-18 11:52:24 -05:00
35880f4d1e
build: Updated dependencies 2024-09-18 11:51:50 -05:00
Sayantan Santra
0d0da1141b
Merge pull request #27 from yilmaz08/main
Fix simple dockerfile errors
2024-08-28 11:49:51 -05:00
4a8b62446c
fix: Case mismatch 2024-08-28 11:37:45 -05:00
Abdürrahim YILMAZ
855145d4d7 fix: argument was not being passed
Argument was not being passed, although it is declared. So changed to a
static path in "FROM scratch" part

Error message before the fix:

Step 17/19 : COPY --from=builder
/chhoto-url/target/$target/release/chhoto-url /chhoto-url
COPY failed: stat chhoto-url/target//release/chhoto-url: file does not
exist
2024-08-28 03:12:33 +03:00
Abdürrahim YILMAZ
59f679a1c2 fix: copy destination path
Error message before the fix:

Step 4/19 : COPY ./actix/Cargo.toml ./actix/Cargo.lock .

When using COPY with more than one source file, the destination must be
a directory and end with a /
2024-08-28 03:04:37 +03:00
5213a9df2e
docs: Added a docker image size badge 2024-06-21 10:01:19 -05:00
3e6f482533
docs: Updated the screenshots 2024-06-21 09:46:47 -05:00
56ab16aa4e
build: Bumped version to 5.4.2 2024-06-20 20:59:06 -05:00
066fa9c80a
build: Updated deps 2024-06-20 20:48:06 -05:00
9fc8634704
fix: Typo during reading slug_length, fixes #24 2024-06-20 20:46:26 -05:00
5bbaad3001
build: Bumped version to 5.4.1 2024-06-07 15:34:28 -05:00
892959d49d
build: Updated deps 2024-06-07 15:33:58 -05:00
9948ce713c
chg: Some small changes to the html pages
Should be nicer on mobile devices
2024-06-01 00:48:40 -05:00
Sayantan Santra
70d9d828c5
Update bug_report.md 2024-06-01 00:36:14 -05:00
114a97a273
build: Bumped version to 5.4.0 2024-05-31 20:31:29 -05:00
96495b037d
new: Made the Cache-Control headers fully configurable 2024-05-31 20:30:09 -05:00
69fc25a264
build: Add port settings to the docker-test recipe 2024-05-31 11:29:41 -05:00
f19f3249cc
docs: Changed a word in the README 2024-05-31 01:51:11 -05:00
2cf0e5d2de
new: Ability to disable Cache-Control headers in testing recipe 2024-05-31 01:45:19 -05:00
de9bc130d5
docs: Add info about configuring Cache-Control header 2024-05-31 01:42:51 -05:00
8ff4c3f24f
chg: Make the Cache-Control header configurable 2024-05-31 01:41:55 -05:00
eab1c9bc73
new: Added cache-control header, fixes #22 2024-05-31 01:24:33 -05:00
0b50a7c261
chg: Move all the services into their own file 2024-05-30 14:45:18 -05:00
e55c6f82b4
fix: Make the link clickable when securecontext is missing 2024-05-29 19:14:31 -05:00
6992d27390
build: Bumped version to 5.3.1 2024-05-29 16:12:31 -05:00
39e4d2df74
chg: Show the whole link when clipboard is not accessible 2024-05-29 16:03:03 -05:00
41b7e37819
build: Bumped version to 5.3.0 2024-05-29 10:45:42 -05:00
e67e0a88cd
build: Updated deps 2024-05-29 10:45:15 -05:00
00ade1af40
fix: Do not allow empty values for db_url or site_url 2024-05-29 10:40:51 -05:00
b1632c4c87
fix: Fixed some UI visibility related stuff 2024-05-29 10:40:51 -05:00
3445d5366a
fix: Properly handle visibility of elements 2024-05-29 10:40:51 -05:00
Sayantan Santra
5fb8587628
Merge pull request #21 from SinTan1729/public-mode
Enable support for public mode, closes #20
2024-05-29 00:02:38 -05:00
6cdacda510
build: Make testing more configurable using env variables 2024-05-28 18:20:20 -05:00
ed2be0e883
docs: Some documentation about public-mode 2024-05-28 18:20:09 -05:00
0fce881654
new: Got public mode working, testing needed 2024-05-28 18:16:58 -05:00
168cff94a2
build: Bumped version to 5.2.6 2024-05-06 16:49:47 -05:00
b1c4142296
chg: Reply when shortlink cannot be added 2024-05-06 16:49:01 -05:00
75912e8f9d
build: Updated deps 2024-05-06 16:41:31 -05:00
5ac822d5f9
fix: Middleware order 2024-04-11 13:01:33 -05:00
233247e154
build: Bumped version to 5.2.5 2024-04-10 14:47:31 -05:00
108abc2b30
build: Updated deps 2024-04-10 14:43:34 -05:00
4a158bbea6
docs: Made some changes to the badges 2024-04-10 13:46:29 -05:00
51c6817487
chg: Change the button in login dialog 2024-04-10 13:33:30 -05:00
Sayantan Santra
0192c26fd0
Merge pull request #17 from SinTan1729/dependabot/cargo/actix/h2-0.3.26
build(deps): bump h2 from 0.3.25 to 0.3.26 in /actix
2024-04-05 11:46:10 -05:00
dependabot[bot]
8b815e1bbb
build(deps): bump h2 from 0.3.25 to 0.3.26 in /actix
Bumps [h2](https://github.com/hyperium/h2) from 0.3.25 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.25...v0.3.26)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-05 16:23:49 +00:00
3ad05f1e63
docs: Added mobile friendly UI in README 2024-04-03 20:50:27 -05:00
931b4a95e4
chg: Added SPDX headers 2024-04-03 20:40:26 -05:00
cdc3508a0c
chg: Added use std::io::Result 2024-04-03 17:40:59 -05:00
e742c0ab5e
fix: Use proper HTTP response codes 2024-04-03 16:19:39 -05:00
231fd3c8ca
build: Bumped version to 5.2.4 2024-04-02 21:28:02 -05:00
86ec787d96
fix: Errors in script.js 2024-04-02 21:26:18 -05:00
30c0b8b50a
fix: Typo in script.js 2024-04-02 21:02:11 -05:00
62ae71f4ca
build: Bumped version to 5.2.3 2024-04-02 18:12:43 -05:00
e9bb9d0b65
new: Use samesite cookies 2024-04-02 18:07:29 -05:00
ca14c02e70
docs: Added some comments and changed the token name 2024-04-02 17:57:49 -05:00
0469f9b933
chg: Get rid of naked unwraps and improve code flow 2024-04-02 17:43:36 -05:00
f27984a63f
docs: Edit README 2024-04-01 13:43:03 -05:00
7ad874a1ff
chg: Cleaned up the js code 2024-04-01 02:07:43 -05:00
604e95aa9c
chg: More readable response when not logged in 2024-04-01 00:58:18 -05:00
917be6ade4
docs: Fix typo, and slight change in grammar 2024-04-01 00:47:25 -05:00
2594051a54
build: Bumped version to 5.2.2 2024-03-31 21:24:38 -05:00
38b817fdf8
new: Proper reply when invalid data is sent 2024-03-31 21:23:22 -05:00
a9168e3459
docs: Added CLI usage instructions 2024-03-31 21:07:31 -05:00
d48b664c0a
chg: Updated the screenshot 2024-03-31 16:39:47 -05:00
a0f0eb5280
build: Added docker-stop make entry 2024-03-31 16:11:45 -05:00
34518affaf
chg: Changed some struct names 2024-03-31 16:07:33 -05:00
4f80b1b522
build: Bumped version to 5.2.1 2024-03-31 15:40:30 -05:00
Sayantan Santra
f38abdf1fb
Merge pull request #15 from SinTan1729/json
chg: Use json while sending form for new url
2024-03-31 15:39:31 -05:00
f2b5e1ab6d
chg: Use json for sending url list 2024-03-31 15:38:59 -05:00
99b5298cd8
chg: Use json while sending form for new url 2024-03-31 15:17:54 -05:00
6659452c51
docs: Do not use quotes around variable examples 2024-03-31 01:17:13 -05:00
Sayantan Santra
3441d3ae90
Merge pull request #12 from SinTan1729/nanoid
Support uid slugs
2024-03-31 01:01:33 -05:00
f6060eb649
build: Bumped version to 5.2.0 2024-03-31 00:59:58 -05:00
6c7ca8d0ac
fix: slug_style options are consistent now 2024-03-31 00:59:05 -05:00
599b013fc9
chg: Change uid to UID 2024-03-31 00:51:32 -05:00
841f877ee8
docs: Include info about UID in docs 2024-03-31 00:51:10 -05:00
088cd594a5
chg: Change the env_var name 2024-03-31 00:45:29 -05:00
f9e642275a
new: Support uid slug 2024-03-31 00:38:42 -05:00
9a520c122e
chg: Disable column separators and right align hits 2024-03-31 00:12:48 -05:00
3a712f812a
build: Bumped version to 5.1.0 2024-03-29 15:41:13 -05:00
Sayantan Santra
753b73c4a3
Merge pull request #10 from SinTan1729/multi-arch
Multi-arch builds
2024-03-29 15:38:48 -05:00
fbcb088260
chg: Updated deps 2024-03-28 13:28:28 -05:00
f526e7ec5b
chg: 404 response doesn't change the url 2024-03-28 13:27:33 -05:00
5d8dd6fb63
chg: Use Option instead of returning empty String 2024-03-26 23:52:24 -05:00
5e4db14ea2
docs: Some changes to the README 2024-03-26 17:46:26 -05:00
c76b39dc16
fix: Typo in Makefile, and break lines 2024-03-25 17:28:24 -05:00
731cb41646
chg: Added stopping the docker image to make clean 2024-03-25 16:44:47 -05:00
0cfa674029
fix: Typo in Makefile 2024-03-25 16:08:04 -05:00
1f18766f79
build: Switch workflow to only use Makefile 2024-03-25 16:02:52 -05:00
dfefff2703
docs: Added a note about building 2024-03-25 01:40:10 -05:00
6d3d220cff
build: Fix the docker_push_script 2024-03-25 01:26:13 -05:00
629e66a57c
docs: Updated build instructions 2024-03-25 01:22:54 -05:00
ce76f04f35
build: Support specifying targets with build-arg 2024-03-25 01:10:09 -05:00
db5d1f72bd
chg: Use username as a variable in Makefile 2024-03-25 01:05:06 -05:00
e54aa3b33b
build: Use different dockerfiles for single and multi-arch builds 2024-03-25 01:00:25 -05:00
82559d38fd
chg: Nicer multi-arch builds and also add armv7 2024-03-25 00:56:39 -05:00
2baa481040
chg: Change the push script to work with the new setup 2024-03-24 23:33:41 -05:00
ffb4846239
fix: Multi-arch upload 2024-03-24 23:27:31 -05:00
5bd174d287
build: Switched to a cross-rt based multi-arch build process 2024-03-24 23:03:18 -05:00
9221c3e371
chg: Add support for specifying the architecture 2024-03-24 19:38:33 -05:00
c5cfba85f9
chg: Allow specifying target in the Dockerfile 2024-03-24 19:12:38 -05:00
d6dcd2f18d
fix: Remove surrounding quotes for siteurl 2024-03-24 16:43:27 -05:00
d278021e1b
fix: Support working inside subdirectories 2024-03-23 18:08:25 -05:00
8dbb6e9bd6
new: Added a dev tag option 2024-03-23 16:07:55 -05:00
cde3fb4c89
chg: Switch to woff2 fonts 2024-03-22 11:10:04 -05:00
f3d5e2cf50
chg: Do not force the use of big fonts 2024-03-19 15:44:59 -05:00
fd74a941d9
build: Bumped version to 5.0.8 2024-03-18 19:28:09 -05:00
2cab341e8b
fix: Broken table 2024-03-18 19:27:58 -05:00
2a2ed7e41a
fix: Screen overflow for table in mobile UI 2024-03-18 18:59:34 -05:00
5a5a1bc775
build: Bumped version to 5.0.7 2024-03-18 16:04:06 -05:00
b66086be38
fix: Centering on mobile UI 2024-03-18 16:02:45 -05:00
3be2862e9f
fix: Width issues on mobile 2024-03-18 15:23:12 -05:00
b0603f62b4
chg: Make the table a little wide 2024-03-18 02:07:46 -05:00
9d5bc2d0fd
build: Bumped version to 5.0.6 2024-03-18 02:00:07 -05:00
94af81b802
fix: Mobile UI 2024-03-18 01:59:31 -05:00
03154fd010
build: Bumped version to 5.0.5 2024-03-18 01:23:45 -05:00
4c394c8004
chg: Change the font to Montserrat 2024-03-18 01:22:17 -05:00
9d46546e44
chg: Some small changes to the UI 2024-03-17 01:47:01 -05:00
c07bb5c25f
build: Bumped version to 5.0.4 2024-03-15 23:21:15 -05:00
85f150b543
build: Updated dependencies 2024-03-15 23:14:28 -05:00
d235f1aea7
new: Added some validation check in client side 2024-03-15 23:05:56 -05:00
13d613093b
build: Change order of pushing the builds, looks nicer lol 2024-03-14 00:47:27 -05:00
71ac19028e
docs: Remove the cargo build instructions 2024-03-13 19:26:10 -05:00
d8ee2ce658
build: Bumped version to 5.0.3 2024-03-13 18:59:46 -05:00
5ecd29926d
chg: Added some checks during deletion of a shortlink 2024-03-13 18:59:10 -05:00
aa097ad982
fix: bash if issue 2024-03-10 18:44:11 -05:00
bf0944a5fd
build: Bumped version to 5.0.2 2024-03-10 18:39:05 -05:00
6693941985
docs: Updated the issue templates 2024-03-10 18:22:22 -05:00
0bfaa49e7b
fix: Wrong regex in validate_link, fixes #6 2024-03-10 18:18:07 -05:00
a32f00c36e
fix: Logo in README 2024-03-08 13:33:44 -06:00
8b1af0a169
chg: Moved the resources folder out of actix since it isn't Rust 2024-03-08 10:41:52 -06:00
9fdb634d71
new: Added a convenience script for publishing 2024-03-07 14:30:41 -06:00
a5574683a6
docs: Change NPM recommendation to caddy 2024-03-06 22:42:06 -06:00
d1ac769fc1
build: Use cargo-chef to potentially speed up builds 2024-03-06 19:24:36 -06:00
8ba8472940
docs: Edited the features section 2024-03-04 19:37:36 -06:00
Sayantan Santra
f331370767
Update issue templates 2024-03-04 19:17:00 -06:00
cb8f6f3e1d
fix: Link to the older base project 2024-03-04 19:10:47 -06:00
4d3e52cc95
docs: Added some stats and goals 2024-03-04 19:05:11 -06:00
8f0a6f0fc6
build: Bump version to 5.0.1 2024-03-04 17:05:34 -06:00
Sayantan Santra
15ff8819a7
Merge pull request #5 from SinTan1729/dependabot/cargo/actix/mio-0.8.11
Bump mio from 0.8.6 to 0.8.11 in /actix
2024-03-04 17:03:38 -06:00
dependabot[bot]
501f7f1d65
Bump mio from 0.8.6 to 0.8.11 in /actix
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.6 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.6...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 21:45:41 +00:00
bd47f3c74b
chg: Expand the name explanation 2024-02-29 18:29:36 -06:00
31212ab252
docs: Updated the screenshot 2024-02-13 00:56:30 -06:00
deff47db2c
build: Bumped version number to 5.0.0 2024-02-10 19:42:19 -06:00
95a8263797
new: Show version number 2024-02-10 19:41:50 -06:00
0227c5f783
docs: Explain the name 2024-02-10 18:44:15 -06:00
4a8385955b
chg: Renamed the project
When starting out, I kept the name same as the original project.
But I think I've made significant changes, so keeping the name
same would be disingenuous.
2024-02-10 18:29:50 -06:00
1f9cf1d777
chg: Untrack some unnecessary files 2024-02-08 19:09:07 -06:00
2a85189155
build: Bumped version to 4.4.0 2024-02-08 11:34:26 -06:00
caa6c58fd2
build: Switch to a static build to mitigate incompatible packages 2024-02-08 11:33:26 -06:00
583700cdbf
build: Updated minimum deps 2024-02-08 10:04:12 -06:00
432328b97e
doc: Fix the license 2024-01-30 22:08:04 -06:00
Sayantan Santra
bcf30049a2
Merge pull request #4 from SinTan1729/dependabot/cargo/actix/h2-0.3.24
Bump h2 from 0.3.17 to 0.3.24 in /actix
2024-01-30 22:01:02 -06:00
dependabot[bot]
bfd7e111d9
Bump h2 from 0.3.17 to 0.3.24 in /actix
Bumps [h2](https://github.com/hyperium/h2) from 0.3.17 to 0.3.24.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.17...v0.3.24)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-19 16:10:34 +00:00
0bd8fbe96c
build: Bump version to 4.3.6 2023-10-08 18:23:05 -05:00
0c80a0ac21
build: Update deps 2023-10-08 18:22:45 -05:00
f6255566b0
chg: Allow insecure cookies 2023-10-08 18:20:34 -05:00
57d390a129
build: Bump version to 4.3.5 2023-09-22 15:10:32 -05:00
acb67fdcf6
build: Bump dependencies 2023-09-22 15:08:12 -05:00
Sayantan Santra
9e8a9395a5
Merge pull request #2 from SinTan1729/dependabot/cargo/actix/aes-gcm-0.10.3
Bump aes-gcm from 0.10.1 to 0.10.3 in /actix
2023-09-22 16:28:41 +00:00
dependabot[bot]
c77763c1c2
Bump aes-gcm from 0.10.1 to 0.10.3 in /actix
Bumps [aes-gcm](https://github.com/RustCrypto/AEADs) from 0.10.1 to 0.10.3.
- [Commits](https://github.com/RustCrypto/AEADs/compare/aes-gcm-v0.10.1...aes-gcm-v0.10.3)

---
updated-dependencies:
- dependency-name: aes-gcm
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-22 16:12:48 +00:00
79cca4bc26
build: Bump version to 4.3.4 2023-09-19 18:11:47 -05:00
1ee7ebe847
fix: Correct order for random names 2023-09-19 18:11:29 -05:00
ac82396584
change: Spelling 2023-06-24 17:39:21 -05:00
bed96d6de5
build: Bumped version to 4.3.3 2023-06-03 18:50:31 -05:00
7550d197b4
new: Show loading and no links text 2023-06-03 18:49:59 -05:00
a4e7c6b444
change: Do not dereference str whenever possible 2023-05-23 19:03:53 -05:00
7b59a9aa5c
change: Improve consistency in string declaration whenever suitable 2023-05-23 19:03:47 -05:00
ff0a17e57b
build: Bumped version to 4.3.2 2023-05-23 19:03:42 -05:00
e3520be75b
fix: Handle most wrong routes 2023-05-22 00:44:27 -05:00
1c91855820 Bumped version to 4.3.1 2023-04-28 12:16:37 -05:00
74f3fddd01 Fixed the 404 error page display 2023-04-28 12:12:31 -05:00
8be3e54861 Bumped version to 4.3.0 2023-04-28 11:40:48 -05:00
f3984624d9 Add option for temporary redirection 2023-04-28 00:22:30 -05:00
ca01676916 Use 404 response code when appropriate 2023-04-28 00:08:16 -05:00
ff4801a476 Fixes suggested by clippy 2023-04-26 14:40:54 -05:00
Sayantan Santra
2c34598faf
Merge pull request #1 from SinTan1729/dependabot/cargo/actix/h2-0.3.17
Bump h2 from 0.3.16 to 0.3.17 in /actix
2023-04-14 17:19:39 -05:00
8325f0c2d2 Improve UI for mobile 2023-04-14 17:18:18 -05:00
dependabot[bot]
b657919a82
Bump h2 from 0.3.16 to 0.3.17 in /actix
Bumps [h2](https://github.com/hyperium/h2) from 0.3.16 to 0.3.17.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.16...v0.3.17)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-13 17:09:32 +00:00
a91bcc0503 Use more direct calls instead of setAttribute 2023-04-11 19:45:00 -05:00
5e4f6aa1fc Bumped version to 4.1.0 2023-04-11 19:18:11 -05:00
e14fc1f3b5 Don't include resources during build for better caching 2023-04-11 19:17:42 -05:00
abe8238713 Hide password when entering 2023-04-11 19:17:04 -05:00
d22d88b985 Updated LICENSE 2023-04-10 20:08:11 -05:00
43dab10d22 Some minor cosmetic changes 2023-04-10 15:20:20 -05:00
cdfcb901fa Added some info in Cargo.toml and bumped version to 4.0.2 2023-04-10 12:01:15 -05:00
66ea86b6d3 Enabled logging of warnings 2023-04-10 11:51:20 -05:00
e18a71a73c Reorganize files, keep table from moving on alert 2023-04-10 11:31:34 -05:00
2f48b1e58c Added badges in README 2023-04-09 21:35:51 -05:00
7e6e419dbc Updated README 2023-04-09 17:31:00 -05:00
6b1d82bded Do not ask for password if none is provided 2023-04-09 17:19:54 -05:00
3e07e9aadb Rearrange the resources 2023-04-09 16:38:14 -05:00
a9c21a214f Remove .gitlab-ci.yml file 2023-04-09 00:24:53 -05:00
0add351480 Remove username since it's no longer used 2023-04-08 23:13:09 -05:00
3a2b1acf49 Remove redundant EXPOSE from DockerFile 2023-04-08 18:19:37 -05:00
cc066c0716 Updated README 2023-04-08 17:22:59 -05:00
ab5a032269 Updated CI/CD to use cache 2023-04-08 16:33:03 -05:00
d831bd65e0 Bumped version 2023-04-08 16:02:32 -05:00
c3aa7438e9 Merge branch 'rust-backend' into 'main'
Complete rewrite of backend in Rust

See merge request SinTan1729/simply-shorten!3
2023-04-08 20:59:25 +00:00
4edf7349b0 Updated README 2023-04-08 15:56:42 -05:00
0ddd75502d Reverted the default urls.sqlite location 2023-04-08 15:56:29 -05:00
b779019716 Removed some debug code 2023-04-08 15:41:23 -05:00
66868effba Updated the compose.yaml 2023-04-08 15:39:09 -05:00
96b3ae1297 Password login enabled 2023-04-08 15:36:33 -05:00
34b2b116ba Auth basic working - unsafe 2023-04-08 02:52:16 -05:00
7d5ff40893 Updated .gitignore 2023-04-03 21:48:49 -05:00
51e75f726e Added GZip compression 2023-04-03 19:24:33 -05:00
a14dbf5cd2 Use env variable for opening database 2023-04-03 18:58:36 -05:00
71d7253248 Use one database connection throghout 2023-04-03 18:52:17 -05:00
e4ff2df3f1 Ability to delete links from database 2023-04-03 17:58:19 -05:00
98d10cfd5b Fixed counting hits 2023-04-03 17:41:52 -05:00
7f6ba5175f Ability to add new links 2023-04-03 17:40:37 -05:00
618fd0e53a Count number of hits 2023-04-03 15:46:22 -05:00
046c6d63a4 Updated README 2023-04-03 13:52:01 -05:00
3c699d8b02 Support for providing site_url 2023-04-03 13:50:23 -05:00
a1f73c8a9d Display list of links 2023-04-03 11:55:27 -05:00
b9d76b6734 Basic redirection working 2023-04-02 22:26:23 -05:00
0e97516759 Rearrangement 2023-04-02 16:53:55 -05:00
d9f7e9537d Delete all the java backend stuff 2023-04-02 15:13:02 -05:00
7acc336d4c Proper formatting in the 404 page 2023-03-31 20:08:11 -05:00
e45ecd8eef Update .gitlab-ci.yml file 2023-02-28 17:47:29 -06:00
6b8ae8657f Small changes to the 404 page 2023-02-04 23:57:00 -06:00
d7d93d1873 Changed 404 page 2023-01-17 00:27:30 +05:30
c887b15a38 Fix typo in README 2023-01-13 13:33:15 +05:30
f6279cd313 Added link to source code 2023-01-13 13:23:48 +05:30
36c2678e40 Merge branch 'java-17-update' into 'main'
Updated to Java 17

See merge request SinTan1729/simply-shorten!2
2023-01-12 17:59:48 +00:00
214f2af0b0 Updated to Java 17 2023-01-12 23:27:37 +05:30
635d51a659 Updated README 2022-12-28 19:01:03 +05:30
3aac41d3a7 Changed 404 message 2022-12-28 19:00:50 +05:30
cb56d31abe Add svg favicons and some meta tags 2022-11-16 00:05:31 -06:00
41e6231217 Made compose file adhere to official specs 2022-11-14 23:12:26 -06:00
c8118a4c96 Vectorize the favicon 2022-11-14 20:35:56 -06:00
272dc38933 Fix for magnet links 2022-11-13 00:06:29 -06:00
94aece0d1d Don't show the full short url 2022-11-12 23:01:21 -06:00
5c8307c57a Bumped dependencies 2022-11-12 20:12:39 -06:00
cfdc7a7644 Custom 404 page 2022-11-12 20:12:25 -06:00
cc15ffa463 Updated README.md 2022-11-12 18:50:10 -06:00
9f68f2632f Removed docker badge from README 2022-11-12 18:45:17 -06:00
8956519a10 Renamed one folder 2022-11-12 18:01:15 -06:00
b803f5805d Proper error handling for when clipboard is unavailable 2022-11-12 17:57:14 -06:00
2c8076e2e1 Automatically add https if no protocol specified for longurl 2022-11-11 18:52:22 -06:00
99073bfd6c Changed name of folder, project etc. 2022-11-11 17:50:12 -06:00
3598199957 For random shorturl adjective-name pair is used 2022-11-11 17:33:31 -06:00
e5f1839822 Code cleanup 2022-11-10 23:19:42 -06:00
53ba9eb129 Do not show url scheme for short url 2022-11-10 20:31:25 -06:00
deeba64e74 Allow setting site_url 2022-11-10 20:17:39 -06:00
e2656ff94e Clicking on shortUrl copies it and bug fix 2022-11-10 19:01:21 -06:00
d196c0373f Copy shortUrl after adding to db 2022-11-10 18:11:57 -06:00
68ae439f5b Disable forced lowercase for placeholder 2022-11-09 23:44:30 -06:00
e88cc1f738 Updated screenshot 2022-11-09 22:05:11 -06:00
a9af9fddbb Remove the ugly shadow in delete button 2022-11-09 21:49:33 -06:00
c81573c3de Rounded delete buttons 2022-11-09 19:26:17 -06:00
a21933d3eb Improve display of url table 2022-11-09 18:55:50 -06:00
b73148706d Confirm before deleting entries 2022-11-09 17:58:15 -06:00
77f835430b Add logo in interface 2022-11-09 17:46:54 -06:00
47c47e91dd Add logo in README 2022-11-09 17:46:43 -06:00
60 changed files with 4239 additions and 1040 deletions

View file

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

6
.gitattributes vendored
View file

@ -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
View 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.

View 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
View file

@ -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

View file

@ -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

View file

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

View file

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

View file

@ -1,14 +1,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
View 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"]

View file

@ -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
View 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

264
README.md
View file

@ -1,54 +1,86 @@
[![Docker Pulls](https://img.shields.io/docker/pulls/sintan1729/simply-shorten?style=for-the-badge)](https://hub.docker.com/r/sintan1729/simply-shorten)
<!-- SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com> -->
<!-- SPDX-License-Identifier: MIT -->
A fork of [this project](https://gitlab.com/draganczukp/simply-shorten).
[![docker-pulls-badge](https://img.shields.io/docker/pulls/sintan1729/chhoto-url)](https://hub.docker.com/r/sintan1729/chhoto-url)
[![maintainer-badge](https://img.shields.io/badge/maintainer-SinTan1729-blue)](https://github.com/SinTan1729)
[![latest-release-badge](https://img.shields.io/github/v/release/SinTan1729/chhoto-url?label=latest%20release)](https://github.com/SinTan1729/chhoto-url/releases/latest)
![docker-image-size-badge](https://img.shields.io/docker/image-size/sintan1729/chhoto-url)
![commit-since-latest-release-badge](https://img.shields.io/github/commits-since/SinTan1729/chhoto-url/latest?sort=semver&label=commits%20since%20latest%20release)
[![license-badge](https://img.shields.io/github/license/SinTan1729/chhoto-url)](https://spdx.org/licenses/MIT.html)
# ![Logo](resources/assets/favicon-32.png) <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
- 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
![Screenshot](./screenshot.png)
# 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
```
@ -56,72 +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'
```
### 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 password="password" \
-v ./urls.sqlite:/urls.sqlite \
-e db_url=/urls.sqlite \
-e site_url="https://www.example.com" \
-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
- 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).

2100
actix/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

40
actix/Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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.")
)
}
}

View file

@ -1,33 +0,0 @@
plugins {
// Apply the java plugin to add support for Java
id 'java'
// Apply the application plugin to add support for building a CLI application.
id 'application'
}
repositories {
jcenter()
}
jar {
manifest {
attributes "Main-Class": "tk.draganczuk.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.draganczuk.url.App'
}

75
compose.yaml Normal file
View 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:

View file

@ -1,26 +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
- username=admin
- password=$3CuReP4S$W0rD
volumes:
- db:/urls.sqlite
networks:
- proxy
volumes:
db:
networks:
proxy:
external: true

BIN
favicon.svgz Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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
View file

@ -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
View file

@ -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
View 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"

View 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

View 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

View 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 }}

View 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 }}

View 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

View 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
View 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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

87
resources/index.html Normal file
View 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">&nbsp;</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">&times;</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>
&nbsp;
<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
View 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
View 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 = "&times;";
btn.onclick = e => {
e.preventDefault();
if (confirm("Do you want to delete the entry " + shortUrl + "?")) {
document.getElementById("alert-box")?.remove();
showAlert("&nbsp;", "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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
screenshot-mobile.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View file

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

View file

@ -1,43 +0,0 @@
package tk.draganczuk.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("/:shortUrl", Routes::goToLongUrl);
}
}

View file

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

View file

@ -1,68 +0,0 @@
package tk.draganczuk.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();
if (body.endsWith(";")) {
body = body + "$";
}
var split = body.split(";");
String longUrl = split[0];
if (split[1].equals("$")) {
split[1] = Utils.randomString();
}
String shortUrl = split[1];
shortUrl = shortUrl.toLowerCase();
var shortUrlPresent = urlRepository
.findForShortUrl(shortUrl);
if (shortUrlPresent.isEmpty() && 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 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 "";
}
}

View file

@ -1,117 +0,0 @@
package tk.draganczuk.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 "";
}
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();
}
}
}

View file

@ -1,40 +0,0 @@
package tk.draganczuk.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);
public static String randomString() {
int leftLimit = 48; // numeral '0'
int rightLimit = 122; // letter 'z'
int targetStringLength = 10;
return random.ints(leftLimit, rightLimit + 1)
.filter(i -> (i <= 57 || i >= 97))
.limit(targetStringLength)
.collect(StringBuilder::new,
StringBuilder::appendCodePoint,
StringBuilder::append)
.toString();
}
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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

View file

@ -1,84 +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%;
}
input {
width: 65%;
}
table tr td div {
max-height: 75px;
line-height: 25px;
word-wrap: break-word;
max-width: 575px;
overflow: auto;
}
form input[name="shortUrl"] {
text-transform: lowercase;
}
</style>
</head>
<body>
<div class="container">
<form class="pure-form pure-form-aligned" name="new-url-form">
<fieldset>
<legend>Add new 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" 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">Submit</button>
</div>
</fieldset>
</form>
<table class="pure-table">
<thead>
<tr>
<td>Short URL</td>
<td>Long URL</td>
<td>Hits</td>
<td></td>
</tr>
</thead>
<tbody id="url-table">
</tbody>
</table>
</div>
</body>
</html>

View file

@ -1,108 +0,0 @@
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 = (data) => {
const table = document.querySelector("#url-table");
table.innerHTML = ''; // Clear
data.map(TR)
.forEach(tr => table.appendChild(tr));
};
const addErrBox = async () => {
const controls = document.querySelector(".pure-controls");
const errBox = document.createElement("p");
errBox.setAttribute("id", "errBox");
errBox.setAttribute("style", "color:red");
errBox.innerHTML = "Short URL not valid or already in use!";
controls.appendChild(errBox);
}
const TR = (row) => {
const tr = document.createElement("tr");
const longTD = TD(A(row.long));
const shortTD = TD(A_INT(row.short));
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 A = (s) => `<a href='${s}'>${s}</a>`;
const A_INT = (s) => `<a href='/${s}'>${window.location.host}/${s}</a>`;
const deleteButton = (shortUrl) => {
const btn = document.createElement("button");
btn.innerHTML = "&times;";
btn.onclick = e => {
e.preventDefault();
fetch(`/api/${shortUrl}`, {
method: "DELETE"
}).then(_ => refreshData());
};
return btn;
};
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) {
if (document.getElementById("errBox") == null) {
addErrBox();
}
}
else {
document.getElementById("errBox")?.remove();
longUrl.value = "";
shortUrl.value = "";
refreshData();
}
});
};
(async () => {
await refreshData();
const form = document.forms.namedItem("new-url-form");
form.onsubmit = e => {
e.preventDefault();
submitForm();
}
})();