- Removing admin features from core service binary API

Because this feature is going to be optional we're going to provide this as
a seperate service as the two have entirely different goals anyway
This commit is contained in:
shockrah 2022-11-13 12:38:09 -08:00
parent 9671be1ff3
commit 9ca54103e5
7 changed files with 0 additions and 335 deletions

View File

@ -1,51 +0,0 @@
use rocket::request::{Outcome, Request, FromRequest};
use rocket::async_trait;
use rocket::http::Status;
use crate::db::{self, DB_PATH};
pub struct ApiKey {
// These are used by rocket's driver code/decl macros however cargo
// is not able to check those as the code is generated at compile time.
// The dead code thing is just to stifle pointless warnings
#[allow(dead_code)]
uid: String,
#[allow(dead_code)]
key: String
}
#[derive(Debug)]
pub enum ApiKeyError {
Missing,
Invalid,
}
#[async_trait]
impl<'r> FromRequest<'r> for ApiKey {
type Error = ApiKeyError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let key = req.headers().get_one("ADMIN-API-KEY");
let uid = req.headers().get_one("ADMIN-API-UID");
if key.is_none() || uid.is_none() {
return Outcome::Failure((Status::Forbidden, ApiKeyError::Missing));
}
let (key, uid) = (key.unwrap(), uid.unwrap());
println!("Path to use for db file {:?}", DB_PATH.to_string());
let db = db::Database::load(DB_PATH.as_str().into()).unwrap();
if let Some(stored) = db.get(uid) {
if stored == key {
return Outcome::Success(ApiKey {
key: key.into(),
uid: uid.into()
})
}
return Outcome::Failure((Status::Forbidden, ApiKeyError::Invalid))
}
return Outcome::Failure((Status::Forbidden, ApiKeyError::Invalid))
}
}

View File

