Compare commits
10 Commits
1075ab0ed1
...
a4cfb64155
Author | SHA1 | Date | |
---|---|---|---|
a4cfb64155 | |||
84f5f57ade | |||
c733d4c98f | |||
a4193f830f | |||
938be8949f | |||
2faf289829 | |||
21b18aac54 | |||
3b7d5454cc | |||
f080854b84 | |||
2405c9cf31 |
147
Cargo.lock
generated
147
Cargo.lock
generated
@ -36,6 +36,21 @@ version = "0.2.16"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
|
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.79"
|
version = "0.1.79"
|
||||||
@ -168,6 +183,12 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.15.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -194,19 +215,38 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chela"
|
name = "chela"
|
||||||
version = "0.3.0"
|
version = "1.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"chrono",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"eyre",
|
"eyre",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
"info_utils",
|
"info_utils",
|
||||||
"serde",
|
"serde",
|
||||||
"sqids",
|
"sqids",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-targets 0.52.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color-eyre"
|
name = "color-eyre"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
@ -240,6 +280,12 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@ -721,6 +767,29 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.60"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ident_case"
|
name = "ident_case"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -774,6 +843,15 @@ version = "1.0.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
||||||
|
dependencies = [
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@ -1447,6 +1525,7 @@ dependencies = [
|
|||||||
"atoi",
|
"atoi",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"either",
|
"either",
|
||||||
@ -1529,6 +1608,7 @@ dependencies = [
|
|||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"digest",
|
"digest",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
@ -1570,6 +1650,7 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"etcetera",
|
"etcetera",
|
||||||
@ -1605,6 +1686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
|
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
|
"chrono",
|
||||||
"flume",
|
"flume",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -1951,6 +2033,60 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.58",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.58",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "0.25.4"
|
version = "0.25.4"
|
||||||
@ -1967,6 +2103,15 @@ dependencies = [
|
|||||||
"wasite",
|
"wasite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
|
11
Cargo.toml
11
Cargo.toml
@ -1,17 +1,22 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chela"
|
name = "chela"
|
||||||
version = "0.3.0"
|
version = "1.4.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
license = "BSL-1.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.7.5"
|
axum = { version = "0.7.5", features = ["tokio"] }
|
||||||
|
chrono = { version = "0.4.37", features = ["serde"] }
|
||||||
color-eyre = "0.6.3"
|
color-eyre = "0.6.3"
|
||||||
eyre = "0.6.12"
|
eyre = "0.6.12"
|
||||||
|
hyper = "1.2.0"
|
||||||
|
hyper-util = { version = "0.1.3", features = ["tokio"] }
|
||||||
info_utils = "2.2.3"
|
info_utils = "2.2.3"
|
||||||
serde = "1.0.197"
|
serde = "1.0.197"
|
||||||
sqids = "0.4.1"
|
sqids = "0.4.1"
|
||||||
sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "macros", "migrate", "tls-rustls"] }
|
sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "macros", "migrate", "tls-rustls", "chrono"] }
|
||||||
tokio = { version = "1.37.0", features = ["full"] }
|
tokio = { version = "1.37.0", features = ["full"] }
|
||||||
|
tower = "0.4.13"
|
||||||
url = { version = "2.5.0", features = ["serde"] }
|
url = { version = "2.5.0", features = ["serde"] }
|
||||||
|
23
LICENSE
Normal file
23
LICENSE
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
Boost Software License - Version 1.0 - August 17th, 2003
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
obtaining a copy of the software and accompanying documentation covered by
|
||||||
|
this license (the "Software") to use, reproduce, display, distribute,
|
||||||
|
execute, and transmit the Software, and to prepare derivative works of the
|
||||||
|
Software, and to permit third-parties to whom the Software is furnished to
|
||||||
|
do so, all subject to the following:
|
||||||
|
|
||||||
|
The copyright notices in the Software and this entire statement, including
|
||||||
|
the above license grant, this restriction and the following disclaimer,
|
||||||
|
must be included in all copies of the Software, in whole or in part, and
|
||||||
|
all derivative works of the Software, unless such copies or derivative
|
||||||
|
works are solely in the form of machine-executable object code generated by
|
||||||
|
a source language processor.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
26
README.md
26
README.md
@ -7,6 +7,8 @@ Chela is a minimal URL shortener built in Rust. It is named after the small claw
|
|||||||
## Usage
|
## Usage
|
||||||
You can create a redirect by navigating to the `/create` page and filling out the form. By default, every path passed to Chela will be treated as a redirect except `/` and `/create`.
|
You can create a redirect by navigating to the `/create` page and filling out the form. By default, every path passed to Chela will be treated as a redirect except `/` and `/create`.
|
||||||
|
|
||||||
|
Chela also supports basic analytics for shortened URLs. This page is available at `/tracking`, and `/tracking/<URL ID>`.
|
||||||
|
|
||||||
## Install and Run
|
## Install and Run
|
||||||
### With Docker
|
### With Docker
|
||||||
#### CLI
|
#### CLI
|
||||||
@ -63,6 +65,15 @@ A page that Chela will redirect to when `/` is requested instead of replying wit
|
|||||||
##### `CHELA_BEHIND_PROXY`
|
##### `CHELA_BEHIND_PROXY`
|
||||||
If this variable is set, Chela will use the `X-Real-IP` header as the client IP address rather than the connection address.
|
If this variable is set, Chela will use the `X-Real-IP` header as the client IP address rather than the connection address.
|
||||||
|
|
||||||
|
##### `CHELA_UNIX_SOCKET`
|
||||||
|
If you would like Chela to listen for HTTP requests over a Unix socket, set this variable to the socket path that it should use. By default, Chela will listen via a Tcp socket.
|
||||||
|
|
||||||
|
##### `CHELA_ALPHABET`
|
||||||
|
If this variable is set, Chela will use the characters in `CHELA_ALPHABET` to create IDs for URLs. The default alphabet is `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`. See [here](https://sqids.org/faq#unique) for more information on Sqids alphabets.
|
||||||
|
|
||||||
|
##### `CHELA_USES_HTTPS`
|
||||||
|
If this variable is set, Chela will refer to itself as `https://$CHELA_HOST` instead of the default `http://$CHELA_HOST`.
|
||||||
|
|
||||||
### Manually
|
### Manually
|
||||||
#### Build
|
#### Build
|
||||||
```bash
|
```bash
|
||||||
@ -80,16 +91,16 @@ $ ./target/release/chela
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Hosting
|
## Hosting
|
||||||
Chela uses the [axum](https://crates.io/crates/axum) to manage HTTP requests, so it is possible to expose it directly to the outer internet. However, there is no authentication for the `/create` endpoint so anyone will be able to create redirects.
|
Chela uses the [axum](https://crates.io/crates/axum) to manage HTTP requests, so it is possible to expose it directly to the outer internet. However, there is no authentication for the `/create` or `/tracking` endpoints so anyone will be able to create redirects and view analytics.
|
||||||
|
|
||||||
If you would prefer to be the only one able to create redirects, then you can proxy Chela through Nginx with http-basic-auth. Refer to [this](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/) documentation for more information.
|
If you would prefer to be the only one able to access these pages, then you can proxy Chela through Nginx with http-basic-auth. Refer to [this](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/) documentation for more information.
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
server {
|
server {
|
||||||
server_name example.com;
|
server_name a.com;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://localhost:3000/;
|
proxy_pass http://localhost:3000;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|
||||||
limit_except GET HEAD {
|
limit_except GET HEAD {
|
||||||
@ -97,5 +108,12 @@ server {
|
|||||||
auth_basic_user_file /path/to/your/.htpasswd;
|
auth_basic_user_file /path/to/your/.htpasswd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /tracking {
|
||||||
|
proxy_pass http://localhost:3000$request_uri;
|
||||||
|
|
||||||
|
auth_basic 'Restricted';
|
||||||
|
auth_basic_user_file /path/to/your/.htpasswd;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
329
src/get.rs
329
src/get.rs
@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::hash_map::HashMap;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
use axum::extract::{ConnectInfo, Path};
|
use axum::extract::{ConnectInfo, Path};
|
||||||
@ -9,8 +10,16 @@ use axum::Extension;
|
|||||||
use info_utils::prelude::*;
|
use info_utils::prelude::*;
|
||||||
|
|
||||||
use crate::ServerState;
|
use crate::ServerState;
|
||||||
|
use crate::TrackingRow;
|
||||||
|
use crate::UdsConnectInfo;
|
||||||
use crate::UrlRow;
|
use crate::UrlRow;
|
||||||
|
|
||||||
|
enum TrackingParameter {
|
||||||
|
Ip,
|
||||||
|
Referrer,
|
||||||
|
UserAgent,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn index(Extension(state): Extension<ServerState>) -> impl IntoResponse {
|
pub async fn index(Extension(state): Extension<ServerState>) -> impl IntoResponse {
|
||||||
if let Some(redirect) = state.main_page_redirect {
|
if let Some(redirect) = state.main_page_redirect {
|
||||||
return Redirect::temporary(redirect.as_str()).into_response();
|
return Redirect::temporary(redirect.as_str()).into_response();
|
||||||
@ -34,6 +43,30 @@ pub async fn index(Extension(state): Extension<ServerState>) -> impl IntoRespons
|
|||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn id_unix(
|
||||||
|
headers: HeaderMap,
|
||||||
|
ConnectInfo(addr): ConnectInfo<UdsConnectInfo>,
|
||||||
|
Extension(state): Extension<ServerState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let ip = if state.behind_proxy {
|
||||||
|
match headers.get("x-real-ip") {
|
||||||
|
Some(it) => {
|
||||||
|
if let Ok(i) = it.to_str() {
|
||||||
|
Some(i.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(format!("{:?}", addr.peer_addr))
|
||||||
|
}
|
||||||
|
.unwrap_or_default();
|
||||||
|
run_id(headers, ip, state, id).await
|
||||||
|
}
|
||||||
|
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Will panic if `parse()` fails
|
/// Will panic if `parse()` fails
|
||||||
pub async fn id(
|
pub async fn id(
|
||||||
@ -41,9 +74,19 @@ pub async fn id(
|
|||||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||||
Extension(state): Extension<ServerState>,
|
Extension(state): Extension<ServerState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let ip = get_ip(&headers, addr, &state).unwrap_or_default();
|
||||||
|
run_id(headers, ip, state, id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_id(
|
||||||
|
headers: HeaderMap,
|
||||||
|
ip: String,
|
||||||
|
state: ServerState,
|
||||||
|
id: String,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let mut show_request = false;
|
let mut show_request = false;
|
||||||
log!("Request for '{}' from {}", id.clone(), addr.ip());
|
log!("Request for '{}' from {}", id.clone(), ip);
|
||||||
let mut use_id = id;
|
let mut use_id = id;
|
||||||
if use_id.ends_with('+') {
|
if use_id.ends_with('+') {
|
||||||
show_request = true;
|
show_request = true;
|
||||||
@ -65,7 +108,7 @@ pub async fn id(
|
|||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
log!("Redirecting {} -> {}", it.id, it.url);
|
log!("Redirecting {} -> {}", it.id, it.url);
|
||||||
save_analytics(headers, it.clone(), addr, state).await;
|
save_analytics(headers, it.clone(), ip, state).await;
|
||||||
let mut response_headers = HeaderMap::new();
|
let mut response_headers = HeaderMap::new();
|
||||||
response_headers.insert("Cache-Control", "private, max-age=90".parse().unwrap());
|
response_headers.insert("Cache-Control", "private, max-age=90".parse().unwrap());
|
||||||
response_headers.insert("Location", it.url.parse().unwrap());
|
response_headers.insert("Location", it.url.parse().unwrap());
|
||||||
@ -91,22 +134,8 @@ pub async fn id(
|
|||||||
(StatusCode::NOT_FOUND, Html("<pre>Not found.</pre>")).into_response()
|
(StatusCode::NOT_FOUND, Html("<pre>Not found.</pre>")).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_analytics(headers: HeaderMap, item: UrlRow, addr: SocketAddr, state: ServerState) {
|
async fn save_analytics(headers: HeaderMap, item: UrlRow, ip: String, state: ServerState) {
|
||||||
let id = item.id;
|
let id = item.id;
|
||||||
let ip: Option<String> = if state.behind_proxy {
|
|
||||||
match headers.get("x-real-ip") {
|
|
||||||
Some(it) => {
|
|
||||||
if let Ok(i) = it.to_str() {
|
|
||||||
Some(i.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Some(addr.ip().to_string())
|
|
||||||
};
|
|
||||||
let referer = match headers.get("referer") {
|
let referer = match headers.get("referer") {
|
||||||
Some(it) => {
|
Some(it) => {
|
||||||
if let Ok(i) = it.to_str() {
|
if let Ok(i) = it.to_str() {
|
||||||
@ -142,7 +171,24 @@ VALUES ($1,$2,$3,$4)
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
if res.is_ok() {
|
if res.is_ok() {
|
||||||
log!("Saved analytics for '{id}' from {}", ip.unwrap_or_default());
|
log!("Saved analytics for '{id}' from {}", ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ip(headers: &HeaderMap, addr: SocketAddr, state: &ServerState) -> Option<String> {
|
||||||
|
if state.behind_proxy {
|
||||||
|
match headers.get("x-real-ip") {
|
||||||
|
Some(it) => {
|
||||||
|
if let Ok(i) = it.to_str() {
|
||||||
|
Some(i.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(addr.ip().to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,3 +220,250 @@ pub async fn create_id(Extension(state): Extension<ServerState>) -> Html<String>
|
|||||||
state.host
|
state.host
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn tracking(Extension(state): Extension<ServerState>) -> impl IntoResponse {
|
||||||
|
let url_rows: Vec<UrlRow> = sqlx::query_as("SELECT * FROM chela.urls")
|
||||||
|
.fetch_all(&state.db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let html = format!(
|
||||||
|
r#"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{} Tracking</title>
|
||||||
|
</head>
|
||||||
|
<style>{}</style>
|
||||||
|
<body>
|
||||||
|
{}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#,
|
||||||
|
state.host,
|
||||||
|
table_css(),
|
||||||
|
make_table_from_urls(&url_rows)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Html(html).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn tracking_id(
|
||||||
|
Extension(state): Extension<ServerState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let tracking_rows: Vec<TrackingRow> =
|
||||||
|
sqlx::query_as("SELECT * FROM chela.tracking WHERE id = $1")
|
||||||
|
.bind(id.clone())
|
||||||
|
.fetch_all(&state.db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let url: UrlRow = sqlx::query_as("SELECT * FROM chela.urls WHERE id = $1")
|
||||||
|
.bind(id.clone())
|
||||||
|
.fetch_one(&state.db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let html = format!(
|
||||||
|
r#"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{} Tracking {}</title>
|
||||||
|
</head>
|
||||||
|
<style>{}</style>
|
||||||
|
<body>
|
||||||
|
<h1>Tracking for <a href="{}">{}</a> from ID '{}'</h1>
|
||||||
|
<h2>Visited {} times</h2>
|
||||||
|
{}
|
||||||
|
|
||||||
|
<h2>By IP</h2>
|
||||||
|
{}
|
||||||
|
<h2>By Referrer</h2>
|
||||||
|
{}
|
||||||
|
<h2>By User Agent</h2>
|
||||||
|
{}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#,
|
||||||
|
state.host,
|
||||||
|
id,
|
||||||
|
table_css(),
|
||||||
|
url.url,
|
||||||
|
url.url,
|
||||||
|
url.id,
|
||||||
|
tracking_rows.len(),
|
||||||
|
make_table_from_tracking(&tracking_rows),
|
||||||
|
make_grouped_table_from_tracking(&tracking_rows, TrackingParameter::Ip),
|
||||||
|
make_grouped_table_from_tracking(&tracking_rows, TrackingParameter::Referrer),
|
||||||
|
make_grouped_table_from_tracking(&tracking_rows, TrackingParameter::UserAgent)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Html(html).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_table_from_tracking(rows: &Vec<TrackingRow>) -> String {
|
||||||
|
let mut html = r#"<table>
|
||||||
|
<colgroup>
|
||||||
|
<col>
|
||||||
|
<col>
|
||||||
|
<col>
|
||||||
|
<col>
|
||||||
|
<col>
|
||||||
|
</colgroup>
|
||||||
|
<tr>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>Referrer</th>
|
||||||
|
<th>User Agent</th>
|
||||||
|
</tr>
|
||||||
|
"#
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
html += &format!(
|
||||||
|
r#"
|
||||||
|
<tr>
|
||||||
|
<td>{}</td>
|
||||||
|
<td>{}</td>
|
||||||
|
<td>{}</td>
|
||||||
|
<td>{}</td>
|
||||||
|
<td>{}</td>
|
||||||
|
</tr>
|
||||||
|
"#,
|
||||||
|
row.timestamp,
|
||||||
|
row.id,
|
||||||
|
row.ip.as_ref().unwrap_or(&String::default()),
|
||||||
|
row.referrer.as_ref().unwrap_or(&String::default()),
|
||||||
|
row.user_agent.as_ref().unwrap_or(&String::default())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
html += r#"
|
||||||
|
</table>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
html
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_grouped_table_from_tracking(rows: &Vec<TrackingRow>, group: TrackingParameter) -> String {
|
||||||
|
let column_name = match group {
|
||||||
|
TrackingParameter::Ip => "IP",
|
||||||
|
TrackingParameter::Referrer => "Referrer",
|
||||||
|
TrackingParameter::UserAgent => "User Agent",
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut html = format!(
|
||||||
|
r#"
|
||||||
|
<table>
|
||||||
|
<colgroup>
|
||||||
|
<col>
|
||||||
|
<col>
|
||||||
|
</colgroup>
|
||||||
|
<tr>
|
||||||
|
<th>Occurrences</th>
|
||||||
|
<th>{}</th>
|
||||||
|
</tr>
|
||||||
|
"#,
|
||||||
|
column_name
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut aggregate: HashMap<String, u32> = HashMap::new();
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let tracker = match group {
|
||||||
|
TrackingParameter::Ip => {
|
||||||
|
let v = match &row.ip {
|
||||||
|
Some(val) => val,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
v
|
||||||
|
}
|
||||||
|
TrackingParameter::Referrer => {
|
||||||
|
let v = match &row.referrer {
|
||||||
|
Some(val) => val,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
v
|
||||||
|
}
|
||||||
|
TrackingParameter::UserAgent => {
|
||||||
|
let v = match &row.user_agent {
|
||||||
|
Some(val) => val,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
v
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let count = aggregate.get(tracker).unwrap_or(&0);
|
||||||
|
aggregate.insert(tracker.to_string(), count + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, val) in aggregate {
|
||||||
|
html += &format!(
|
||||||
|
r#"
|
||||||
|
<tr>
|
||||||
|
<td>{}</td>
|
||||||
|
<td>{}</td>
|
||||||
|
</tr>
|
||||||
|
"#,
|
||||||
|
val, key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
html += r#"
|
||||||
|
</table>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
html
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_table_from_urls(urls: &Vec<UrlRow>) -> String {
|
||||||
|
let mut html = r#"<table>
|
||||||
|
<colgroup>
|
||||||
|
<col>
|
||||||
|
<col>
|
||||||
|
<col>
|
||||||
|
<col>
|
||||||
|
</colgroup>
|
||||||
|
<tr>
|
||||||
|
<th>Index</th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>URL</th>
|
||||||
|
<th>Custom ID</th>
|
||||||
|
</tr>
|
||||||
|
"#
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
for url in urls {
|
||||||
|
html += &format!(
|
||||||
|
r#"
|
||||||
|
<tr>
|
||||||
|
<td>{}</td>
|
||||||
|
<td><a href="/tracking/{}">{}</a></td>
|
||||||
|
<td><a href="{}">{}</a></td>
|
||||||
|
<td>{}</td>
|
||||||
|
</tr>
|
||||||
|
"#,
|
||||||
|
url.index, url.id, url.id, url.url, url.url, url.custom_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
html += r#"
|
||||||
|
</table>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
html
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table_css() -> String {
|
||||||
|
r#"
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
159
src/main.rs
159
src/main.rs
@ -1,18 +1,23 @@
|
|||||||
use std::net::SocketAddr;
|
use axum::extract::connect_info;
|
||||||
|
use axum::http::Request;
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use sqlx::{Pool, Postgres};
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
use sqids::Sqids;
|
use hyper::body::Incoming;
|
||||||
|
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||||
use serde::Deserialize;
|
use hyper_util::server;
|
||||||
|
|
||||||
use info_utils::prelude::*;
|
use info_utils::prelude::*;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqids::Sqids;
|
||||||
|
use tower::Service;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub mod get;
|
pub mod get;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
@ -24,6 +29,7 @@ pub struct ServerState {
|
|||||||
pub sqids: Sqids,
|
pub sqids: Sqids,
|
||||||
pub main_page_redirect: Option<Url>,
|
pub main_page_redirect: Option<Url>,
|
||||||
pub behind_proxy: bool,
|
pub behind_proxy: bool,
|
||||||
|
pub uses_https: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, sqlx::FromRow, PartialEq, Eq)]
|
#[derive(Debug, Clone, sqlx::FromRow, PartialEq, Eq)]
|
||||||
@ -31,6 +37,16 @@ pub struct UrlRow {
|
|||||||
pub index: i64,
|
pub index: i64,
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
pub custom_id: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, sqlx::FromRow, PartialEq, Eq)]
|
||||||
|
pub struct TrackingRow {
|
||||||
|
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub id: String,
|
||||||
|
pub ip: Option<String>,
|
||||||
|
pub referrer: Option<String>,
|
||||||
|
pub user_agent: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
@ -39,41 +55,116 @@ pub struct CreateForm {
|
|||||||
pub url: url::Url,
|
pub url: url::Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct UdsConnectInfo {
|
||||||
|
pub peer_addr: Arc<tokio::net::unix::SocketAddr>,
|
||||||
|
pub peer_cred: tokio::net::unix::UCred,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl connect_info::Connected<&tokio::net::UnixStream> for UdsConnectInfo {
|
||||||
|
fn connect_info(target: &tokio::net::UnixStream) -> Self {
|
||||||
|
let peer_addr = target.peer_addr().unwrap();
|
||||||
|
let peer_cred = target.peer_cred().unwrap();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
peer_addr: Arc::new(peer_addr),
|
||||||
|
peer_cred,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> eyre::Result<()> {
|
async fn main() -> eyre::Result<()> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
|
|
||||||
let db_pool = init_db().await?;
|
let db_pool = init_db().await?;
|
||||||
let host = std::env::var("CHELA_HOST").unwrap_or("localhost".to_string());
|
let host = env::var("CHELA_HOST").unwrap_or("localhost".to_string());
|
||||||
|
let alphabet = env::var("CHELA_ALPHABET")
|
||||||
|
.unwrap_or("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".to_string());
|
||||||
let sqids = Sqids::builder()
|
let sqids = Sqids::builder()
|
||||||
.alphabet(
|
.alphabet(alphabet.chars().collect())
|
||||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
.blocklist(["create".to_string(), "tracking".to_string()].into())
|
||||||
.chars()
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
.blocklist(["create".to_string()].into())
|
|
||||||
.build()?;
|
.build()?;
|
||||||
let main_page_redirect = std::env::var("CHELA_MAIN_PAGE_REDIRECT").unwrap_or_default();
|
let main_page_redirect = env::var("CHELA_MAIN_PAGE_REDIRECT").unwrap_or_default();
|
||||||
let behind_proxy = std::env::var("CHELA_BEHIND_PROXY").is_ok();
|
let behind_proxy = env::var("CHELA_BEHIND_PROXY").is_ok();
|
||||||
|
let uses_https = env::var("CHELA_USES_HTTPS").is_ok();
|
||||||
let server_state = ServerState {
|
let server_state = ServerState {
|
||||||
db_pool,
|
db_pool,
|
||||||
host,
|
host,
|
||||||
sqids,
|
sqids,
|
||||||
main_page_redirect: Url::parse(&main_page_redirect).ok(),
|
main_page_redirect: Url::parse(&main_page_redirect).ok(),
|
||||||
behind_proxy,
|
behind_proxy,
|
||||||
|
uses_https
|
||||||
};
|
};
|
||||||
|
|
||||||
let address = std::env::var("CHELA_LISTEN_ADDRESS").unwrap_or("0.0.0.0".to_string());
|
serve(server_state).await?;
|
||||||
let port = 3000;
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve(state: ServerState) -> eyre::Result<()> {
|
||||||
|
let unix_socket = env::var("CHELA_UNIX_SOCKET").unwrap_or_default();
|
||||||
|
if unix_socket.is_empty() {
|
||||||
|
let router = Router::new()
|
||||||
|
.route("/", get(get::index))
|
||||||
|
.route("/create", get(get::create_id))
|
||||||
|
.route("/tracking", get(get::tracking))
|
||||||
|
.route("/tracking/:id", get(get::tracking_id))
|
||||||
|
.route("/:id", get(get::id))
|
||||||
|
.route("/", post(post::create_link))
|
||||||
|
.layer(axum::Extension(state));
|
||||||
|
let address = env::var("CHELA_LISTEN_ADDRESS").unwrap_or("0.0.0.0".to_string());
|
||||||
|
let port = 3000;
|
||||||
|
let listener = tokio::net::TcpListener::bind(format!("{address}:{port}")).await?;
|
||||||
|
log!("Listening at {}:{}", address, port);
|
||||||
|
axum::serve(
|
||||||
|
listener,
|
||||||
|
router.into_make_service_with_connect_info::<std::net::SocketAddr>(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
let router = Router::new()
|
||||||
|
.route("/", get(get::index))
|
||||||
|
.route("/create", get(get::create_id))
|
||||||
|
.route("/tracking", get(get::tracking))
|
||||||
|
.route("/tracking/:id", get(get::tracking_id))
|
||||||
|
.route("/:id", get(get::id_unix))
|
||||||
|
.route("/", post(post::create_link))
|
||||||
|
.layer(axum::Extension(state));
|
||||||
|
let unix_socket_path = std::path::Path::new(&unix_socket);
|
||||||
|
if unix_socket_path.exists() {
|
||||||
|
tokio::fs::remove_file(unix_socket_path).await?;
|
||||||
|
}
|
||||||
|
let listener = tokio::net::UnixListener::bind(unix_socket_path)?;
|
||||||
|
log!("Listening via Unix socket at {}", unix_socket);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut service = router.into_make_service_with_connect_info::<UdsConnectInfo>();
|
||||||
|
loop {
|
||||||
|
let (socket, _remote_addr) = listener.accept().await.unwrap();
|
||||||
|
let tower_service = match service.call(&socket).await {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => match err {},
|
||||||
|
};
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let socket = TokioIo::new(socket);
|
||||||
|
let hyper_service =
|
||||||
|
hyper::service::service_fn(move |request: Request<Incoming>| {
|
||||||
|
tower_service.clone().call(request)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(err) = server::conn::auto::Builder::new(TokioExecutor::new())
|
||||||
|
.serve_connection_with_upgrades(socket, hyper_service)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!("Failed to serve connection: {}", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
let router = init_routes(server_state);
|
|
||||||
let listener = tokio::net::TcpListener::bind(format!("{address}:{port}")).await?;
|
|
||||||
log!("Listening at {}:{}", address, port);
|
|
||||||
axum::serve(
|
|
||||||
listener,
|
|
||||||
router.into_make_service_with_connect_info::<SocketAddr>(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +172,7 @@ async fn init_db() -> eyre::Result<Pool<Postgres>> {
|
|||||||
let db_pool = PgPoolOptions::new()
|
let db_pool = PgPoolOptions::new()
|
||||||
.max_connections(15)
|
.max_connections(15)
|
||||||
.connect(
|
.connect(
|
||||||
std::env::var("DATABASE_URL")
|
env::var("DATABASE_URL")
|
||||||
.expect("DATABASE_URL must be set")
|
.expect("DATABASE_URL must be set")
|
||||||
.as_str(),
|
.as_str(),
|
||||||
)
|
)
|
||||||
@ -98,7 +189,8 @@ async fn init_db() -> eyre::Result<Pool<Postgres>> {
|
|||||||
CREATE TABLE IF NOT EXISTS chela.urls (
|
CREATE TABLE IF NOT EXISTS chela.urls (
|
||||||
index BIGSERIAL PRIMARY KEY,
|
index BIGSERIAL PRIMARY KEY,
|
||||||
id TEXT NOT NULL UNIQUE,
|
id TEXT NOT NULL UNIQUE,
|
||||||
url TEXT NOT NULL
|
url TEXT NOT NULL,
|
||||||
|
custom_id BOOLEAN NOT NULL
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
@ -109,7 +201,7 @@ CREATE TABLE IF NOT EXISTS chela.urls (
|
|||||||
sqlx::query(
|
sqlx::query(
|
||||||
"
|
"
|
||||||
CREATE TABLE IF NOT EXISTS chela.tracking (
|
CREATE TABLE IF NOT EXISTS chela.tracking (
|
||||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
id TEXT NOT NULL,
|
id TEXT NOT NULL,
|
||||||
ip TEXT,
|
ip TEXT,
|
||||||
referrer TEXT,
|
referrer TEXT,
|
||||||
@ -123,12 +215,3 @@ CREATE TABLE IF NOT EXISTS chela.tracking (
|
|||||||
|
|
||||||
Ok(db_pool)
|
Ok(db_pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_routes(state: ServerState) -> Router {
|
|
||||||
Router::new()
|
|
||||||
.route("/", get(get::index))
|
|
||||||
.route("/:id", get(get::id))
|
|
||||||
.route("/create", get(get::create_id))
|
|
||||||
.route("/", post(post::create_link))
|
|
||||||
.layer(axum::Extension(state))
|
|
||||||
}
|
|
||||||
|
16
src/post.rs
16
src/post.rs
@ -32,7 +32,8 @@ pub async fn create_link(
|
|||||||
if id.exists {
|
if id.exists {
|
||||||
log!("Serving cached id {} -> {}", id.id, form.url.as_str());
|
log!("Serving cached id {} -> {}", id.id, form.url.as_str());
|
||||||
return Html(format!(
|
return Html(format!(
|
||||||
r#"<pre>http://{}/{} -> <a href="{}"">{}</a></pre>"#,
|
r#"<pre>http{}://{}/{} -> <a href="{}"">{}</a></pre>"#,
|
||||||
|
if state.uses_https { "s" } else { "" },
|
||||||
state.host,
|
state.host,
|
||||||
id.id,
|
id.id,
|
||||||
form.url.as_str(),
|
form.url.as_str(),
|
||||||
@ -44,8 +45,8 @@ pub async fn create_link(
|
|||||||
if let Some(index) = id.index {
|
if let Some(index) = id.index {
|
||||||
res = sqlx::query(
|
res = sqlx::query(
|
||||||
"
|
"
|
||||||
INSERT INTO chela.urls (index,id,url)
|
INSERT INTO chela.urls (index,id,url,custom_id)
|
||||||
VALUES ($1,$2,$3)
|
VALUES ($1,$2,$3,false)
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.bind(index)
|
.bind(index)
|
||||||
@ -56,8 +57,8 @@ VALUES ($1,$2,$3)
|
|||||||
} else {
|
} else {
|
||||||
res = sqlx::query(
|
res = sqlx::query(
|
||||||
"
|
"
|
||||||
INSERT INTO chela.urls (id,url)
|
INSERT INTO chela.urls (id,url,custom_id)
|
||||||
VALUES ($1,$2)
|
VALUES ($1,$2,true)
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.bind(id.id.clone())
|
.bind(id.id.clone())
|
||||||
@ -72,7 +73,8 @@ VALUES ($1,$2)
|
|||||||
return (
|
return (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Html(format!(
|
Html(format!(
|
||||||
r#"<pre>http://{}/{} -> <a href="{}"">{}</a></pre>"#,
|
r#"<pre>http{}://{}/{} -> <a href="{}"">{}</a></pre>"#,
|
||||||
|
if state.uses_https { "s" } else { "" },
|
||||||
state.host,
|
state.host,
|
||||||
id.id,
|
id.id,
|
||||||
form.url.as_str(),
|
form.url.as_str(),
|
||||||
@ -102,7 +104,7 @@ VALUES ($1,$2)
|
|||||||
async fn generate_id(form: CreateForm, state: ServerState) -> eyre::Result<NextId> {
|
async fn generate_id(form: CreateForm, state: ServerState) -> eyre::Result<NextId> {
|
||||||
if form.id.is_empty() {
|
if form.id.is_empty() {
|
||||||
let existing_row: Result<UrlRow, sqlx::Error> =
|
let existing_row: Result<UrlRow, sqlx::Error> =
|
||||||
sqlx::query_as("SELECT * FROM chela.urls WHERE url = $1")
|
sqlx::query_as("SELECT * FROM chela.urls WHERE url = $1 AND custom_id = 'false'")
|
||||||
.bind(form.url.as_str())
|
.bind(form.url.as_str())
|
||||||
.fetch_one(&state.db_pool)
|
.fetch_one(&state.db_pool)
|
||||||
.await;
|
.await;
|
||||||
|
Loading…
Reference in New Issue
Block a user