Multiple enhancements and bugfixes

!Breaking change! - The updated version will not be able to read your old database file

Major improvements:
- Added editable pastas
- Added private pastas
- Added line numbers
- Added support for wide mode (1080p instead of 720p)
- Added syntax highlighting support
- Added read-only mode
- Added built-in help page
- Added option to remove logo, change title and footer text

Minor improvements:
- Improved looks in pure html mode
- Removed link to GitHub repo from navbar
- Broke up 7km long main.rs file into smaller modules
- Moved water.css into a template instead of serving it as an external resource
- Made Save button a bit bigger
- Updated README.MD

Bugfixes:
- Fixed a bug where an incorrect animal ID in a request would cause a crash
- Fixed a bug where an empty or corrupt JSON database would cause a crash
This commit is contained in:
Daniel Szabo 2022-06-03 17:24:34 +01:00
parent 1c873d23b5
commit 4cc737731a
27 changed files with 1343 additions and 494 deletions

58
src/args.rs Normal file
View file

@ -0,0 +1,58 @@
use clap::Parser;
use lazy_static::lazy_static;
lazy_static! {
pub static ref ARGS: Args = Args::parse();
}
#[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
#[clap(long)]
pub auth_username: Option<String>,
#[clap(long)]
pub auth_password: Option<String>,
#[clap(long)]
pub editable: bool,
#[clap(long)]
pub footer_text: Option<String>,
#[clap(long)]
pub hide_footer: bool,
#[clap(long)]
pub hide_header: bool,
#[clap(long)]
pub hide_logo: bool,
#[clap(long)]
pub no_listing: bool,
#[clap(long)]
pub highlightsyntax: bool,
#[clap(short, long, default_value_t = 8080)]
pub port: u32,
#[clap(long)]
pub private: bool,
#[clap(long)]
pub pure_html: bool,
#[clap(long)]
pub readonly: bool,
#[clap(long)]
pub title: Option<String>,
#[clap(short, long, default_value_t = 1)]
pub threads: u8,
#[clap(long)]
pub wide: bool,
}

142
src/endpoints/create.rs Normal file
View file

@ -0,0 +1,142 @@
use crate::dbio::save_to_file;
use crate::util::animalnumbers::to_animal_names;
use crate::util::misc::is_valid_url;
use crate::{AppState, Pasta, ARGS};
use actix_multipart::Multipart;
use actix_web::{get, web, Error, HttpResponse, Responder};
use askama::Template;
use futures::TryStreamExt;
use rand::Rng;
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate<'a> {
args: &'a ARGS,
}
#[get("/")]
pub async fn index() -> impl Responder {
HttpResponse::Found()
.content_type("text/html")
.body(IndexTemplate { args: &ARGS }.render().unwrap())
}
pub async fn create(
data: web::Data<AppState>,
mut payload: Multipart,
) -> Result<HttpResponse, Error> {
if ARGS.readonly {
return Ok(HttpResponse::Found()
.append_header(("Location", "/"))
.finish());
}
let mut pastas = data.pastas.lock().unwrap();
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
let mut new_pasta = Pasta {
id: rand::thread_rng().gen::<u16>() as u64,
content: String::from("No Text Content"),
file: String::from("no-file"),
extension: String::from(""),
private: false,
editable: false,
created: timenow,
pasta_type: String::from(""),
expiration: 0,
};
while let Some(mut field) = payload.try_next().await? {
match field.name() {
"editable" => {
// while let Some(_chunk) = field.try_next().await? {}
new_pasta.editable = true;
continue;
}
"private" => {
// while let Some(_chunk) = field.try_next().await? {}
new_pasta.private = true;
continue;
}
"expiration" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.expiration = match std::str::from_utf8(&chunk).unwrap() {
"1min" => timenow + 60,
"10min" => timenow + 60 * 10,
"1hour" => timenow + 60 * 60,
"24hour" => timenow + 60 * 60 * 24,
"1week" => timenow + 60 * 60 * 24 * 7,
"never" => 0,
_ => {
log::error!("{}", "Unexpected expiration time!");
0
}
};
}
continue;
}
"content" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.content = std::str::from_utf8(&chunk).unwrap().to_string();
new_pasta.pasta_type = if is_valid_url(new_pasta.content.as_str()) {
String::from("url")
} else {
String::from("text")
};
}
continue;
}
"syntax-highlight" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.extension = std::str::from_utf8(&chunk).unwrap().to_string();
}
continue;
}
"file" => {
let content_disposition = field.content_disposition();
let filename = match content_disposition.get_filename() {
Some("") => continue,
Some(filename) => filename.replace(' ', "_").to_string(),
None => continue,
};
std::fs::create_dir_all(format!("./pasta_data/{}", &new_pasta.id_as_animals()))
.unwrap();
let filepath = format!("./pasta_data/{}/{}", &new_pasta.id_as_animals(), &filename);
new_pasta.file = filename;
let mut f = web::block(|| std::fs::File::create(filepath)).await??;
while let Some(chunk) = field.try_next().await? {
f = web::block(move || f.write_all(&chunk).map(|_| f)).await??;
}
new_pasta.pasta_type = String::from("text");
}
_ => {}
}
}
let id = new_pasta.id;
pastas.push(new_pasta);
save_to_file(&pastas);
Ok(HttpResponse::Found()
.append_header(("Location", format!("/pasta/{}", to_animal_names(id))))
.finish())
}

