mirror of
https://github.com/SinTan1729/chhoto-url
synced 2025-02-05 13:52:33 -06:00
Merge pull request #39 from SolninjaA/main
Improvements of the API system
This commit is contained in:
commit
f8f4dae457
11 changed files with 367 additions and 25 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,3 +9,4 @@ urls.sqlite
|
||||||
**/.directory
|
**/.directory
|
||||||
.env
|
.env
|
||||||
cookie*
|
cookie*
|
||||||
|
.idea/
|
||||||
|
|
1
Makefile
1
Makefile
|
@ -24,6 +24,7 @@ docker-test: docker-local docker-stop
|
||||||
docker run -p ${PORT}:${PORT} --name chhoto-url -e password="${PASSWORD}" -e public_mode="${PUBLIC_MODE}" \
|
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 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 slug_style="${SLUG_STYLE}" -e slug_length="${SLUG_LENGTH}" -e cache_control_header="${CACHE_CONTROL_HEADER}"\
|
||||||
|
-e api_key="${API_KEY}"\
|
||||||
-d chhoto-url
|
-d chhoto-url
|
||||||
docker logs chhoto-url -f
|
docker logs chhoto-url -f
|
||||||
|
|
||||||
|
|
47
README.md
47
README.md
|
@ -128,6 +128,17 @@ docker run -p 4567:4567 \
|
||||||
-e site_url="https://www.example.com" \
|
-e site_url="https://www.example.com" \
|
||||||
-d chhoto-url:latest
|
-d chhoto-url:latest
|
||||||
```
|
```
|
||||||
|
1.c Further, set an API key to activate JSON result mode (optional)
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -p 4567:4567 \
|
||||||
|
-e password="password" \
|
||||||
|
-e api_key="SECURE_API_KEY" \
|
||||||
|
-v ./urls.sqlite:/urls.sqlite \
|
||||||
|
-e db_url=/urls.sqlite \
|
||||||
|
-e site_url="https://www.example.com" \
|
||||||
|
-d chhoto-url:latest
|
||||||
|
```
|
||||||
|
|
||||||
You can set the redirect method to Permanent 308 (default) or Temporary 307 by setting
|
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
|
the `redirect_method` variable to `TEMPORARY` or `PERMANENT` (it's matched exactly). By
|
||||||
|
@ -148,8 +159,11 @@ served through a proxy.
|
||||||
The application can be used from the terminal using something like `curl`. In all the examples
|
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.
|
below, replace `http://localhost:4567` with where your instance of `chhoto-url` is accessible.
|
||||||
|
|
||||||
If you have set up
|
You can get the version of `chhoto-url` the server is running using `curl http://localhost:4567/api/version` and
|
||||||
a password, first do the following to get an authentication cookie and store it in a file.
|
get the siteurl using `curl http://localhost:4567/api/siteurl`. These routes are accessible without any authentication.
|
||||||
|
|
||||||
|
### 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
|
```bash
|
||||||
curl -X POST -d "<your-password>" -c cookie.txt http://localhost:4567/api/login
|
curl -X POST -d "<your-password>" -c cookie.txt http://localhost:4567/api/login
|
||||||
```
|
```
|
||||||
|
@ -173,8 +187,33 @@ curl -X DELETE http://localhost:4567/api/del/<shortlink>
|
||||||
```
|
```
|
||||||
The server will send a confirmation.
|
The server will send a confirmation.
|
||||||
|
|
||||||
You can get the version of `chhoto-url` the server is running using `curl http://localhost:4567/api/version` and
|
### API key validation
|
||||||
get the siteurl using `curl http://localhost:4567/api/siteurl`.
|
**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 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.
|
||||||
|
|
||||||
## Disable authentication
|
## Disable authentication
|
||||||
If you do not define a password environment variable when starting the docker image, authentication
|
If you do not define a password environment variable when starting the docker image, authentication
|
||||||
|
|
51
actix/Cargo.lock
generated
51
actix/Cargo.lock
generated
|
@ -1,6 +1,6 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-codec"
|
name = "actix-codec"
|
||||||
|
@ -476,13 +476,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chhoto-url"
|
name = "chhoto-url"
|
||||||
version = "5.4.5"
|
version = "5.4.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-session",
|
"actix-session",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
|
"passwords",
|
||||||
"rand",
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
@ -1227,6 +1228,15 @@ dependencies = [
|
||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "passwords"
|
||||||
|
version = "3.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11407193a7c2bd14ec6b0ec3394da6fdcf7a4d5dcbc8c3cc38dfb17802c8d59c"
|
||||||
|
dependencies = [
|
||||||
|
"random-pick",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
@ -1284,6 +1294,12 @@ dependencies = [
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-hack"
|
||||||
|
version = "0.5.20+deprecated"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.89"
|
version = "1.0.89"
|
||||||
|
@ -1332,6 +1348,37 @@ dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "random-number"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fc8cdd49be664772ffc3dbfa743bb8c34b78f9cc6a9f50e56ae878546796067"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-hack",
|
||||||
|
"rand",
|
||||||
|
"random-number-macro-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "random-number-macro-impl"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f5135143cb48d14289139e4615bffec0d59b4cbfd4ea2398a3770bd2abfc4aa2"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-hack",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "random-pick"
|
||||||
|
version = "1.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c179499072da789afe44127d5f4aa6012de2c2f96ef759990196b37387a2a0f8"
|
||||||
|
dependencies = [
|
||||||
|
"random-number",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.7"
|
version = "0.5.7"
|
||||||
|
|
|
@ -32,6 +32,7 @@ actix-files = "0.6.5"
|
||||||
rusqlite = { version = "0.32.0", features = ["bundled"] }
|
rusqlite = { version = "0.32.0", features = ["bundled"] }
|
||||||
regex = "1.10.3"
|
regex = "1.10.3"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
passwords = "3.1.16"
|
||||||
actix-session = { version = "0.10.0", features = ["cookie-session"] }
|
actix-session = { version = "0.10.0", features = ["cookie-session"] }
|
||||||
env_logger = "0.11.1"
|
env_logger = "0.11.1"
|
||||||
nanoid = "0.4.0"
|
nanoid = "0.4.0"
|
||||||
|
|
|
@ -2,8 +2,55 @@
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
use actix_session::Session;
|
use actix_session::Session;
|
||||||
|
use actix_web::HttpRequest;
|
||||||
use std::{env, time::SystemTime};
|
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
|
// Validate a given password
|
||||||
pub fn validate(session: Session) -> bool {
|
pub fn validate(session: Session) -> bool {
|
||||||
// If there's no password provided, just return true
|
// If there's no password provided, just return true
|
||||||
|
|
|
@ -91,5 +91,6 @@ pub fn open_db(path: String) -> Connection {
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
.expect("Unable to initialize empty database.");
|
.expect("Unable to initialize empty database.");
|
||||||
|
|
||||||
db
|
db
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
// Generate session key in runtime so that restart invalidates older logins
|
// Generate session key in runtime so that restart invalidates older logins
|
||||||
let secret_key = Key::generate();
|
let secret_key = Key::generate();
|
||||||
|
|
||||||
let db_location = env::var("db_url")
|
let db_location = env::var("db_url")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|s| !s.trim().is_empty())
|
.filter(|s| !s.trim().is_empty())
|
||||||
|
@ -38,6 +39,21 @@ async fn main() -> Result<()> {
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|s| !s.trim().is_empty());
|
.filter(|s| !s.trim().is_empty());
|
||||||
|
|
||||||
|
// If an API key is set, check the security
|
||||||
|
if let Ok(key) = env::var("api_key") {
|
||||||
|
if !auth::is_key_secure() {
|
||||||
|
eprintln!("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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}.");
|
||||||
|
if let Some(site_url) = env::var("site_url").ok().filter(|s| !s.trim().is_empty()) {
|
||||||
|
eprintln!("Configured Site URL is: {site_url}.");
|
||||||
|
}
|
||||||
|
|
||||||
// Actually start the server
|
// Actually start the server
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
|
|
|
@ -8,10 +8,13 @@ use actix_web::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
post,
|
post,
|
||||||
web::{self, Redirect},
|
web::{self, Redirect},
|
||||||
Either, HttpResponse, Responder,
|
Either, HttpRequest, HttpResponse, Responder,
|
||||||
};
|
};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
// Serialize JSON data
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::auth;
|
use crate::auth;
|
||||||
use crate::database;
|
use crate::database;
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
|
@ -20,12 +23,68 @@ use crate::AppState;
|
||||||
// Store the version number
|
// Store the version number
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
// Define JSON struct for returning JSON data
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response {
|
||||||
|
success: bool,
|
||||||
|
error: bool,
|
||||||
|
reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needs to return the short URL to make it easier for programs leveraging the API
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct CreatedURL {
|
||||||
|
success: bool,
|
||||||
|
error: bool,
|
||||||
|
shorturl: String,
|
||||||
|
}
|
||||||
|
|
||||||
// Define the routes
|
// Define the routes
|
||||||
|
|
||||||
// Add new links
|
// Add new links
|
||||||
#[post("/api/new")]
|
#[post("/api/new")]
|
||||||
pub async fn add_link(req: String, data: web::Data<AppState>, session: Session) -> HttpResponse {
|
pub async fn add_link(
|
||||||
if env::var("public_mode") == Ok(String::from("Enable")) || auth::validate(session) {
|
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 url = format!(
|
||||||
|
"{}:{}",
|
||||||
|
env::var("site_url")
|
||||||
|
.ok()
|
||||||
|
.filter(|s| !s.trim().is_empty())
|
||||||
|
.unwrap_or(String::from("http://localhost")),
|
||||||
|
port
|
||||||
|
);
|
||||||
|
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);
|
let out = utils::add_link(req, &data.db);
|
||||||
if out.0 {
|
if out.0 {
|
||||||
HttpResponse::Created().body(out.1)
|
HttpResponse::Created().body(out.1)
|
||||||
|
@ -39,8 +98,20 @@ pub async fn add_link(req: String, data: web::Data<AppState>, session: Session)
|
||||||
|
|
||||||
// Return all active links
|
// Return all active links
|
||||||
#[get("/api/all")]
|
#[get("/api/all")]
|
||||||
pub async fn getall(data: web::Data<AppState>, session: Session) -> HttpResponse {
|
pub async fn getall(
|
||||||
if auth::validate(session) {
|
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))
|
HttpResponse::Ok().body(utils::getall(&data.db))
|
||||||
} else {
|
} else {
|
||||||
let body = if env::var("public_mode") == Ok(String::from("Enable")) {
|
let body = if env::var("public_mode") == Ok(String::from("Enable")) {
|
||||||
|
@ -105,20 +176,48 @@ pub async fn link_handler(
|
||||||
// Handle login
|
// Handle login
|
||||||
#[post("/api/login")]
|
#[post("/api/login")]
|
||||||
pub async fn login(req: String, session: Session) -> HttpResponse {
|
pub async fn login(req: String, session: Session) -> HttpResponse {
|
||||||
if let Ok(password) = env::var("password") {
|
// Keep this function backwards compatible
|
||||||
if password != req {
|
if env::var("api_key").is_ok() {
|
||||||
eprintln!("Failed login attempt!");
|
if let Ok(password) = env::var("password") {
|
||||||
return HttpResponse::Unauthorized().body("Wrong 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!")
|
||||||
}
|
}
|
||||||
// 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
|
// 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")]
|
#[delete("/api/logout")]
|
||||||
pub async fn logout(session: Session) -> HttpResponse {
|
pub async fn logout(session: Session) -> HttpResponse {
|
||||||
if session.remove("chhoto-url-auth").is_some() {
|
if session.remove("chhoto-url-auth").is_some() {
|
||||||
|
@ -134,8 +233,31 @@ pub async fn delete_link(
|
||||||
shortlink: web::Path<String>,
|
shortlink: web::Path<String>,
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
session: Session,
|
session: Session,
|
||||||
|
http: HttpRequest,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
if auth::validate(session) {
|
// 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) {
|
if utils::delete_link(shortlink.to_string(), &data.db) {
|
||||||
HttpResponse::Ok().body(format!("Deleted {shortlink}"))
|
HttpResponse::Ok().body(format!("Deleted {shortlink}"))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use crate::{auth, database};
|
||||||
|
use actix_web::HttpRequest;
|
||||||
use nanoid::nanoid;
|
use nanoid::nanoid;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use crate::database;
|
|
||||||
|
|
||||||
// Struct for reading link pairs sent during API call
|
// Struct for reading link pairs sent during API call
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct URLPair {
|
struct URLPair {
|
||||||
|
@ -17,6 +17,68 @@ struct URLPair {
|
||||||
longlink: 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
|
// Request the DB for searching an URL
|
||||||
pub fn get_longurl(shortlink: String, db: &Connection) -> Option<String> {
|
pub fn get_longurl(shortlink: String, db: &Connection) -> Option<String> {
|
||||||
if validate_link(&shortlink) {
|
if validate_link(&shortlink) {
|
||||||
|
|
|
@ -24,7 +24,12 @@ services:
|
||||||
# - site_url=https://www.example.com
|
# - site_url=https://www.example.com
|
||||||
|
|
||||||
- password=TopSecretPass
|
- 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
|
# Pass the redirect method, if needed. TEMPORARY and PERMANENT
|
||||||
# are accepted values, defaults to PERMANENT.
|
# are accepted values, defaults to PERMANENT.
|
||||||
# - redirect_method=TEMPORARY
|
# - redirect_method=TEMPORARY
|
||||||
|
|
Loading…
Reference in a new issue