Merge branch 'actix'
This commit is contained in:
commit
e6642a7a94
13 changed files with 1868 additions and 1259 deletions
2571
Cargo.lock
generated
2571
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
29
Cargo.toml
29
Cargo.toml
|
@ -1,24 +1,31 @@
|
||||||
[package]
|
[package]
|
||||||
name = "bin"
|
name = "bin"
|
||||||
version = "1.0.5"
|
version = "2.0.0"
|
||||||
description = "a paste bin."
|
description = "a paste bin."
|
||||||
repository = "https://github.com/w4/bin"
|
repository = "https://github.com/w4/bin"
|
||||||
license = "WTFPL OR 0BSD"
|
license = "WTFPL OR 0BSD"
|
||||||
authors = ["Jordan Doyle <jordan@doyle.la>"]
|
authors = ["Jordan Doyle <jordan@doyle.la>"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
owning_ref = "0.4"
|
argh = "0.1"
|
||||||
|
log = "0.4"
|
||||||
|
pretty_env_logger = "0.4"
|
||||||
linked-hash-map = "0.5"
|
linked-hash-map = "0.5"
|
||||||
rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master" }
|
once_cell = "1.10"
|
||||||
askama = "0.9"
|
parking_lot = "0.12"
|
||||||
lazy_static = "1.4"
|
bytes = { version = "1.1", features = ["serde"] }
|
||||||
rand = { version = "0.7", features = ["nightly"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
rand = { version = "0.8" }
|
||||||
gpw = "0.1"
|
gpw = "0.1"
|
||||||
syntect = "4.1"
|
actix = "0.13"
|
||||||
serde_derive = "1.0"
|
actix-web = "4.0"
|
||||||
tokio = { version = "0.2", features = ["sync", "macros"] }
|
htmlescape = "0.3"
|
||||||
async-trait = "0.1"
|
askama = "0.11"
|
||||||
|
bat = "0.20"
|
||||||
|
syntect = "4.6"
|
||||||
|
tokio = { version = "1.17", features = ["sync"] }
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
FROM rust:1.34.2-slim-stretch AS builder
|
FROM rust:1-slim AS builder
|
||||||
RUN rustup install nightly-x86_64-unknown-linux-gnu
|
|
||||||
|
|
||||||
RUN apt update && apt install -y libclang-dev
|
RUN apt update && apt install -y libclang-dev
|
||||||
|
|
||||||
COPY . /sources
|
COPY . /sources
|
||||||
WORKDIR /sources
|
WORKDIR /sources
|
||||||
RUN cargo +nightly build --release
|
RUN cargo build --release
|
||||||
RUN chown nobody:nogroup /sources/target/release/bin
|
RUN chown nobody:nogroup /sources/target/release/bin
|
||||||
|
|
||||||
|
|
||||||
FROM debian:stretch-slim
|
FROM debian:bullseye-slim
|
||||||
COPY --from=builder /sources/target/release/bin /pastebin
|
COPY --from=builder /sources/target/release/bin /pastebin
|
||||||
|
|
||||||
USER nobody
|
USER nobody
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
ENTRYPOINT ["/pastebin"]
|
ENTRYPOINT ["/pastebin", "0.0.0.0:8000"]
|
||||||
|
|
19
README.md
19
README.md
|
@ -3,7 +3,7 @@ a paste bin.
|
||||||
|
|
||||||
A paste bin that's actually minimalist. No database requirement, no commenting functionality, no self-destructing or time bomb messages and no social media integration—just an application to quickly send snippets of text to people.
|
A paste bin that's actually minimalist. No database requirement, no commenting functionality, no self-destructing or time bomb messages and no social media integration—just an application to quickly send snippets of text to people.
|
||||||
|
|
||||||
[bin](https://bin.gy/) is written in Rust in around 200 lines of code. It's fast, it's simple, there's code highlighting and you can ⌘+A without going to the 'plain' page. It's revolutionary in the paste bin industry, disrupting markets and pushing boundaries never seen before.
|
[bin](https://bin.gy/) is written in Rust in around 300 lines of code. It's fast, it's simple, there's code highlighting and you can ⌘+A without going to the 'plain' page. It's revolutionary in the paste bin industry, disrupting markets and pushing boundaries never seen before.
|
||||||
|
|
||||||
##### so how do you get bin?
|
##### so how do you get bin?
|
||||||
|
|
||||||
|
@ -29,9 +29,22 @@ $ ./bin
|
||||||
|
|
||||||
##### funny, what settings are there?
|
##### funny, what settings are there?
|
||||||
|
|
||||||
bin uses [rocket](https://rocket.rs) so you can add a [rocket config file](https://api.rocket.rs/v0.3/rocket/config/) if you like. You can set `ROCKET_PORT` in your environment if you want to change the default port (8820).
|
```
|
||||||
|
$ ./bin
|
||||||
|
|
||||||
bin's only configuration value is `BIN_BUFFER_SIZE` which defaults to 2000. Change this value if you want your bin to hold more pastes.
|
Usage: bin [<bind_addr>] [--buffer-size <buffer-size>] [--max-paste-size <max-paste-size>]
|
||||||
|
|
||||||
|
a pastebin.
|
||||||
|
|
||||||
|
Positional Arguments:
|
||||||
|
bind_addr socket address to bind to (default: 127.0.0.1:8820)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--buffer-size maximum amount of pastes to store before rotating (default:
|
||||||
|
1000)
|
||||||
|
--max-paste-size maximum paste size in bytes (default. 32kB)
|
||||||
|
--help display usage information
|
||||||
|
```
|
||||||
|
|
||||||
##### is there curl support?
|
##### is there curl support?
|
||||||
|
|
||||||
|
|
56
src/errors.rs
Normal file
56
src/errors.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use actix_web::{body::BoxBody, http::header, http::StatusCode, web, HttpResponse, ResponseError};
|
||||||
|
|
||||||
|
use std::fmt::{Formatter, Write};
|
||||||
|
|
||||||
|
macro_rules! impl_response_error_for_http_resp {
|
||||||
|
($ty:ty, $path:expr, $status:expr) => {
|
||||||
|
impl ResponseError for $ty {
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
HtmlResponseError::error_response(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for $ty {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", include_str!($path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlResponseError for $ty {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
$status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NotFound;
|
||||||
|
|
||||||
|
impl_response_error_for_http_resp!(NotFound, "../templates/404.html", StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InternalServerError(pub Box<dyn std::error::Error>);
|
||||||
|
|
||||||
|
impl_response_error_for_http_resp!(
|
||||||
|
InternalServerError,
|
||||||
|
"../templates/500.html",
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
);
|
||||||
|
|
||||||
|
pub trait HtmlResponseError: ResponseError {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
let mut resp = HttpResponse::new(HtmlResponseError::status_code(self));
|
||||||
|
let mut buf = web::BytesMut::new();
|
||||||
|
let _ = write!(&mut buf, "{}", self);
|
||||||
|
resp.headers_mut().insert(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
header::HeaderValue::from_static("text/html; charset=utf-8"),
|
||||||
|
);
|
||||||
|
resp.set_body(BoxBody::new(buf))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,26 +1,45 @@
|
||||||
extern crate syntect;
|
use bat::assets::HighlightingAssets;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use syntect::{
|
||||||
|
html::{ClassStyle, ClassedHTMLGenerator},
|
||||||
|
parsing::SyntaxSet,
|
||||||
|
};
|
||||||
|
|
||||||
use syntect::easy::HighlightLines;
|
thread_local!(pub static BAT_ASSETS: HighlightingAssets = HighlightingAssets::from_binary());
|
||||||
use syntect::highlighting::ThemeSet;
|
|
||||||
use syntect::html::{styled_line_to_highlighted_html, IncludeBackground};
|
|
||||||
use syntect::parsing::SyntaxSet;
|
|
||||||
|
|
||||||
/// Takes the content of a paste and the extension passed in by the viewer and will return the content
|
/// Takes the content of a paste and the extension passed in by the viewer and will return the content
|
||||||
/// highlighted in the appropriate format in HTML.
|
/// highlighted in the appropriate format in HTML.
|
||||||
///
|
///
|
||||||
/// Returns `None` if the extension isn't supported.
|
/// Returns `None` if the extension isn't supported.
|
||||||
pub fn highlight(content: &str, ext: &str) -> Option<String> {
|
pub fn highlight(content: &str, ext: &str) -> Option<String> {
|
||||||
lazy_static! {
|
static SS: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
|
||||||
static ref SS: SyntaxSet = SyntaxSet::load_defaults_newlines();
|
|
||||||
static ref TS: ThemeSet = ThemeSet::load_defaults();
|
|
||||||
}
|
|
||||||
|
|
||||||
let syntax = SS.find_syntax_by_extension(ext)?;
|
BAT_ASSETS.with(|f| {
|
||||||
let mut h = HighlightLines::new(syntax, &TS.themes["base16-ocean.dark"]);
|
let ss = f.get_syntax_set().ok().unwrap_or(&SS);
|
||||||
let regions = h.highlight(content, &SS);
|
let syntax = ss.find_syntax_by_extension(ext)?;
|
||||||
|
let mut html_generator =
|
||||||
Some(styled_line_to_highlighted_html(
|
ClassedHTMLGenerator::new_with_class_style(syntax, ss, ClassStyle::Spaced);
|
||||||
®ions[..],
|
for line in LinesWithEndings(content.trim()) {
|
||||||
IncludeBackground::No,
|
html_generator.parse_html_for_line_which_includes_newline(line);
|
||||||
))
|
}
|
||||||
|
Some(html_generator.finalize())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LinesWithEndings<'a>(&'a str);
|
||||||
|
|
||||||
|
impl<'a> Iterator for LinesWithEndings<'a> {
|
||||||
|
type Item = &'a str;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.0.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let split = self.0.find('\n').map_or(self.0.len(), |i| i + 1);
|
||||||
|
let (line, rest) = self.0.split_at(split);
|
||||||
|
self.0 = rest;
|
||||||
|
Some(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
62
src/io.rs
62
src/io.rs
|
@ -1,42 +1,21 @@
|
||||||
extern crate gpw;
|
use actix_web::web::Bytes;
|
||||||
extern crate linked_hash_map;
|
|
||||||
extern crate owning_ref;
|
|
||||||
extern crate rand;
|
|
||||||
|
|
||||||
use rand::distributions::Alphanumeric;
|
|
||||||
use rand::{thread_rng, Rng};
|
|
||||||
|
|
||||||
use linked_hash_map::LinkedHashMap;
|
use linked_hash_map::LinkedHashMap;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use owning_ref::OwningRef;
|
use parking_lot::RwLock;
|
||||||
|
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::env;
|
|
||||||
|
|
||||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
pub type PasteStore = RwLock<LinkedHashMap<String, Bytes>>;
|
||||||
|
|
||||||
type RwLockReadGuardRef<'a, T, U = T> = OwningRef<Box<RwLockReadGuard<'a, T>>, U>;
|
static BUFFER_SIZE: Lazy<usize> = Lazy::new(|| argh::from_env::<crate::BinArgs>().buffer_size);
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref ENTRIES: RwLock<LinkedHashMap<String, String>> = RwLock::new(LinkedHashMap::new());
|
|
||||||
static ref BUFFER_SIZE: usize = env::var("BIN_BUFFER_SIZE")
|
|
||||||
.map(|f| f
|
|
||||||
.parse::<usize>()
|
|
||||||
.expect("Failed to parse value of BIN_BUFFER_SIZE"))
|
|
||||||
.unwrap_or(1000usize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensures `ENTRIES` is less than the size of `BIN_BUFFER_SIZE`. If it isn't then
|
/// Ensures `ENTRIES` is less than the size of `BIN_BUFFER_SIZE`. If it isn't then
|
||||||
/// `ENTRIES.len() - BIN_BUFFER_SIZE` elements will be popped off the front of the map.
|
/// `ENTRIES.len() - BIN_BUFFER_SIZE` elements will be popped off the front of the map.
|
||||||
///
|
///
|
||||||
/// During the purge, `ENTRIES` is locked and the current thread will block.
|
/// During the purge, `ENTRIES` is locked and the current thread will block.
|
||||||
async fn purge_old() {
|
fn purge_old(entries: &mut LinkedHashMap<String, Bytes>) {
|
||||||
let entries_len = ENTRIES.read().await.len();
|
if entries.len() > *BUFFER_SIZE {
|
||||||
|
let to_remove = entries.len() - *BUFFER_SIZE;
|
||||||
if entries_len > *BUFFER_SIZE {
|
|
||||||
let to_remove = entries_len - *BUFFER_SIZE;
|
|
||||||
|
|
||||||
let mut entries = ENTRIES.write().await;
|
|
||||||
|
|
||||||
for _ in 0..to_remove {
|
for _ in 0..to_remove {
|
||||||
entries.pop_front();
|
entries.pop_front();
|
||||||
|
@ -52,29 +31,24 @@ pub fn generate_id() -> String {
|
||||||
thread_rng()
|
thread_rng()
|
||||||
.sample_iter(&Alphanumeric)
|
.sample_iter(&Alphanumeric)
|
||||||
.take(6)
|
.take(6)
|
||||||
.collect::<String>()
|
.map(char::from)
|
||||||
|
.collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stores a paste under the given id
|
/// Stores a paste under the given id
|
||||||
pub async fn store_paste(id: String, content: String) {
|
pub fn store_paste(entries: &PasteStore, id: String, content: Bytes) {
|
||||||
purge_old().await;
|
let mut entries = entries.write();
|
||||||
|
|
||||||
ENTRIES.write().await.insert(id, content);
|
purge_old(&mut entries);
|
||||||
|
|
||||||
|
entries.insert(id, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a paste by id.
|
/// Get a paste by id.
|
||||||
///
|
///
|
||||||
/// Returns `None` if the paste doesn't exist.
|
/// Returns `None` if the paste doesn't exist.
|
||||||
pub async fn get_paste(
|
pub fn get_paste(entries: &PasteStore, id: &str) -> Option<Bytes> {
|
||||||
id: &str,
|
|
||||||
) -> Option<RwLockReadGuardRef<'_, LinkedHashMap<String, String>, String>> {
|
|
||||||
// need to box the guard until owning_ref understands Pin is a stable address
|
// need to box the guard until owning_ref understands Pin is a stable address
|
||||||
let or = RwLockReadGuardRef::new(Box::new(ENTRIES.read().await));
|
entries.read().get(id).map(Bytes::clone)
|
||||||
|
|
||||||
if or.contains_key(id) {
|
|
||||||
Some(or.map(|x| x.get(id).unwrap()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
221
src/main.rs
221
src/main.rs
|
@ -1,140 +1,193 @@
|
||||||
#![feature(proc_macro_hygiene, decl_macro)]
|
#![deny(clippy::pedantic)]
|
||||||
|
#![allow(clippy::unused_async)]
|
||||||
#[macro_use]
|
|
||||||
extern crate lazy_static;
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate rocket;
|
|
||||||
|
|
||||||
extern crate askama;
|
|
||||||
|
|
||||||
|
mod errors;
|
||||||
mod highlight;
|
mod highlight;
|
||||||
mod io;
|
mod io;
|
||||||
mod params;
|
mod params;
|
||||||
|
|
||||||
use highlight::highlight;
|
use crate::{
|
||||||
use io::{generate_id, get_paste, store_paste};
|
errors::{InternalServerError, NotFound},
|
||||||
use params::{HostHeader, IsPlaintextRequest};
|
highlight::highlight,
|
||||||
|
io::{generate_id, get_paste, store_paste, PasteStore},
|
||||||
|
params::{HostHeader, IsPlaintextRequest},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
http::header,
|
||||||
|
web::{self, Bytes, Data, FormConfig, PayloadConfig},
|
||||||
|
App, Error, HttpRequest, HttpResponse, HttpServer, Responder,
|
||||||
|
};
|
||||||
use askama::{Html as AskamaHtml, MarkupDisplay, Template};
|
use askama::{Html as AskamaHtml, MarkupDisplay, Template};
|
||||||
|
use log::{error, info};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
|
};
|
||||||
|
use syntect::html::{css_for_theme_with_class_style, ClassStyle};
|
||||||
|
|
||||||
use rocket::http::{ContentType, RawStr, Status};
|
#[derive(argh::FromArgs, Clone)]
|
||||||
use rocket::request::Form;
|
/// a pastebin.
|
||||||
use rocket::response::content::{Content, Html};
|
pub struct BinArgs {
|
||||||
use rocket::response::Redirect;
|
/// socket address to bind to (default: 127.0.0.1:8820)
|
||||||
use rocket::Data;
|
#[argh(
|
||||||
|
positional,
|
||||||
|
default = "SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8820)"
|
||||||
|
)]
|
||||||
|
bind_addr: SocketAddr,
|
||||||
|
/// maximum amount of pastes to store before rotating (default: 1000)
|
||||||
|
#[argh(option, default = "1000")]
|
||||||
|
buffer_size: usize,
|
||||||
|
/// maximum paste size in bytes (default. 32kB)
|
||||||
|
#[argh(option, default = "32 * 1024")]
|
||||||
|
max_paste_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
use std::borrow::Cow;
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
if std::env::var_os("RUST_LOG").is_none() {
|
||||||
|
std::env::set_var("RUST_LOG", "INFO");
|
||||||
|
}
|
||||||
|
pretty_env_logger::init();
|
||||||
|
|
||||||
use tokio::io::AsyncReadExt;
|
let args: BinArgs = argh::from_env();
|
||||||
|
|
||||||
///
|
let store = Data::new(PasteStore::default());
|
||||||
/// Homepage
|
|
||||||
///
|
let server = HttpServer::new({
|
||||||
|
let args = args.clone();
|
||||||
|
|
||||||
|
move || {
|
||||||
|
App::new()
|
||||||
|
.app_data(store.clone())
|
||||||
|
.app_data(PayloadConfig::default().limit(args.max_paste_size))
|
||||||
|
.app_data(FormConfig::default().limit(args.max_paste_size))
|
||||||
|
.wrap(actix_web::middleware::Compress::default())
|
||||||
|
.route("/", web::get().to(index))
|
||||||
|
.route("/", web::post().to(submit))
|
||||||
|
.route("/", web::put().to(submit_raw))
|
||||||
|
.route("/", web::head().to(HttpResponse::MethodNotAllowed))
|
||||||
|
.route("/highlight.css", web::get().to(highlight_css))
|
||||||
|
.route("/{paste}", web::get().to(show_paste))
|
||||||
|
.route("/{paste}", web::head().to(HttpResponse::MethodNotAllowed))
|
||||||
|
.default_service(web::to(|req: HttpRequest| async move {
|
||||||
|
error!("Couldn't find resource {}", req.uri());
|
||||||
|
HttpResponse::from_error(NotFound)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
info!("Listening on http://{}", args.bind_addr);
|
||||||
|
|
||||||
|
server.bind(args.bind_addr)?.run().await
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
struct Index;
|
struct Index;
|
||||||
|
|
||||||
#[get("/")]
|
async fn index(req: HttpRequest) -> Result<HttpResponse, Error> {
|
||||||
fn index() -> Result<Html<String>, Status> {
|
render_template(&req, &Index)
|
||||||
Index
|
|
||||||
.render()
|
|
||||||
.map(Html)
|
|
||||||
.map_err(|_| Status::InternalServerError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
#[derive(serde::Deserialize)]
|
||||||
/// Submit Paste
|
|
||||||
///
|
|
||||||
|
|
||||||
#[derive(FromForm)]
|
|
||||||
struct IndexForm {
|
struct IndexForm {
|
||||||
val: String,
|
val: Bytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/", data = "<input>")]
|
async fn submit(input: web::Form<IndexForm>, store: Data<PasteStore>) -> impl Responder {
|
||||||
async fn submit(input: Form<IndexForm>) -> Redirect {
|
|
||||||
let id = generate_id();
|
let id = generate_id();
|
||||||
let uri = uri!(show_paste: &id);
|
let uri = format!("/{}", &id);
|
||||||
store_paste(id, input.into_inner().val).await;
|
store_paste(&store, id, input.into_inner().val);
|
||||||
Redirect::to(uri)
|
HttpResponse::Found()
|
||||||
|
.append_header((header::LOCATION, uri))
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/", data = "<input>")]
|
async fn submit_raw(
|
||||||
async fn submit_raw(input: Data, host: HostHeader<'_>) -> Result<String, Status> {
|
data: Bytes,
|
||||||
let mut data = String::new();
|
host: HostHeader,
|
||||||
input
|
store: Data<PasteStore>,
|
||||||
.open()
|
) -> Result<String, Error> {
|
||||||
.take(1024 * 1000)
|
|
||||||
.read_to_string(&mut data)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Status::InternalServerError)?;
|
|
||||||
|
|
||||||
let id = generate_id();
|
let id = generate_id();
|
||||||
let uri = uri!(show_paste: &id);
|
let uri = if let Some(Ok(host)) = host.0.as_ref().map(|v| std::str::from_utf8(v.as_bytes())) {
|
||||||
|
format!("https://{}/{}", host, id)
|
||||||
|
} else {
|
||||||
|
format!("/{}", id)
|
||||||
|
};
|
||||||
|
|
||||||
store_paste(id, data).await;
|
store_paste(&store, id, data);
|
||||||
|
|
||||||
match *host {
|
Ok(uri)
|
||||||
Some(host) => Ok(format!("https://{}{}", host, uri)),
|
|
||||||
None => Ok(format!("{}", uri)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
/// Show paste page
|
|
||||||
///
|
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "paste.html")]
|
#[template(path = "paste.html")]
|
||||||
struct ShowPaste<'a> {
|
struct ShowPaste<'a> {
|
||||||
content: MarkupDisplay<AskamaHtml, Cow<'a, String>>,
|
content: MarkupDisplay<AskamaHtml, Cow<'a, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<key>")]
|
async fn show_paste(
|
||||||
async fn show_paste(key: String, plaintext: IsPlaintextRequest) -> Result<Content<String>, Status> {
|
req: HttpRequest,
|
||||||
|
key: actix_web::web::Path<String>,
|
||||||
|
plaintext: IsPlaintextRequest,
|
||||||
|
store: Data<PasteStore>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
let mut splitter = key.splitn(2, '.');
|
let mut splitter = key.splitn(2, '.');
|
||||||
let key = splitter.next().ok_or_else(|| Status::NotFound)?;
|
let key = splitter.next().unwrap();
|
||||||
let ext = splitter.next();
|
let ext = splitter.next();
|
||||||
|
|
||||||
let entry = &*get_paste(key).await.ok_or_else(|| Status::NotFound)?;
|
let entry = get_paste(&store, key).ok_or(NotFound)?;
|
||||||
|
|
||||||
if *plaintext {
|
if *plaintext {
|
||||||
Ok(Content(ContentType::Plain, entry.to_string()))
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type("text/plain; charset=utf-8")
|
||||||
|
.body(entry))
|
||||||
} else {
|
} else {
|
||||||
|
let data = std::str::from_utf8(entry.as_ref())?;
|
||||||
|
|
||||||
let code_highlighted = match ext {
|
let code_highlighted = match ext {
|
||||||
Some(extension) => match highlight(&entry, extension) {
|
Some(extension) => match highlight(data, extension) {
|
||||||
Some(html) => html,
|
Some(html) => html,
|
||||||
None => return Err(Status::NotFound),
|
None => return Err(NotFound.into()),
|
||||||
},
|
},
|
||||||
None => String::from(RawStr::from_str(entry).html_escape()),
|
None => htmlescape::encode_minimal(data),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add <code> tags to enable line numbering with CSS
|
// Add <code> tags to enable line numbering with CSS
|
||||||
let html = format!(
|
let html = format!(
|
||||||
"<code>{}</code>",
|
"<code>{}</code>",
|
||||||
code_highlighted.replace("\n", "\n</code><code>")
|
code_highlighted.replace('\n', "</code><code>")
|
||||||
);
|
);
|
||||||
|
|
||||||
let content = MarkupDisplay::new_safe(Cow::Borrowed(&html), AskamaHtml);
|
let content = MarkupDisplay::new_safe(Cow::Borrowed(&html), AskamaHtml);
|
||||||
|
|
||||||
let template = ShowPaste { content };
|
render_template(&req, &ShowPaste { content })
|
||||||
match template.render() {
|
}
|
||||||
Ok(html) => Ok(Content(ContentType::HTML, html)),
|
}
|
||||||
Err(_) => Err(Status::InternalServerError),
|
|
||||||
|
async fn highlight_css() -> HttpResponse {
|
||||||
|
static CSS: Lazy<Bytes> = Lazy::new(|| {
|
||||||
|
highlight::BAT_ASSETS.with(|s| {
|
||||||
|
Bytes::from(css_for_theme_with_class_style(
|
||||||
|
s.get_theme("OneHalfDark"),
|
||||||
|
ClassStyle::Spaced,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("text/css")
|
||||||
|
.body(CSS.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_template<T: Template>(req: &HttpRequest, template: &T) -> Result<HttpResponse, Error> {
|
||||||
|
match template.render() {
|
||||||
|
Ok(html) => Ok(HttpResponse::Ok().content_type("text/html").body(html)),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error while rendering template for {}: {}", req.uri(), e);
|
||||||
|
Err(InternalServerError(Box::new(e)).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let result = rocket::ignite()
|
|
||||||
.mount("/", routes![index, submit, submit_raw, show_paste])
|
|
||||||
.launch()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Err(e) = result {
|
|
||||||
eprintln!("Failed to launch Rocket: {:#?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use rocket::request::{FromRequest, Outcome};
|
use actix_web::{
|
||||||
use rocket::Request;
|
dev::Payload,
|
||||||
|
http::header::{self, HeaderValue},
|
||||||
use async_trait::async_trait;
|
FromRequest, HttpMessage, HttpRequest,
|
||||||
|
};
|
||||||
|
use futures::future::ok;
|
||||||
|
|
||||||
/// Holds a value that determines whether or not this request wanted a plaintext response.
|
/// Holds a value that determines whether or not this request wanted a plaintext response.
|
||||||
///
|
///
|
||||||
|
@ -19,26 +21,22 @@ impl Deref for IsPlaintextRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
impl FromRequest for IsPlaintextRequest {
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for IsPlaintextRequest {
|
type Error = actix_web::Error;
|
||||||
type Error = ();
|
type Future = futures::future::Ready<Result<Self, Self::Error>>;
|
||||||
|
|
||||||
async fn from_request(request: &'a Request<'r>) -> Outcome<IsPlaintextRequest, ()> {
|
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||||
if let Some(format) = request.format() {
|
if req.content_type() == "text/plain" {
|
||||||
if format.is_plain() {
|
return ok(IsPlaintextRequest(true));
|
||||||
return Outcome::Success(IsPlaintextRequest(true));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match request
|
match req
|
||||||
.headers()
|
.headers()
|
||||||
.get_one("User-Agent")
|
.get(header::USER_AGENT)
|
||||||
.and_then(|u| u.splitn(2, '/').next())
|
.and_then(|u| u.to_str().unwrap().split('/').next())
|
||||||
{
|
{
|
||||||
None | Some("Wget") | Some("curl") | Some("HTTPie") => {
|
None | Some("Wget" | "curl" | "HTTPie") => ok(IsPlaintextRequest(true)),
|
||||||
Outcome::Success(IsPlaintextRequest(true))
|
_ => ok(IsPlaintextRequest(false)),
|
||||||
}
|
|
||||||
_ => Outcome::Success(IsPlaintextRequest(false)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,21 +45,13 @@ impl<'a, 'r> FromRequest<'a, 'r> for IsPlaintextRequest {
|
||||||
///
|
///
|
||||||
/// The inner value of this `HostHeader` will be `None` if there was no Host header
|
/// The inner value of this `HostHeader` will be `None` if there was no Host header
|
||||||
/// on the request.
|
/// on the request.
|
||||||
pub struct HostHeader<'a>(pub Option<&'a str>);
|
pub struct HostHeader(pub Option<HeaderValue>);
|
||||||
|
|
||||||
impl<'a> Deref for HostHeader<'a> {
|
impl FromRequest for HostHeader {
|
||||||
type Target = Option<&'a str>;
|
type Error = actix_web::Error;
|
||||||
|
type Future = futures::future::Ready<Result<Self, Self::Error>>;
|
||||||
|
|
||||||
fn deref(&self) -> &Option<&'a str> {
|
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||||
&self.0
|
ok(Self(req.headers().get(header::HOST).cloned()))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for HostHeader<'a> {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
async fn from_request(request: &'a Request<'r>) -> Outcome<HostHeader<'a>, ()> {
|
|
||||||
Outcome::Success(HostHeader(request.headers().get_one("Host")))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
templates/404.html
Normal file
15
templates/404.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>404 Not Found</title>
|
||||||
|
</head>
|
||||||
|
<body align="center">
|
||||||
|
<div align="center">
|
||||||
|
<h1>404: Not Found</h1>
|
||||||
|
<p>The requested resource could not be found.</p>
|
||||||
|
<hr />
|
||||||
|
<small>bin.</small>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
15
templates/500.html
Normal file
15
templates/500.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>500 Internal Server Error</title>
|
||||||
|
</head>
|
||||||
|
<body align="center">
|
||||||
|
<div align="center">
|
||||||
|
<h1>500: Internal Server Error</h1>
|
||||||
|
<p>An error occurred while fetching the requested resource.</p>
|
||||||
|
<hr />
|
||||||
|
<small>bin.</small>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,18 +1,14 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<title>bin.</title>
|
<title>bin.</title>
|
||||||
|
|
||||||
<link rel="help" href="https://github.com/w4/bin">
|
<link rel="help" href="https://github.com/w4/bin">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
html, body { margin: 0; }
|
html, body { margin: 0; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
@ -20,13 +16,11 @@
|
||||||
color: #B0BEC5;
|
color: #B0BEC5;
|
||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: 'Courier New', Courier, monospace;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
{% block styles %}{% endblock styles %}
|
||||||
{% block styles %}
|
|
||||||
{% endblock styles %}
|
|
||||||
</style>
|
</style>
|
||||||
|
{% block head %}{% endblock head %}
|
||||||
</head>
|
</head>
|
||||||
<body>{% block content %}{% endblock content %}</body>
|
<body>{% block content %}{% endblock content %}</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
code {
|
code {
|
||||||
counter-increment: line;
|
counter-increment: line;
|
||||||
}
|
}
|
||||||
|
|
||||||
code::before {
|
code::before {
|
||||||
content: counter(line);
|
content: counter(line);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -24,8 +23,10 @@
|
||||||
color: #888;
|
color: #888;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
{% endblock styles %}
|
{% endblock styles %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="highlight.css" />
|
||||||
|
{% endblock head %}
|
||||||
|
|
||||||
{% block content %}<pre>{{ content|safe }}</pre>{% endblock content %}
|
{% block content %}<pre>{{ content|safe }}</pre>{% endblock content %}
|
Loading…
Add table
Add a link
Reference in a new issue