Improves API functionality
This commit is contained in:
1 parent
e6eed2dd70
commit
2c56c68637
11 files changed
+265
-44
No files matched your search
@@ -3,6 +3,53 @@
|
||||
|
||||
use actix_session::Session;
|
||||
use std::{env, time::SystemTime};
|
||||
use actix_web::{HttpRequest};
|
||||
|
||||
// API key generation and scoring
|
||||
use passwords::{PasswordGenerator, scorer, analyzer};
|
||||
|
||||
// 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("Chhoto-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()));
|
||||
if score < 90.0 { false } else { true }
|
||||
}
|
||||
|
||||
// Validate a given password
|
||||
pub fn validate(session: Session) -> bool {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use rusqlite::Connection;
|
||||
use rusqlite::{Connection};
|
||||
use serde::Serialize;
|
||||
|
||||
// Struct for encoding a DB row
|
||||
@@ -91,5 +91,6 @@ pub fn open_db(path: String) -> Connection {
|
||||
[],
|
||||
)
|
||||
.expect("Unable to initialize empty database.");
|
||||
|
||||
db
|
||||
}
|
||||
@@ -24,11 +24,13 @@ async fn main() -> Result<()> {
|
||||
|
||||
// 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"));
|
||||
|
||||
|
||||
let port = env::var("port")
|
||||
.unwrap_or(String::from("4567"))
|
||||
.parse::<u16>()
|
||||
@@ -38,6 +40,16 @@ async fn main() -> Result<()> {
|
||||
.ok()
|
||||
.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())
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {}. Configured Site URL is: {}", port, env::var("site_url").unwrap_or(String::from("http://localhost")));
|
||||
|
||||
// Actually start the server
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
|
||||
+117
-31
@@ -3,14 +3,11 @@
|
||||
|
||||
use actix_files::NamedFile;
|
||||
use actix_session::Session;
|
||||
use actix_web::{
|
||||
delete, get,
|
||||
http::StatusCode,
|
||||
post,
|
||||
web::{self, Redirect},
|
||||
Either, HttpResponse, Responder,
|
||||
};
|
||||
use std::env;
|
||||
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;
|
||||
@@ -20,35 +17,68 @@ 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,
|
||||
}
|
||||
|
||||
// Define the routes
|
||||
|
||||
// Add new links
|
||||
#[post("/api/new")]
|
||||
pub async fn add_link(req: String, data: web::Data<AppState>, session: Session) -> HttpResponse {
|
||||
if env::var("public_mode") == Ok(String::from("Enable")) || auth::validate(session) {
|
||||
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 {
|
||||
HttpResponse::Created().body(out.1)
|
||||
} else {
|
||||
HttpResponse::Conflict().body(out.1)
|
||||
}
|
||||
} else if result.error {
|
||||
HttpResponse::Unauthorized().json(result)
|
||||
// If "pass" is true - keeps backwards compatibility
|
||||
} else {
|
||||
HttpResponse::Unauthorized().body("Not logged in!")
|
||||
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) -> HttpResponse {
|
||||
if auth::validate(session) {
|
||||
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 "pass" is true - keeps backwards compatibility
|
||||
} else {
|
||||
let body = if env::var("public_mode") == Ok(String::from("Enable")) {
|
||||
"Using public mode."
|
||||
if auth::validate(session){
|
||||
HttpResponse::Ok().body(utils::getall(&data.db))
|
||||
} else {
|
||||
"Not logged in!"
|
||||
};
|
||||
HttpResponse::Unauthorized().body(body)
|
||||
let body = if env::var("public_mode") == Ok(String::from("Enable")) {
|
||||
"Using public mode."
|
||||
} else {
|
||||
"Not logged in!"
|
||||
};
|
||||
HttpResponse::Unauthorized().body(body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,20 +135,51 @@ pub async fn link_handler(
|
||||
// Handle login
|
||||
#[post("/api/login")]
|
||||
pub async fn login(req: String, session: Session) -> HttpResponse {
|
||||
if let Ok(password) = env::var("password") {
|
||||
if password != req {
|
||||
eprintln!("Failed login attempt!");
|
||||
return HttpResponse::Unauthorized().body("Wrong password!");
|
||||
// Someone's API may be listening for the plain HTML body response of "Correct password!"
|
||||
// rather than a 200 OK HTTP response. Because of that, a check is performed to see whether
|
||||
// the api_key environment variable is set. If it is set, then it is assumed the user will expect a JSON response for all API routes.
|
||||
// *If this is not a concern, this can be removed.*
|
||||
if let Ok(_) = env::var("api_key") {
|
||||
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!")
|
||||
}
|
||||
// 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() {
|
||||
@@ -134,14 +195,39 @@ pub async fn delete_link(
|
||||
shortlink: web::Path<String>,
|
||||
data: web::Data<AppState>,
|
||||
session: Session,
|
||||
http: HttpRequest,
|
||||
) -> 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) {
|
||||
HttpResponse::Ok().body(format!("Deleted {shortlink}"))
|
||||
let response = Response {
|
||||
success: true,
|
||||
error: false,
|
||||
reason: format!("Deleted {}", shortlink)
|
||||
};
|
||||
HttpResponse::Ok().json(response)
|
||||
} else {
|
||||
HttpResponse::NotFound().body("Not found!")
|
||||
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 {
|
||||
HttpResponse::Unauthorized().body("Not logged in!")
|
||||
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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
-3
@@ -5,10 +5,10 @@ use nanoid::nanoid;
|
||||
use rand::seq::SliceRandom;
|
||||
use regex::Regex;
|
||||
use rusqlite::Connection;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
|
||||
use crate::database;
|
||||
use actix_web::HttpRequest;
|
||||
use crate::{auth, database};
|
||||
|
||||
// Struct for reading link pairs sent during API call
|
||||
#[derive(Deserialize)]
|
||||
@@ -17,6 +17,42 @@ struct URLPair {
|
||||
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 eists
|
||||
pub fn is_api_ok(http: HttpRequest) -> Response {
|
||||
// If the api_key environment variable exists
|
||||
if let Ok(_) = env::var("api_key") {
|
||||
// If the header exists
|
||||
if let Some(header) = auth::api_header(&http) {
|
||||
// If the header is correct
|
||||
if auth::validate_key(header.to_string()) {
|
||||
let result = Response { success: true, error: false, reason: "".to_string(), pass: false };
|
||||
result
|
||||
} else {
|
||||
let result = Response { success: false, error: true, reason: "Incorrect API key".to_string(), pass: false };
|
||||
result
|
||||
}
|
||||
// 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 {
|
||||
let result = Response { success: false, error: false, reason: "Chhoto-Api-Key header not found".to_string(), pass: true };
|
||||
result
|
||||
}
|
||||
} else {
|
||||
let result = Response {success: false, error: false, reason: "".to_string(), pass: true};
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Request the DB for searching an URL
|
||||
pub fn get_longurl(shortlink: String, db: &Connection) -> Option<String> {
|
||||
if validate_link(&shortlink) {
|
||||
|
||||
Reference in new issue
Block a user