99
src/endpoints/edit.rs Normal file
View file

@ -0,0 +1,99 @@
use crate::args::Args;
use crate::dbio::save_to_file;
use crate::endpoints::errors::ErrorTemplate;
use crate::util::animalnumbers::to_u64;
use crate::util::misc::remove_expired;
use crate::{AppState, Pasta, ARGS};
use actix_multipart::Multipart;
use actix_web::{get, post, web, Error, HttpResponse};
use askama::Template;
use futures::TryStreamExt;
#[derive(Template)]
#[template(path = "edit.html", escape = "none")]
struct EditTemplate<'a> {
pasta: &'a Pasta,
args: &'a Args,
}
#[get("/edit/{id}")]
pub async fn get_edit(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner()).unwrap_or(0);
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
if !pasta.editable {
return HttpResponse::Found()
.append_header(("Location", "/"))
.finish();
}
return HttpResponse::Found().content_type("text/html").body(
EditTemplate {
pasta: &pasta,
args: &ARGS,
}
.render()
.unwrap(),
);
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[post("/edit/{id}")]
pub async fn post_edit(
data: web::Data<AppState>,
id: web::Path<String>,
mut payload: Multipart,
) -> Result<HttpResponse, Error> {
if ARGS.readonly {
return Ok(HttpResponse::Found()
.append_header(("Location", "/"))
.finish());
}
let id = to_u64(&*id.into_inner()).unwrap_or(0);
let mut pastas = data.pastas.lock().unwrap();
remove_expired(&mut pastas);
let mut new_content = String::from("");
while let Some(mut field) = payload.try_next().await? {
match field.name() {
"content" => {
while let Some(chunk) = field.try_next().await? {
new_content = std::str::from_utf8(&chunk).unwrap().to_string();
}
}
_ => {}
}
}
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
if pasta.editable {
pastas[i].content.replace_range(.., &*new_content);
save_to_file(&pastas);
return Ok(HttpResponse::Found()
.append_header(("Location", format!("/pasta/{}", pastas[i].id_as_animals())))
.finish());
} else {
break;
}
}
}
Ok(HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
}

16
src/endpoints/errors.rs Normal file
View file

@ -0,0 +1,16 @@
use actix_web::{Error, HttpResponse};
use askama::Template;
use crate::args::{Args, ARGS};
#[derive(Template)]
#[template(path = "error.html")]
pub struct ErrorTemplate<'a> {
pub args: &'a Args,
}
pub async fn not_found() -> Result<HttpResponse, Error> {
Ok(HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
}

23
src/endpoints/help.rs Normal file
View file

@ -0,0 +1,23 @@
use crate::args::{Args, ARGS};
use actix_web::{get, HttpResponse};
use askama::Template;
use std::marker::PhantomData;
#[derive(Template)]
#[template(path = "help.html")]
struct Help<'a> {
args: &'a Args,
_marker: PhantomData<&'a ()>,
}
#[get("/help")]
pub async fn help() -> HttpResponse {
HttpResponse::Found().content_type("text/html").body(
Help {
args: &ARGS,
_marker: Default::default(),
}
.render()
.unwrap(),
)
}