@ -1,76 +0,0 @@
#[cfg(feature = "admin")]
mod apikey;
mod response;
mod util;
use std::collections::HashMap;
use std::io::Result;
use std::fs;
use std::path::{Path, PathBuf};
use rocket::data::{Data, ToByteUnit};
use rocket::serde::json::Json;
use rocket_dyn_templates::Template;
use rocket::response::Redirect;
use response::{bad_request, ok};
use apikey::ApiKey;
use response::ActionResponse;
use crate::common::get_clips_dir;
#[get("/")]
pub async fn login_dashboard_redirect() -> Redirect {
Redirect::to("/admin/dashboard")
}
#[get("/dashboard")]
pub async fn login_dashboard() -> Template {
// This page is basically just a login form
// However the rest of the form is present on this page, just hidden
let h: HashMap<i32,i32> = HashMap::new(); // does not allocate
return Template::render("admin", &h);
}
#[post("/dashboard")]
pub async fn dashboard(_key: ApiKey) -> Json<ActionResponse> {
// Assuming the api key check doesn't fail we can reply with Ok
// at the application level
ok()
}
#[post("/upload-video/<category>/<filename>", data = "<data>")]
pub async fn updload_video(_key: ApiKey, category: PathBuf, filename: PathBuf, data: Data<'_>)
-> Result<Json<ActionResponse>> {
/*
* Uploads must have BOTH a valid filename and a category
* Without the category the server will simply not find
* the correct endpoint to reach and thus will 404
*/
if util::valid_filename(&filename) == false {
return Ok(bad_request(Some("Invalid filename(s)")));
}
let clips = get_clips_dir();
fs::create_dir_all(Path::new(&clips).join(&category))?;
/*
* We allow up to 200 Megaytes per upload as most short
* clips are not going to be very large anyway and this
* should be a reasonably high limit for those that want
* to upload "large" clips
* */
let filepath = Path::new(&clips).join(category).join(filename);
data.open(250.megabytes()).into_file(filepath).await?;
Ok(ok())
}
#[delete("/remove-video/<category>/<filename>")]
pub async fn remove_video(_key: ApiKey, category: PathBuf, filename: PathBuf)
-> Result<Json<ActionResponse>> {
let clips = get_clips_dir();
let path = Path::new(&clips).join(&category).join(&filename);
fs::remove_file(path)?;
Ok(ok())
}

View File

@ -1,35 +0,0 @@
/*
* Module handles general responses for the admin feature
* Primarily these are responses for Admin related actions
* like fetching video's, updating videos and deleting them
* as well
*/
use serde::Serialize;
use rocket::serde::json::Json;
const FAIL: &'static str = "fail";
const OK: &'static str = "fail";
#[derive(Serialize)]
pub struct ActionResponse {
status: &'static str,
code: i32,
details: Option<&'static str>
}
pub fn ok() -> Json<ActionResponse> {
Json(ActionResponse {
status: OK,
code: 200,
details: None
})
}
pub fn bad_request(text: Option<&'static str>) -> Json<ActionResponse> {
Json(ActionResponse {
status: FAIL,
code: 400,
details: text
})
}

View File

@ -1,14 +0,0 @@
use std::path::PathBuf;
pub fn valid_filename(p: &PathBuf) -> bool {
// Checks if a given filename is actually valid or not
let mut valid = false;
let s = p.file_name().unwrap_or_default().to_string_lossy();
for e in [".mp4", ".webm", ".mkv"] {
if s.ends_with(e) {
valid = true;
break;
}
}
valid
}

View File

@ -1,117 +0,0 @@
#[cfg(feature = "admin")]
// This module defines a tiny async interface for the "database" that this
// project uses for interfacing with the key store
// WARN: at the moment there are no guarantees as far as data integrity is
// concerned. This means there are no real transactions
use std::env;
use std::fs::OpenOptions;
use std::io::{BufWriter, BufReader};
use std::path::PathBuf;
use std::collections::HashMap;
use rocket::serde::{Serialize, Deserialize};
lazy_static! {
pub static ref DB_PATH: String = {
env::var("DB_PATH").unwrap_or("keys.db".into())
};
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Database {
// uid's while random are fine to release as public as the key is more
// important however ideally neither should be release. Furthermore
// the frontend assists in keeping these secret by treating both as
// password fields as they are both randomly generated via a script
// uid -> key
users: HashMap<String,String>,
#[serde(skip)]
path: PathBuf
}
impl Database {
// Opens a handle to a database file
// if none is found then one is created with the new path
// if there is one then the existing database is used
// any thing else is invalid and causes this to return Err
pub fn new(path: PathBuf) -> Result<Self, std::io::Error> {
let file = OpenOptions::new()
.write(true)
.create(true)
.open(&path)?;
let writer = BufWriter::new(&file);
// Dummy value to write in place
let empty = Database { users: HashMap::new(), path: "".into() };
serde_json::to_writer(writer, &empty)?;
Ok(empty)
}
pub fn load(path: PathBuf) -> Result<Self, std::io::Error> {
let file = OpenOptions::new()
.read(true)
.open(&path)?;
let reader = BufReader::new(&file);
let mut data: Database = serde_json::from_reader(reader)?;
data.path = path;
return Ok(data);
}
pub fn get(&self, uid: &str) -> Option<&String> {
return self.users.get(uid);
}
fn write(&self) -> Result<(), std::io::Error> {
let file = OpenOptions::new()
.write(true)
.open(&self.path)?;
let writer = BufWriter::new(file);
serde_json::to_writer(writer, &self.path)?;
return Ok(())
}
pub fn remove(&mut self, uid: &str) -> Result<(), std::io::Error> {
self.users.remove_entry(uid);
self.write()
}
pub fn add(&mut self, key: &str, value: &str) -> Result<(), std::io::Error> {
println!("{:?}", self.path);
self.users.insert(key.into(), value.into());
self.write()
}
}
#[cfg(test)]
mod db_tests {
use super::Database;
const DB: &'static str = "new.db";
#[test]
fn load_db() {
match Database::new(DB.into()) {
Ok(db) => println!("Loaded new sample.db: {:?}", db),
Err(e) => panic!("Error fetching database: {}", e)
}
}
#[test]
fn add_simple_entries() {
match Database::load(DB.into()) {
Ok(mut db) => db.add("key", "value").unwrap(),
Err(e) => println!("Error adding entries: {}", e)
}
}
#[test]
fn remove_simple_entries() {
match Database::load(DB.into()) {
Ok(mut db) => db.remove("key").unwrap(),
Err(e) => println!("Error removing simple entries: {}", e)
}
}
}

View File

@ -10,15 +10,6 @@ mod video;
mod thumbnail;
mod common;
#[cfg(feature = "admin")]
mod admin;
#[cfg(feature = "admin")]
mod sec;
#[cfg(feature = "admin")]
mod db;
#[rocket::main]
async fn main() {
// rid ourselves of random emoji's in logs
@ -33,13 +24,6 @@ async fn main() {
.mount("/api", routes![category::list, video::list]) // json
.mount("/thumbnail", routes![thumbnail::get]) // images
.mount("/video", routes![video::get_video]) // videos
.mount("/admin", routes![
admin::login_dashboard_redirect,
admin::login_dashboard,
admin::dashboard,
admin::updload_video,
admin::remove_video
])
.attach(Template::fairing())
.launch().await;
} else {

View File

@ -1,26 +0,0 @@
#[cfg(feature = "admin")]
// This module concerns itself with the encrypting/decrypting of passwords
// as well as the storage of those items
use rocket::tokio::io::AsyncReadExt;
use rocket::tokio::fs;
async fn random_string() -> Result<String, std::io::Error> {
// First we read in some bytes from /dev/urandom
let mut handle = fs::File::open("/dev/urandom").await?;
let mut buffer = [0;32];
handle.read(&mut buffer[..]).await?;
Ok(base64::encode_config(buffer, base64::URL_SAFE_NO_PAD))
}
#[cfg(test)]
mod sec_api_tests {
use rocket::tokio;
use super::random_string;
#[tokio::test]
async fn generate_string() {
println!("{:?}", random_string().await);
}
}