- 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:
parent
9671be1ff3
commit
9ca54103e5
@ -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))
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
@ -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
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user