88
src/endpoints/pasta.rs Normal file
View file

@ -0,0 +1,88 @@
use actix_web::{get, web, HttpResponse};
use askama::Template;
use crate::args::{Args, ARGS};
use crate::endpoints::errors::ErrorTemplate;
use crate::pasta::Pasta;
use crate::util::animalnumbers::to_u64;
use crate::util::misc::remove_expired;
use crate::AppState;
#[derive(Template)]
#[template(path = "pasta.html", escape = "none")]
struct PastaTemplate<'a> {
pasta: &'a Pasta,
args: &'a Args,
}
#[get("/pasta/{id}")]
pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner()).unwrap_or(0);
println!("{}", id);
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
return HttpResponse::Found().content_type("text/html").body(
PastaTemplate {
pasta: &pasta,
args: &ARGS,
}
.render()
.unwrap(),
);
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/url/{id}")]
pub async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner()).unwrap_or(0);
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
if pasta.pasta_type == "url" {
return HttpResponse::Found()
.append_header(("Location", String::from(&pasta.content)))
.finish();
} else {
return HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap());
}
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/raw/{id}")]
pub async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> String {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner()).unwrap_or(0);
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
return pasta.content.to_owned();
}
}
String::from("Pasta not found! :-(")
}

View file

@ -0,0 +1,38 @@
use actix_web::{get, web, HttpResponse};
use askama::Template;
use crate::args::{Args, ARGS};
use crate::pasta::Pasta;
use crate::util::misc::remove_expired;
use crate::AppState;
#[derive(Template)]
#[template(path = "pastalist.html")]
struct PastaListTemplate<'a> {
pastas: &'a Vec<Pasta>,
args: &'a Args,
}
#[get("/pastalist")]
pub async fn list(data: web::Data<AppState>) -> HttpResponse {
if ARGS.no_listing {
return HttpResponse::Found()
.append_header(("Location", "/"))
.finish();
}
let mut pastas = data.pastas.lock().unwrap();
pastas.retain(|p| !p.private);
remove_expired(&mut pastas);
HttpResponse::Found().content_type("text/html").body(
PastaListTemplate {
pastas: &pastas,
args: &ARGS,
}
.render()
.unwrap(),
)
}

36
src/endpoints/remove.rs Normal file
View file

@ -0,0 +1,36 @@
use actix_web::{get, web, HttpResponse};
use crate::args::ARGS;
use crate::endpoints::errors::ErrorTemplate;
use crate::util::animalnumbers::to_u64;
use crate::util::misc::remove_expired;
use crate::AppState;
use askama::Template;
#[get("/remove/{id}")]
pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
if ARGS.readonly {
return HttpResponse::Found()
.append_header(("Location", "/"))
.finish();
}
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner()).unwrap_or(0);
remove_expired(&mut pastas);
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
pastas.remove(i);
return HttpResponse::Found()
.append_header(("Location", "/pastalist"))
.finish();
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}

View file

@ -0,0 +1,23 @@
use actix_web::{get, web, HttpResponse};
use askama::Template;
use std::marker::PhantomData;
#[derive(Template)]
#[template(path = "water.css", escape = "none")]
struct WaterCSS<'a> {
_marker: PhantomData<&'a ()>,
}
#[get("/static/{resource}")]
pub async fn static_resources(resource_id: web::Path<String>) -> HttpResponse {
match resource_id.into_inner().as_str() {
"water.css" => HttpResponse::Found().content_type("text/html").body(
WaterCSS {
_marker: Default::default(),
}
.render()
.unwrap(),
),
_ => HttpResponse::NotFound().content_type("text/html").finish(),
}
}

View file

@ -1,331 +1,49 @@
extern crate core;
use crate::args::ARGS;
use crate::endpoints::{
create, edit, errors, help, pasta as pasta_endpoint, pastalist, remove, static_resources,
};
use crate::pasta::Pasta;
use crate::util::dbio;
use actix_web::middleware::Condition;
use actix_web::{middleware, web, App, HttpServer};
use actix_web_httpauth::middleware::HttpAuthentication;
use chrono::Local;
use env_logger::Builder;
use log::LevelFilter;
use std::fs;
use std::io::Write;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use actix_files;
use actix_multipart::Multipart;
use actix_web::dev::ServiceRequest;
use actix_web::middleware::Condition;
use actix_web::{error, get, middleware, web, App, Error, HttpResponse, HttpServer, Responder};
use actix_web_httpauth::extractors::basic::BasicAuth;
use actix_web_httpauth::middleware::HttpAuthentication;
use askama::Template;
use chrono::Local;
use clap::Parser;
use futures::TryStreamExt as _;
use lazy_static::lazy_static;
use linkify::{LinkFinder, LinkKind};
use log::LevelFilter;
use rand::Rng;
use std::fs;
pub mod args;
pub mod pasta;
use crate::animalnumbers::{to_animal_names, to_u64};
use crate::dbio::save_to_file;
use crate::pasta::Pasta;
mod animalnumbers;
mod dbio;
mod pasta;
lazy_static! {
static ref ARGS: Args = Args::parse();
pub mod util {
pub mod animalnumbers;
pub mod auth;
pub mod dbio;
pub mod misc;
pub mod syntaxhighlighter;
}
struct AppState {
pastas: Mutex<Vec<Pasta>>,
pub mod endpoints {
pub mod create;
pub mod edit;
pub mod errors;
pub mod help;
pub mod pasta;
pub mod pastalist;
pub mod remove;
pub mod static_resources;
}
#[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(short, long, default_value_t = 8080)]
port: u32,
#[clap(short, long, default_value_t = 1)]
threads: u8,
#[clap(long)]
hide_header: bool,
#[clap(long)]
hide_footer: bool,
#[clap(long)]
pure_html: bool,
#[clap(long)]
no_listing: bool,
#[clap(long)]
auth_username: Option<String>,
#[clap(long)]
auth_password: Option<String>,
}
async fn auth_validator(
req: ServiceRequest,
credentials: BasicAuth,
) -> Result<ServiceRequest, Error> {
// check if username matches
if credentials.user_id().as_ref() == ARGS.auth_username.as_ref().unwrap() {
return match ARGS.auth_password.as_ref() {
Some(cred_pass) => match credentials.password() {
None => Err(error::ErrorBadRequest("Invalid login details.")),
Some(arg_pass) => {
if arg_pass == cred_pass {
Ok(req)
} else {
Err(error::ErrorBadRequest("Invalid login details."))
}
}
},
None => Ok(req),
};
} else {
Err(error::ErrorBadRequest("Invalid login details."))
}
}
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate<'a> {
args: &'a Args,
}
#[derive(Template)]
#[template(path = "error.html")]
struct ErrorTemplate<'a> {
args: &'a Args,
}
#[derive(Template)]
#[template(path = "pasta.html")]
struct PastaTemplate<'a> {
pasta: &'a Pasta,
args: &'a Args,
}
#[derive(Template)]
#[template(path = "pastalist.html")]
struct PastaListTemplate<'a> {
pastas: &'a Vec<Pasta>,
args: &'a Args,
}
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Found()
.content_type("text/html")
.body(IndexTemplate { args: &ARGS }.render().unwrap())
}
async fn not_found() -> Result<HttpResponse, Error> {
Ok(HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
}
async fn create(data: web::Data<AppState>, mut payload: Multipart) -> Result<HttpResponse, Error> {
let mut pastas = data.pastas.lock().unwrap();
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
} as i64;
let mut new_pasta = Pasta {
id: rand::thread_rng().gen::<u16>() as u64,
content: String::from("No Text Content"),
file: String::from("no-file"),
created: timenow,
pasta_type: String::from(""),
expiration: 0,
};
while let Some(mut field) = payload.try_next().await? {
match field.name() {
"expiration" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.expiration = match std::str::from_utf8(&chunk).unwrap() {
"1min" => timenow + 60,
"10min" => timenow + 60 * 10,
"1hour" => timenow + 60 * 60,
"24hour" => timenow + 60 * 60 * 24,
"1week" => timenow + 60 * 60 * 24 * 7,
"never" => 0,
_ => panic!("Unexpected expiration time!"),
};
}
continue;
}
"content" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.content = std::str::from_utf8(&chunk).unwrap().to_string();
new_pasta.pasta_type = if is_valid_url(new_pasta.content.as_str()) {
String::from("url")
} else {
String::from("text")
};
}
continue;
}
"file" => {
let content_disposition = field.content_disposition();
let filename = match content_disposition.get_filename() {
Some("") => continue,
Some(filename) => filename.replace(' ', "_").to_string(),
None => continue,
};
std::fs::create_dir_all(format!("./pasta_data/{}", &new_pasta.id_as_animals()))
.unwrap();
let filepath = format!("./pasta_data/{}/{}", &new_pasta.id_as_animals(), &filename);
new_pasta.file = filename;
let mut f = web::block(|| std::fs::File::create(filepath)).await??;
while let Some(chunk) = field.try_next().await? {
f = web::block(move || f.write_all(&chunk).map(|_| f)).await??;
}
new_pasta.pasta_type = String::from("text");
}
_ => {}
}
}
let id = new_pasta.id;
pastas.push(new_pasta);
save_to_file(&pastas);
Ok(HttpResponse::Found()
.append_header(("Location", format!("/pasta/{}", to_animal_names(id))))
.finish())
}
#[get("/pasta/{id}")]
async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner());
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
return HttpResponse::Found()
.content_type("text/html")
.body(PastaTemplate { pasta, args: &ARGS }.render().unwrap());
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/url/{id}")]
async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner());
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
if pasta.pasta_type == "url" {
return HttpResponse::Found()
.append_header(("Location", String::from(&pasta.content)))
.finish();
} else {
return HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap());
}
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/raw/{id}")]
async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> String {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner());
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
return pasta.content.to_owned();
}
}
String::from("Pasta not found! :-(")
}
#[get("/remove/{id}")]
async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner());
remove_expired(&mut pastas);
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
pastas.remove(i);
return HttpResponse::Found()
.append_header(("Location", "/pastalist"))
.finish();
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/pastalist")]
async fn list(data: web::Data<AppState>) -> HttpResponse {
if ARGS.no_listing {
return HttpResponse::Found()
.append_header(("Location", "/"))
.finish();
}
let mut pastas = data.pastas.lock().unwrap();
remove_expired(&mut pastas);
HttpResponse::Found().content_type("text/html").body(
PastaListTemplate {
pastas: &pastas,
args: &ARGS,
}
.render()
.unwrap(),
)
pub struct AppState {
pub pastas: Mutex<Vec<Pasta>>,
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let args: Args = Args::parse();
Builder::new()
.format(|buf, record| {
writeln!(
@ -341,10 +59,10 @@ async fn main() -> std::io::Result<()> {
log::info!(
"MicroBin starting on http://127.0.0.1:{}",
args.port.to_string()
ARGS.port.to_string()
);
match std::fs::create_dir_all("./pasta_data") {
match fs::create_dir_all("./pasta_data") {
Ok(dir) => dir,
Err(error) => {
log::error!("Couldn't create data directory ./pasta_data: {:?}", error);
@ -360,53 +78,27 @@ async fn main() -> std::io::Result<()> {
App::new()
.app_data(data.clone())
.wrap(middleware::NormalizePath::trim())
.service(index)
.service(getpasta)
.service(redirecturl)
.service(getrawpasta)
.service(actix_files::Files::new("/static", "./static"))
.service(create::index)
.service(help::help)
.service(pasta_endpoint::getpasta)
.service(pasta_endpoint::getrawpasta)
.service(pasta_endpoint::redirecturl)
.service(edit::get_edit)
.service(edit::post_edit)
.service(static_resources::static_resources)
.service(actix_files::Files::new("/file", "./pasta_data"))
.service(web::resource("/upload").route(web::post().to(create)))
.default_service(web::route().to(not_found))
.service(web::resource("/upload").route(web::post().to(create::create)))
.default_service(web::route().to(errors::not_found))
.wrap(middleware::Logger::default())
.service(remove)
.service(list)
.service(remove::remove)
.service(pastalist::list)
.wrap(Condition::new(
args.auth_username.is_some(),
HttpAuthentication::basic(auth_validator),
ARGS.auth_username.is_some(),
HttpAuthentication::basic(util::auth::auth_validator),
))
})
.bind(format!("0.0.0.0:{}", args.port.to_string()))?
.workers(args.threads as usize)
.bind(format!("0.0.0.0:{}", ARGS.port.to_string()))?
.workers(ARGS.threads as usize)
.run()
.await
}
fn remove_expired(pastas: &mut Vec<Pasta>) {
// get current time - this will be needed to check which pastas have expired
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
} as i64;
pastas.retain(|p| {
// expiration is `never` or not reached
if p.expiration == 0 || p.expiration > timenow {
// keep
true
} else {
// remove the file itself
fs::remove_file(format!("./pasta_data/{}/{}", p.id_as_animals(), p.file));
// and remove the containing directory
fs::remove_dir(format!("./pasta_data/{}/", p.id_as_animals()));
// remove
false
}
});
}
fn is_valid_url(url: &str) -> bool {
let finder = LinkFinder::new();
let spans: Vec<_> = finder.spans(url).collect();
spans[0].as_str() == url && Some(&LinkKind::Url) == spans[0].kind()
}

View file

@ -3,13 +3,17 @@ use std::fmt;
use chrono::{DateTime, Datelike, NaiveDateTime, Timelike, Utc};
use serde::{Deserialize, Serialize};
use crate::to_animal_names;
use crate::util::animalnumbers::to_animal_names;
use crate::util::syntaxhighlighter::html_highlight;
#[derive(Serialize, Deserialize)]
pub struct Pasta {
pub id: u64,
pub content: String,
pub file: String,
pub extension: String,
pub private: bool,
pub editable: bool,
pub created: i64,
pub expiration: i64,
pub pasta_type: String,
@ -46,6 +50,14 @@ impl Pasta {
)
}
}
pub fn content_syntax_highlighted(&self) -> String {
html_highlight(&self.content, &self.extension)
}
pub fn content_not_highlighted(&self) -> String {
html_highlight(&self.content, "txt")
}
}
impl fmt::Display for Pasta {

View file

@ -14,8 +14,8 @@ pub fn to_animal_names(mut number: u64) -> String {
return ANIMAL_NAMES[0].parse().unwrap();
}
// max 4 animals so 6 * 6 = 64 bits
let mut power = 6;
loop {
let digit = number / ANIMAL_NAMES.len().pow(power) as u64;
if !(result.is_empty() && digit == 0) {
@ -32,7 +32,7 @@ pub fn to_animal_names(mut number: u64) -> String {
result.join("-")
}
pub fn to_u64(animal_names: &str) -> u64 {
pub fn to_u64(animal_names: &str) -> Result<u64, &str> {
let mut result: u64 = 0;
let animals: Vec<&str> = animal_names.split("-").collect();
@ -40,9 +40,14 @@ pub fn to_u64(animal_names: &str) -> u64 {
let mut pow = animals.len();
for i in 0..animals.len() {
pow -= 1;
result += (ANIMAL_NAMES.iter().position(|&r| r == animals[i]).unwrap()
* ANIMAL_NAMES.len().pow(pow as u32)) as u64;
let animal_index = ANIMAL_NAMES.iter().position(|&r| r == animals[i]);
match animal_index {
None => return Err("Failed to convert animal name to u64!"),
Some(_) => {
result += (animal_index.unwrap() * ANIMAL_NAMES.len().pow(pow as u32)) as u64
}
}
}
result
Ok(result)
}

29
src/util/auth.rs Normal file
View file

@ -0,0 +1,29 @@
use actix_web::dev::ServiceRequest;
use actix_web::{error, Error};
use actix_web_httpauth::extractors::basic::BasicAuth;
use crate::args::ARGS;
pub async fn auth_validator(
req: ServiceRequest,
credentials: BasicAuth,
) -> Result<ServiceRequest, Error> {
// check if username matches
if credentials.user_id().as_ref() == ARGS.auth_username.as_ref().unwrap() {
return match ARGS.auth_password.as_ref() {
Some(cred_pass) => match credentials.password() {
None => Err(error::ErrorBadRequest("Invalid login details.")),
Some(arg_pass) => {
if arg_pass == cred_pass {
Ok(req)
} else {
Err(error::ErrorBadRequest("Invalid login details."))
}
}
},
None => Ok(req),
};
} else {
Err(error::ErrorBadRequest("Invalid login details."))
}
}

View file

@ -39,7 +39,10 @@ pub fn load_from_file() -> io::Result<Vec<Pasta>> {
match file {
Ok(_) => {
let reader = BufReader::new(file.unwrap());
let data: Vec<Pasta> = serde_json::from_reader(reader).unwrap();
let data: Vec<Pasta> = match serde_json::from_reader(reader) {
Ok(t) => t,
_ => Vec::new(),
};
Ok(data)
}
Err(_) => {

42
src/util/misc.rs Normal file
View file

@ -0,0 +1,42 @@
use std::time::{SystemTime, UNIX_EPOCH};
use linkify::{LinkFinder, LinkKind};
use std::fs;
use crate::Pasta;
pub fn remove_expired(pastas: &mut Vec<Pasta>) {
// get current time - this will be needed to check which pastas have expired
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
pastas.retain(|p| {
// expiration is `never` or not reached
if p.expiration == 0 || p.expiration > timenow {
// keep
true
} else {
// remove the file itself
fs::remove_file(format!("./pasta_data/{}/{}", p.id_as_animals(), p.file))
.expect(&*format!("Failed to delete file {}!", p.file));
// and remove the containing directory
fs::remove_dir(format!("./pasta_data/{}/", p.id_as_animals())).expect(&*format!(
"Failed to delete directory {}!",
p.id_as_animals()
));
// remove
false
}
});
}
pub fn is_valid_url(url: &str) -> bool {
let finder = LinkFinder::new();
let spans: Vec<_> = finder.spans(url).collect();
spans[0].as_str() == url && Some(&LinkKind::Url) == spans[0].kind()
}

View file

@ -0,0 +1,37 @@
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style, ThemeSet};
use syntect::html::append_highlighted_html_for_styled_line;
use syntect::html::IncludeBackground::No;
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
pub fn html_highlight(text: &str, extension: &str) -> String {
let ps = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
let syntax = ps
.find_syntax_by_extension(extension)
.or(Option::from(ps.find_syntax_plain_text()))
.unwrap();
let mut h = HighlightLines::new(syntax, &ts.themes["InspiredGitHub"]);
let mut highlighted_content: String = String::from("");
for line in LinesWithEndings::from(text) {
let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps).unwrap();
append_highlighted_html_for_styled_line(&ranges[..], No, &mut highlighted_content)
.expect("Failed to append highlighted line!");
}
let mut highlighted_content2: String = String::from("");
for line in highlighted_content.lines() {
highlighted_content2 += &*format!("<code-line>{}</code-line>\n", line);
}
// Rewrite colours to ones that are compatible with water.css and both light/dark modes
highlighted_content2 = highlighted_content2.replace("style=\"color:#323232;\"", "");
highlighted_content2 =
highlighted_content2.replace("style=\"color:#183691;\"", "style=\"color:blue;\"");
return highlighted_content2;
}