Initial commit

This commit is contained in:
Kaczanowski Mateusz 2020-05-09 00:10:11 +02:00
commit 6133cf9a46
16 changed files with 2050 additions and 0 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

8
.travis.yml Normal file
View file

@ -0,0 +1,8 @@
language: rust
rust:
- stable
- beta
- nightly
script:
- cargo build --verbose --all
- cargo test --verbose --all

19
Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "pastebin"
version = "0.1.0"
authors = ["Kaczanowski Mateusz <kaczanowski.mateusz@gmail.com>"]
edition = "2018"
[dependencies]
rocket = { version = "0.4.4", features = ["tls"] }
rocksdb = "0.13.0"
nanoid = "0.3.0"
flatbuffers = "0.6.1"
structopt = "0.1.3"
structopt-derive = "0.1.3"
num_cpus = "1.0"
handlebars = "3.0.1"
maplit = "1.0.2"
tempdir = "0.3.7"
speculate = "0.1.2"
chrono = "0.4.11"

11
Dockerfile Normal file
View file

@ -0,0 +1,11 @@
FROM rustlang/rust:nightly
RUN apt-get update && apt-get install -y apt-utils software-properties-common lsb-release
RUN bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"
WORKDIR /usr/src/pastebin
COPY . .
RUN cargo install --path .
CMD ["pastebin"]

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Mateusz Kaczanowski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

7
README.md Normal file
View file

@ -0,0 +1,7 @@
[![Build Status][travis-badge]][travis]
[travis-badge]: https://travis-ci.org/mkaczanowski/packer-builder-arm.svg?branch=master
# Pastebin
Simple, fast and standalone pastebin service

12
api.fbs Normal file
View file

@ -0,0 +1,12 @@
namespace api;
table Entry {
create_timestamp:ulong;
expiry_timestamp:ulong;
data:[ubyte];
lang:string;
burn:bool;
encrypted:bool;
}
root_type Entry;

180
src/api_generated.rs Normal file
View file

@ -0,0 +1,180 @@
// automatically generated by the FlatBuffers compiler, do not modify
#![allow(warnings)]
#![allow(clippy)]
#![allow(unknown_lints)]
use std::mem;
use std::cmp::Ordering;
extern crate flatbuffers;
use self::flatbuffers::EndianScalar;
#[allow(unused_imports, dead_code)]
pub mod api {
use std::mem;
use std::cmp::Ordering;
extern crate flatbuffers;
use self::flatbuffers::EndianScalar;
pub enum EntryOffset {}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Entry<'a> {
pub _tab: flatbuffers::Table<'a>,
}
impl<'a> flatbuffers::Follow<'a> for Entry<'a> {
type Inner = Entry<'a>;
#[inline]
fn follow(buf: &'a [u8], loc: usize) -> Self::Inner {
Self {
_tab: flatbuffers::Table { buf: buf, loc: loc },
}
}
}
impl<'a> Entry<'a> {
#[inline]
pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self {
Entry {
_tab: table,
}
}
#[allow(unused_mut)]
pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>(
_fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>,
args: &'args EntryArgs<'args>) -> flatbuffers::WIPOffset<Entry<'bldr>> {
let mut builder = EntryBuilder::new(_fbb);
builder.add_expiry_timestamp(args.expiry_timestamp);
builder.add_create_timestamp(args.create_timestamp);
if let Some(x) = args.lang { builder.add_lang(x); }
if let Some(x) = args.data { builder.add_data(x); }
builder.add_encrypted(args.encrypted);
builder.add_burn(args.burn);
builder.finish()
}
pub const VT_CREATE_TIMESTAMP: flatbuffers::VOffsetT = 4;
pub const VT_EXPIRY_TIMESTAMP: flatbuffers::VOffsetT = 6;
pub const VT_DATA: flatbuffers::VOffsetT = 8;
pub const VT_LANG: flatbuffers::VOffsetT = 10;
pub const VT_BURN: flatbuffers::VOffsetT = 12;
pub const VT_ENCRYPTED: flatbuffers::VOffsetT = 14;
#[inline]
pub fn create_timestamp(&self) -> u64 {
self._tab.get::<u64>(Entry::VT_CREATE_TIMESTAMP, Some(0)).unwrap()
}
#[inline]
pub fn expiry_timestamp(&self) -> u64 {
self._tab.get::<u64>(Entry::VT_EXPIRY_TIMESTAMP, Some(0)).unwrap()
}
#[inline]
pub fn data(&self) -> Option<&'a [u8]> {
self._tab.get::<flatbuffers::ForwardsUOffset<flatbuffers::Vector<'a, u8>>>(Entry::VT_DATA, None).map(|v| v.safe_slice())
}
#[inline]
pub fn lang(&self) -> Option<&'a str> {
self._tab.get::<flatbuffers::ForwardsUOffset<&str>>(Entry::VT_LANG, None)
}
#[inline]
pub fn burn(&self) -> bool {
self._tab.get::<bool>(Entry::VT_BURN, Some(false)).unwrap()
}
#[inline]
pub fn encrypted(&self) -> bool {
self._tab.get::<bool>(Entry::VT_ENCRYPTED, Some(false)).unwrap()
}
}
pub struct EntryArgs<'a> {
pub create_timestamp: u64,
pub expiry_timestamp: u64,
pub data: Option<flatbuffers::WIPOffset<flatbuffers::Vector<'a , u8>>>,
pub lang: Option<flatbuffers::WIPOffset<&'a str>>,
pub burn: bool,
pub encrypted: bool,
}
impl<'a> Default for EntryArgs<'a> {
#[inline]
fn default() -> Self {
EntryArgs {
create_timestamp: 0,
expiry_timestamp: 0,
data: None,
lang: None,
burn: false,
encrypted: false,
}
}
}
pub struct EntryBuilder<'a: 'b, 'b> {
fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>,
start_: flatbuffers::WIPOffset<flatbuffers::TableUnfinishedWIPOffset>,
}
impl<'a: 'b, 'b> EntryBuilder<'a, 'b> {
#[inline]
pub fn add_create_timestamp(&mut self, create_timestamp: u64) {
self.fbb_.push_slot::<u64>(Entry::VT_CREATE_TIMESTAMP, create_timestamp, 0);
}
#[inline]
pub fn add_expiry_timestamp(&mut self, expiry_timestamp: u64) {
self.fbb_.push_slot::<u64>(Entry::VT_EXPIRY_TIMESTAMP, expiry_timestamp, 0);
}
#[inline]
pub fn add_data(&mut self, data: flatbuffers::WIPOffset<flatbuffers::Vector<'b , u8>>) {
self.fbb_.push_slot_always::<flatbuffers::WIPOffset<_>>(Entry::VT_DATA, data);
}
#[inline]
pub fn add_lang(&mut self, lang: flatbuffers::WIPOffset<&'b str>) {
self.fbb_.push_slot_always::<flatbuffers::WIPOffset<_>>(Entry::VT_LANG, lang);
}
#[inline]
pub fn add_burn(&mut self, burn: bool) {
self.fbb_.push_slot::<bool>(Entry::VT_BURN, burn, false);
}
#[inline]
pub fn add_encrypted(&mut self, encrypted: bool) {
self.fbb_.push_slot::<bool>(Entry::VT_ENCRYPTED, encrypted, false);
}
#[inline]
pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> EntryBuilder<'a, 'b> {
let start = _fbb.start_table();
EntryBuilder {
fbb_: _fbb,
start_: start,
}
}
#[inline]
pub fn finish(self) -> flatbuffers::WIPOffset<Entry<'a>> {
let o = self.fbb_.end_table(self.start_);
flatbuffers::WIPOffset::new(o.value())
}
}
#[inline]
pub fn get_root_as_entry<'a>(buf: &'a [u8]) -> Entry<'a> {
flatbuffers::get_root::<Entry<'a>>(buf)
}
#[inline]
pub fn get_size_prefixed_root_as_entry<'a>(buf: &'a [u8]) -> Entry<'a> {
flatbuffers::get_size_prefixed_root::<Entry<'a>>(buf)
}
#[inline]
pub fn finish_entry_buffer<'a, 'b>(
fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>,
root: flatbuffers::WIPOffset<Entry<'a>>) {
fbb.finish(root, None);
}
#[inline]
pub fn finish_size_prefixed_entry_buffer<'a, 'b>(fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>, root: flatbuffers::WIPOffset<Entry<'a>>) {
fbb.finish_size_prefixed(root, None);
}
} // pub mod api

129
src/lib.rs Normal file
View file

@ -0,0 +1,129 @@
extern crate flatbuffers;
use std::io;
use std::time::SystemTime;
use flatbuffers::FlatBufferBuilder;
use rocket::State;
use rocksdb::{compaction_filter, DB};
#[path = "api_generated.rs"]
mod api_generated;
use crate::api_generated::api::{finish_entry_buffer, get_root_as_entry, Entry, EntryArgs};
#[macro_export]
macro_rules! load_static_resources {
( $( $x:expr ),* ) => {
{
let mut resources: HashMap<&str, &[u8]> = HashMap::new();
$(
resources.insert($x, include_bytes!($x));
)*
resources
}
};
}
pub fn compaction_filter_expired_entries(
_: u32,
_: &[u8],
value: &[u8],
) -> compaction_filter::Decision {
use compaction_filter::Decision::*;
let entry = get_root_as_entry(value);
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("time went backwards")
.as_secs();
if entry.expiry_timestamp() != 0 && now >= entry.expiry_timestamp() {
Remove
} else {
Keep
}
}
pub fn get_extension(filename: &str) -> &str {
filename
.rfind('.')
.map(|idx| &filename[idx..])
.filter(|ext| ext.chars().skip(1).all(|c| c.is_ascii_alphanumeric()))
.unwrap_or("")
}
pub fn get_entry_data<'r>(id: &str, state: &'r State<'r, DB>) -> Result<Vec<u8>, io::Error> {
// read data from DB to Entry struct
let root = match state.get(id).unwrap() {
Some(root) => root,
None => return Err(io::Error::new(io::ErrorKind::NotFound, "record not found")),
};
let entry = get_root_as_entry(&root);
// check if data expired (might not be yet deleted by rocksb compaction hook)
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("time went backwards")
.as_secs();
if entry.expiry_timestamp() != 0 && now >= entry.expiry_timestamp() {
state.delete(id).unwrap();
return Err(io::Error::new(io::ErrorKind::NotFound, "record not found"));
}
// "burn" one time only pastebin content
if entry.burn() {
state.delete(id).unwrap();
}
Ok(root)
}
pub fn new_entry(
dest: &mut Vec<u8>,
data: &mut rocket::data::DataStream,
lang: String,
ttl: u64,
burn: bool,
encrypted: bool,
) {
let mut bldr = FlatBufferBuilder::new();
dest.clear();
bldr.reset();
// potential speed improvement over the create_vector:
// https://docs.rs/flatbuffers/0.6.1/flatbuffers/struct.FlatBufferBuilder.html#method.create_vector
let mut tmp_vec: Vec<u8> = vec![];
std::io::copy(data, &mut tmp_vec).unwrap();
bldr.start_vector::<u8>(tmp_vec.len());
for byte in tmp_vec.iter().rev() {
bldr.push::<u8>(*byte);
}
let data_vec = bldr.end_vector::<u8>(tmp_vec.len());
// calc expiry datetime
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("time went backwards")
.as_secs();
let expiry = if ttl == 0 { ttl } else { now + ttl };
// setup actual struct
let args = EntryArgs {
create_timestamp: now,
expiry_timestamp: expiry,
data: Some(data_vec),
lang: Some(bldr.create_string(&lang)),
burn,
encrypted,
};
let user_offset = Entry::create(&mut bldr, &args);
finish_entry_buffer(&mut bldr, user_offset);
let finished_data = bldr.finished_data();
dest.extend_from_slice(finished_data);
}

569
src/main.rs Normal file
View file

@ -0,0 +1,569 @@
#![feature(proc_macro_hygiene, decl_macro)]
#![allow(clippy::too_many_arguments)]
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate structopt_derive;
#[macro_use]
extern crate maplit;
extern crate flatbuffers;
extern crate handlebars;
extern crate nanoid;
extern crate num_cpus;
extern crate speculate;
extern crate structopt;
extern crate chrono;
#[macro_use]
mod lib;
use lib::{compaction_filter_expired_entries, get_entry_data, get_extension, new_entry};
mod api_generated;
use api_generated::api::get_root_as_entry;
use std::collections::HashMap;
use std::io;
use std::io::Cursor;
use rocket::config::{Config, Environment};
use rocket::http::{ContentType, Status};
use rocket::response::{Redirect, Response};
use rocket::{Data, State};
use handlebars::Handlebars;
use nanoid::nanoid;
use rocksdb::{Options, DB};
use structopt::StructOpt;
use chrono::NaiveDateTime;
use speculate::speculate;
speculate! {
use super::rocket;
use rocket::local::Client;
use rocket::http::Status;
before {
use tempdir::TempDir;
// setup temporary database
let tmp_dir = TempDir::new("rocks_db_test").unwrap();
let file_path = tmp_dir.path().join("database");
let mut pastebin_config = PastebinConfig::from_args();
pastebin_config.db_path = file_path.to_str().unwrap().to_string();
let rocket = rocket(pastebin_config);
// init rocket client
let client = Client::new(rocket).expect("invalid rocket instance");
}
#[allow(dead_code)]
fn insert_data<'r>(client: &'r Client, data: &str, path: &str) -> String {
let mut response = client.post(path)
.body(data)
.dispatch();
assert_eq!(response.status(), Status::Ok);
// retrieve paste ID
let url = response.body_string().unwrap();
let id = url.split('/').collect::<Vec<&str>>().last().cloned().unwrap();
id.to_string()
}
#[allow(dead_code)]
fn get_data(client: &Client, path: String) -> rocket::local::LocalResponse {
client.get(format!("/{}", path)).dispatch()
}
it "can get create and fetch paste" {
// store data via post request
let id = insert_data(&client, "random_test_data_to_be_checked", "/");
// retrieve the data via get request
let mut response = get_data(&client, id);
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(response.body_string().unwrap().contains("random_test_data_to_be_checked"));
}
it "can remove paste by id" {
let response = client.delete("/some_id").dispatch();
assert_eq!(response.status(), Status::Ok);
let response = get_data(&client, "some_id".to_string());
assert_eq!(response.status(), Status::NotFound);
}
it "can remove non-existing paste" {
let response = get_data(&client, "some_fake_id".to_string());
assert_eq!(response.status(), Status::NotFound);
let response = client.delete("/some_fake_id").dispatch();
assert_eq!(response.status(), Status::Ok);
let response = get_data(&client, "some_fake_id".to_string());
assert_eq!(response.status(), Status::NotFound);
}
it "can get raw contents" {
// store data via post request
let id = insert_data(&client, "random_test_data_to_be_checked", "/");
// retrieve the data via get request
let mut response = get_data(&client, format!("raw/{}", id));
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::Plain));
assert!(response.body_string().unwrap().contains("random_test_data_to_be_checked"));
}
it "can download contents" {
// store data via post request
let id = insert_data(&client, "random_test_data_to_be_checked", "/");
// retrieve the data via get request
let mut response = get_data(&client, format!("download/{}", id));
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::Binary));
assert!(response.body_string().unwrap().contains("random_test_data_to_be_checked"));
}
it "can clone contents" {
// store data via post request
let id = insert_data(&client, "random_test_data_to_be_checked", "/");
// retrieve the data via get request
let mut response = get_data(&client, format!("new?id={}", id));
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(response.body_string().unwrap().contains("random_test_data_to_be_checked"));
}
it "can't get burned paste" {
// store data via post request
let id = insert_data(&client, "random_test_data_to_be_checked", "/?burn=true");
let response = get_data(&client, id.clone());
assert_eq!(response.status(), Status::Ok);
// retrieve the data via get request
let response = get_data(&client, id);
assert_eq!(response.status(), Status::NotFound);
}
it "can't get expired paste" {
use std::{thread, time};
// store data via post request
let id = insert_data(&client, "random_test_data_to_be_checked", "/?ttl=1");
let response = get_data(&client, id.clone());
assert_eq!(response.status(), Status::Ok);
thread::sleep(time::Duration::from_secs(1));
// retrieve the data via get request
let response = get_data(&client, id);
assert_eq!(response.status(), Status::NotFound);
}
it "can get static contents" {
let mut response = client.get("/static/favicon.ico").dispatch();
let contents = std::fs::read("static/favicon.ico").unwrap();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.body_bytes(), Some(contents));
}
}
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(StructOpt, Debug)]
#[structopt(
name = "pastebin",
about = "Simple, standalone and fast pastebin service."
)]
struct PastebinConfig {
#[structopt(
long = "address",
help = "IP address or host to listen on",
default_value = "localhost"
)]
address: String,
#[structopt(
long = "port",
help = "Port number to listen on",
default_value = "8000"
)]
port: u16,
#[structopt(
long = "environment",
help = "Rocket server environment",
default_value = "production"
)]
environment: String,
#[structopt(
long = "workers",
help = "Number of concurrent thread workers",
default_value = "8"
)]
workers: u16,
#[structopt(
long = "keep-alive",
help = "Keep-alive timeout in seconds",
default_value = "5"
)]
keep_alive: u32,
#[structopt(long = "log", help = "Max log level", default_value = "normal")]
log: rocket::config::LoggingLevel,
#[structopt(
long = "ttl",
help = "Time to live for entries, by default kept forever",
default_value = "0"
)]
ttl: u64,
#[structopt(
long = "db",
help = "Database file path",
default_value = "./pastebin.db"
)]
db_path: String,
#[structopt(long = "tls-certs", help = "Path to certificate chain in PEM format")]
tls_certs: Option<String>,
#[structopt(
long = "tls-key",
help = "Path to private key for tls-certs in PEM format"
)]
tls_key: Option<String>,
}
fn get_url(cfg: &PastebinConfig) -> String {
let port = if vec![443, 80].contains(&cfg.port) {
String::from("")
} else {
format!(":{}", cfg.port)
};
let scheme = if cfg.tls_certs.is_some() {
"https"
} else {
"http"
};
format!(
"{scheme}://{address}{port}",
scheme = scheme,
port = port,
address = cfg.address,
)
}
fn get_error_response(html: String, status: Status, cfg: &PastebinConfig) -> Response {
let map = btreemap! {
"hostname" => cfg.address.as_str(),
"version" => VERSION,
"is_error" => "true",
};
let content = Handlebars::new()
.render_template(html.as_str(), &map)
.unwrap();
Response::build()
.status(status)
.sized_body(Cursor::new(content))
.finalize()
}
#[post("/?<lang>&<ttl>&<burn>&<encrypted>", data = "<paste>")]
fn create(
paste: Data,
state: State<DB>,
cfg: State<PastebinConfig>,
lang: Option<String>,
ttl: Option<u64>,
burn: Option<bool>,
encrypted: Option<bool>,
) -> Result<String, io::Error> {
let id = nanoid!();
let url = format!("{url}/{id}", url = get_url(cfg.inner()), id = id);
let mut writer: Vec<u8> = vec![];
new_entry(
&mut writer,
&mut paste.open(),
lang.unwrap_or_else(|| String::from("markup")),
ttl.unwrap_or(cfg.ttl),
burn.unwrap_or(false),
encrypted.unwrap_or(false),
);
state.put(id, writer).unwrap();
Ok(url)
}
#[delete("/<id>")]
fn remove(id: String, state: State<DB>) -> Result<(), rocksdb::Error> {
state.delete(id)
}
#[get("/<id>?<lang>")]
fn get<'r>(
id: String,
lang: Option<String>,
state: State<'r, DB>,
resources: State<'r, HashMap<&str, &[u8]>>,
cfg: State<PastebinConfig>,
) -> Response<'r> {
let html = String::from_utf8_lossy(resources.get("../static/index.html").unwrap()).to_string();
// handle missing entry
let root = match get_entry_data(&id, &state) {
Ok(x) => x,
Err(e) => {
let err_kind = match e.kind() {
io::ErrorKind::NotFound => Status::NotFound,
_ => Status::InternalServerError,
};
let map = btreemap! {
"hostname" => cfg.address.as_str(),
"version" => VERSION,
"is_error" => "true",
};
let content = Handlebars::new()
.render_template(html.as_str(), &map)
.unwrap();
return Response::build()
.status(err_kind)
.sized_body(Cursor::new(content))
.finalize();
}
};
// handle existing entry
let entry = get_root_as_entry(&root);
let selected_lang = lang
.unwrap_or_else(|| entry.lang().unwrap().to_string())
.to_lowercase();
let mut map = btreemap! {
"is_created" => "true".to_string(),
"pastebin_code" => std::str::from_utf8(entry.data().unwrap()).unwrap().to_string(),
"pastebin_id" => id,
"pastebin_language" => selected_lang,
"hostname" => cfg.address.clone(),
"version" => VERSION.to_string(),
};
if entry.burn() {
map.insert(
"msg",
"FOR YOUR EYES ONLY. The paste is gone, after you close this window.".to_string(),
);
map.insert("level", "warning".to_string());
map.insert("is_burned", "true".to_string());
map.insert("glyph", "fa fa-fire".to_string());
} else if entry.expiry_timestamp() != 0 {
let time = NaiveDateTime::from_timestamp(entry.expiry_timestamp() as i64, 0).format("%Y-%m-%d %H:%M:%S");
map.insert("msg", format!("This paste will expire on {}.", time));
map.insert("level", "info".to_string());
map.insert("glyph", "far fa-clock".to_string());
}
if entry.encrypted() {
map.insert("is_encrypted", "true".to_string());
}
let content = Handlebars::new()
.render_template(html.as_str(), &map)
.unwrap();
Response::build()
.status(Status::Ok)
.header(ContentType::HTML)
.sized_body(Cursor::new(content))
.finalize()
}
#[get("/new?<id>&<level>&<msg>&<glyph>&<url>")]
fn get_new<'r>(
state: State<'r, DB>,
resources: State<'r, HashMap<&str, &[u8]>>,
cfg: State<PastebinConfig>,
id: Option<String>,
level: Option<String>,
glyph: Option<String>,
msg: Option<String>,
url: Option<String>,
) -> Response<'r> {
let html = String::from_utf8_lossy(resources.get("../static/index.html").unwrap()).to_string();
let msg = msg.unwrap_or_else(|| String::from(""));
let level = level.unwrap_or_else(|| String::from("secondary"));
let glyph = glyph.unwrap_or_else(|| String::from(""));
let url = url.unwrap_or_else(|| String::from(""));
let root: Vec<u8>;
let mut map = btreemap! {
"is_editable" => "true",
"hostname" => cfg.address.as_str(),
"version" => VERSION,
"msg" => msg.as_str(),
"level" => level.as_str(),
"glyph" => glyph.as_str(),
"url" => url.as_str(),
};
if let Some(id) = id {
root = get_entry_data(&id, &state).unwrap();
let entry = get_root_as_entry(&root);
if entry.encrypted() {
map.insert("is_encrypted", "true");
}
map.insert(
"pastebin_code",
std::str::from_utf8(entry.data().unwrap()).unwrap(),
);
}
let content = Handlebars::new()
.render_template(html.as_str(), &map)
.unwrap();
Response::build()
.status(Status::Ok)
.header(ContentType::HTML)
.sized_body(Cursor::new(content))
.finalize()
}
#[get("/raw/<id>")]
fn get_raw(id: String, state: State<DB>) -> Response {
// handle missing entry
let root = match get_entry_data(&id, &state) {
Ok(x) => x,
Err(e) => {
let err_kind = match e.kind() {
io::ErrorKind::NotFound => Status::NotFound,
_ => Status::InternalServerError,
};
return Response::build().status(err_kind).finalize();
}
};
let entry = get_root_as_entry(&root);
let mut data: Vec<u8> = vec![];
io::copy(&mut entry.data().unwrap(), &mut data).unwrap();
Response::build()
.status(Status::Ok)
.header(ContentType::Plain)
.sized_body(Cursor::new(data))
.finalize()
}
#[get("/download/<id>")]
fn get_binary(id: String, state: State<DB>) -> Response {
let response = get_raw(id, state);
Response::build_from(response)
.header(ContentType::Binary)
.finalize()
}
#[get("/static/<resource>")]
fn get_static<'r>(
resource: String,
resources: State<'r, HashMap<&str, &[u8]>>,
cfg: State<'r, PastebinConfig>,
) -> Response<'r> {
let pth = format!("../static/{}", resource);
let ext = get_extension(resource.as_str()).replace(".", "");
let content = match resources.get(pth.as_str()) {
Some(data) => data,
None => {
let html =
String::from_utf8_lossy(resources.get("../static/index.html").unwrap()).to_string();
return get_error_response(html, Status::NotFound, cfg.inner());
}
};
let content_type = ContentType::from_extension(ext.as_str()).unwrap();
Response::build()
.status(Status::Ok)
.header(content_type)
.sized_body(Cursor::new(content.iter()))
.finalize()
}
#[get("/")]
fn index() -> Redirect {
Redirect::to("/new")
}
fn rocket(pastebin_config: PastebinConfig) -> rocket::Rocket {
// parse command line opts
let environ: Environment = pastebin_config.environment.parse().unwrap();
let mut rocket_config = Config::build(environ)
.address(pastebin_config.address.clone())
.port(pastebin_config.port)
.workers(pastebin_config.workers)
.keep_alive(pastebin_config.keep_alive)
.log_level(pastebin_config.log)
.finalize()
.unwrap();
// handle tls cert setup
if pastebin_config.tls_certs.is_some() && pastebin_config.tls_key.is_some() {
rocket_config
.set_tls(
pastebin_config.tls_certs.clone().unwrap().as_str(),
pastebin_config.tls_key.clone().unwrap().as_str(),
)
.unwrap();
}
// setup db
let db = DB::open_default(pastebin_config.db_path.clone()).unwrap();
let mut db_opts = Options::default();
db_opts.create_if_missing(true);
db_opts.set_compaction_filter("ttl_entries", compaction_filter_expired_entries);
let resources = load_static_resources!(
"../static/index.html",
"../static/custom.js",
"../static/custom.css",
"../static/prism.js",
"../static/prism.css",
"../static/favicon.ico"
);
// run rocket
rocket::custom(rocket_config)
.manage(pastebin_config)
.manage(db)
.manage(resources)
.mount(
"/",
routes![index, create, remove, get, get_new, get_raw, get_binary, get_static],
)
}
fn main() {
let pastebin_config = PastebinConfig::from_args();
rocket(pastebin_config).launch();
}

28
static/custom.css Normal file
View file

@ -0,0 +1,28 @@
html {
position: relative;
min-height: 100%;
}
body {
min-height: 75rem; /* Minimum navbar */
padding-top: 3.5rem; /* Margin top by nav bar */
padding-bottom: 3.5rem; /* Margin bottom by footer height */
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px; /* Set the fixed height of the footer here */
line-height: 60px; /* Vertically center the text there */
background-color: #f5f5f5;
}
.form-group textarea {
margin: 0.5em 0px;
}
pre[class*="language-"]:before,
pre[class*="language-"]:after {
display: none;
}

156
static/custom.js Normal file
View file

@ -0,0 +1,156 @@
$(document).ready(function() {
function replaceUrlParam(url, param, value) {
if (value == null) {
value = '';
}
var pattern = new RegExp('\\b('+param+'=).*?(&|#|$)');
if (url.search(pattern)>=0) {
return url.replace(pattern,'$1' + value + '$2');
}
url = url.replace(/[?#]$/,'');
return url + (url.indexOf('?')>0 ? '&' : '?') + param + '=' + value;
}
function resetLanguageSelector() {
var url = new URL(document.location);
var params = url.searchParams;
var lang = params.get("lang");
if (lang != null) {
$("#language-selector").val(lang);
} else {
if($("#pastebin-code-block").length) {
$("#language-selector").val(
$("#pastebin-code-block").prop("class").trim().split('-')[1]
);
}
}
}
function checkPasswordModal() {
if ($("#password-modal").length) {
$('#password-modal').modal('toggle');
}
}
var state = {
expiry: 0,
burn: 0,
};
resetLanguageSelector();
checkPasswordModal();
$("#language-selector").change(function() {
if ($("#pastebin-code-block").length) {
$('#pastebin-code-block').attr('class', 'language-' + $("#language-selector").val());
Prism.highlightElement($('#pastebin-code-block')[0]);
}
});
$("#remove-btn").on("click", function(event) {
event.preventDefault();
$.ajax({
url: window.location.pathname,
type: 'DELETE',
success: function(result) {
uri = "/new";
uri = replaceUrlParam(uri, 'level', "info");
uri = replaceUrlParam(uri, 'glyph', "fas fa-info-circle");
uri = replaceUrlParam(uri, 'msg', "The paste has been successfully removed.");
window.location.href = encodeURI(uri);
}
});
});
$("#copy-btn").on("click", function(event) {
event.preventDefault();
$(".toolbar-item button").get(0).click();
var $this = $(this);
$this.text("Copied!");
$this.attr("disabled", "disabled");
setTimeout(function() {
$this.text("Copy");
$this.removeAttr("disabled");
}, 800);
});
$("#send-btn").on("click", function(event) {
event.preventDefault();
uri = "/";
uri = replaceUrlParam(uri, 'lang', $("#language-selector").val());
uri = replaceUrlParam(uri, 'ttl', state.expiry);
uri = replaceUrlParam(uri, 'burn', state.burn);
var data = $("#content-textarea").val();
var pass = $("#pastebin-password").val();
if ($("#pastebin-password").val().length > 0) {
data = CryptoJS.AES.encrypt(data, pass).toString();
uri = replaceUrlParam(uri, 'encrypted', true);
}
$.ajax({
url: uri,
type: 'POST',
data: data,
success: function(result) {
uri = "/new";
uri = replaceUrlParam(uri, 'level', "success");
uri = replaceUrlParam(uri, 'glyph', "fas fa-check");
uri = replaceUrlParam(uri, 'msg', "The paste has been successfully created:");
uri = replaceUrlParam(uri, 'url', result);
window.location.href = encodeURI(uri);
}
});
});
$('#expiry-dropdown a').click(function(event){
event.preventDefault();
state.expiry = $(this).attr("href");
$('#expiry-dropdown-btn').text("Expires: " + this.innerHTML);
});
$('#burn-dropdown a').click(function(event){
event.preventDefault();
state.burn = $(this).attr("href");
$('#burn-dropdown-btn').text("Burn: " + this.innerHTML);
});
$('#decrypt-btn').click(function(event) {
var pass = $("#modal-password").val();
var data = "";
if ($("#pastebin-code-block").length) {
data = $("#pastebin-code-block").text();
} else {
data = $("#content-textarea").text();
}
var decrypted = CryptoJS.AES.decrypt(data, pass).toString(CryptoJS.enc.Utf8);
if (decrypted.length == 0) {
$("#modal-alert").removeClass("collapse");
} else {
if ($("#pastebin-code-block").length) {
$("#pastebin-code-block").text(decrypted);
Prism.highlightElement($('#pastebin-code-block')[0]);
} else {
$("#content-textarea").text(decrypted);
}
$("#modal-close-btn").click();
$("#modal-alert").alert('close');
}
});
});

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

319
static/index.html Normal file
View file

@ -0,0 +1,319 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Simple, fast and standalone pastebin service">
<meta name="author" content="Mateusz Kaczanowski">
<title>Pastebin</title>
<link rel="icon" href="/static/favicon.ico">
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<link href="/static/prism.css" rel="stylesheet" />
<link href="/static/custom.css" rel="stylesheet" />
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<a class="navbar-brand" href="/">PASTEBIN</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="/new">New <span class="sr-only">(current)</span></a>
</li>
{{#if is_created}}
{{#if (not is_burned)}}
<li class="nav-item">
<a class="nav-link" href="#" id="remove-btn">Remove</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/new?id={{pastebin_id}}" id="clone-btn">Clone</a>
</li>
{{/if}}
{{/if}}
{{#if is_editable}}
<li class="nav-item dropdown">
<a id="expiry-dropdown-btn" class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Expires: Never</a>
<div class="dropdown-menu" id="expiry-dropdown">
<a class="dropdown-item" href="300">5 minutes</a>
<a class="dropdown-item" href="600">10 minutes</a>
<a class="dropdown-item" href="3600">1 hour</a>
<a class="dropdown-item" href="86400">1 day</a>
<a class="dropdown-item" href="604800">1 week</a>
<a class="dropdown-item" href="2419200">1 month</a>
<a class="dropdown-item" href="29030400">1 year</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="0">Never</a>
</div>
</li>
<li class="nav-item dropdown">
<a id="burn-dropdown-btn" class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Burn: No</a>
<div class="dropdown-menu" id="burn-dropdown">
<a class="dropdown-item" href="true">Yes</a>
<a class="dropdown-item" href="false">No</a>
</div>
</li>
{{/if}}
</ul>
<div class="form-inline mt-2 mt-md-0">
{{#if is_editable}}
<input class="form-control mr-sm-2" id="pastebin-password" type="password" placeholder="Password (optional)" aria-label="Password (optional)">
{{/if}}
<select class="form-control mr-sm-2" id="language-selector">
<option value="markup">Markup</option>
<option value="css">CSS</option>
<option value="clike">C-like</option>
<option value="javascript">JavaScript</option>
<option disabled>──────────</option>
<option value="abap">ABAP</option>
<option value="actionscript">ActionScript</option>
<option value="ada">Ada</option>
<option value="apacheconf">Apache Configuration</option>
<option value="apl">APL</option>
<option value="applescript">AppleScript</option>
<option value="arduino">Arduino</option>
<option value="arff">ARFF</option>
<option value="asciidoc">AsciiDoc</option>
<option value="asm6502">6502 Assembly</option>
<option value="aspnet">ASP.NET (C#)</option>
<option value="autohotkey">AutoHotkey</option>
<option value="autoit">AutoIt</option>
<option value="bash">Bash</option>
<option value="basic">BASIC</option>
<option value="batch">Batch</option>
<option value="bison">Bison</option>
<option value="brainfuck">Brainfuck</option>
<option value="bro">Bro</option>
<option value="c">C</option>
<option value="csharp">C#</option>
<option value="cpp">C++</option>
<option value="coffeescript">CoffeeScript</option>
<option value="clojure">Clojure</option>
<option value="crystal">Crystal</option>
<option value="csp">Content-Security-Policy</option>
<option value="css-extras">CSS Extras</option>
<option value="d">D</option>
<option value="dart">Dart</option>
<option value="diff">Diff</option>
<option value="django">Django/Jinja2</option>
<option value="docker">Docker</option>
<option value="eiffel">Eiffel</option>
<option value="elixir">Elixir</option>
<option value="elm">Elm</option>
<option value="erb">ERB</option>
<option value="erlang">Erlang</option>
<option value="fsharp">F#</option>
<option value="flow">Flow</option>
<option value="fortran">Fortran</option>
<option value="gedcom">GEDCOM</option>
<option value="gherkin">Gherkin</option>
<option value="git">Git</option>
<option value="glsl">GLSL</option>
<option value="gml">GameMaker</option>
<option value="go">Go</option>
<option value="graphql">GraphQL</option>
<option value="groovy">Groovy</option>
<option value="haml">Haml</option>
<option value="handlebars">Handlebars</option>
<option value="haskell">Haskell</option>
<option value="haxe">Haxe</option>
<option value="http">HTTP</option>
<option value="hpkp">HTTP Public-Key-Pins</option>
<option value="hsts">HTTP STS</option>
<option value="ichigojam">IchigoJam</option>
<option value="icon">Icon</option>
<option value="inform7">Inform 7</option>
<option value="ini">Ini</option>
<option value="io">Io</option>
<option value="j">J</option>
<option value="java">Java</option>
<option value="jolie">Jolie</option>
<option value="json">JSON</option>
<option value="julia">Julia</option>
<option value="keyman">Keyman</option>
<option value="kotlin">Kotlin</option>
<option value="latex">LaTeX</option>
<option value="less">Less</option>
<option value="liquid">Liquid</option>
<option value="lisp">Lisp</option>
<option value="livescript">LiveScript</option>
<option value="lolcode">LOLCODE</option>
<option value="lua">Lua</option>
<option value="makefile">Makefile</option>
<option value="markdown">Markdown</option>
<option value="markup-templating">Markup templating</option>
<option value="matlab">MATLAB</option>
<option value="mel">MEL</option>
<option value="mizar">Mizar</option>
<option value="monkey">Monkey</option>
<option value="n4js">N4JS</option>
<option value="nasm">NASM</option>
<option value="nginx">nginx</option>
<option value="nim">Nim</option>
<option value="nix">Nix</option>
<option value="nsis">NSIS</option>
<option value="objectivec">Objective-C</option>
<option value="ocaml">OCaml</option>
<option value="opencl">OpenCL</option>
<option value="oz">Oz</option>
<option value="parigp">PARI/GP</option>
<option value="parser">Parser</option>
<option value="pascal">Pascal</option>
<option value="perl">Perl</option>
<option value="php">PHP</option>
<option value="php-extras">PHP Extras</option>
<option value="plsql">PL/SQL</option>
<option value="powershell">PowerShell</option>
<option value="processing">Processing</option>
<option value="prolog">Prolog</option>
<option value="properties">.properties</option>
<option value="protobuf">Protocol Buffers</option>
<option value="pug">Pug</option>
<option value="puppet">Puppet</option>
<option value="pure">Pure</option>
<option value="python">Python</option>
<option value="q">Q (kdb+ database)</option>
<option value="qore">Qore</option>
<option value="r">R</option>
<option value="jsx">React JSX</option>
<option value="tsx">React TSX</option>
<option value="renpy">Ren'py</option>
<option value="reason">Reason</option>
<option value="rest">reST (reStructuredText)</option>
<option value="rip">Rip</option>
<option value="roboconf">Roboconf</option>
<option value="ruby">Ruby</option>
<option value="rust">Rust</option>
<option value="sas">SAS</option>
<option value="sass">Sass (Sass)</option>
<option value="scss">Sass (Scss)</option>
<option value="scala">Scala</option>
<option value="scheme">Scheme</option>
<option value="smalltalk">Smalltalk</option>
<option value="smarty">Smarty</option>
<option value="sql">SQL</option>
<option value="soy">Soy (Closure Template)</option>
<option value="stylus">Stylus</option>
<option value="swift">Swift</option>
<option value="tap">TAP</option>
<option value="tcl">Tcl</option>
<option value="textile">Textile</option>
<option value="tt2">Template Toolkit 2</option>
<option value="twig">Twig</option>
<option value="typescript">TypeScript</option>
<option value="vbnet">VB.Net</option>
<option value="velocity">Velocity</option>
<option value="verilog">Verilog</option>
<option value="vhdl">VHDL</option>
<option value="vim">vim</option>
<option value="visual-basic">Visual Basic</option>
<option value="wasm">WebAssembly</option>
<option value="wiki">Wiki markup</option>
<option value="xeora">Xeora</option>
<option value="xojo">Xojo (REALbasic)</option>
<option value="xquery">XQuery</option>
<option value="yaml">YAML</option>
</select>
{{#if is_created}}
<a id="copy-btn" class="btn btn-outline-success d-none d-lg-inline-block mb-3 mb-md-0" href="#">Copy</a>
{{#if (not is_burned)}}
<a class="btn btn-outline-success d-none d-lg-inline-block mb-3 mb-md-0 ml-md-2" href="/raw/{{pastebin_id}}">Raw</a>
<a class="btn btn-outline-success d-none d-lg-inline-block mb-3 mb-md-0 ml-md-2" href="/download/{{pastebin_id}}">Download</a>
{{/if}}
{{/if}}
{{#if is_editable}}
<a id="send-btn" class="btn btn-outline-success d-none d-lg-inline-block mb-3 mb-md-0" href="#">Send</a>
{{/if}}
</div>
</div>
</nav>
<main role="main" class="container-fluid">
{{# if is_error}}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12 text-center">
<span class="display-1 d-block">404</span>
<div class="mb-4 lead">The page you are looking for was not found.</div>
<a href="/" class="btn btn-link">Back to Home</a>
</div>
</div>
</div>
{{ else }}
{{#if msg}}
<div class="mt-3 alert alert-{{level}} alert-dismissible fade show" role="alert">
{{#if glyph}}<i class="{{glyph}}"></i>{{/if}}
{{#if url}}{{msg}} <a href="{{url}}">{{url}}</a>{{else}}{{msg}}{{/if}}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{{/if}}
{{# if is_editable}}
<div class="form-group">
<textarea class="form-control" id="content-textarea" rows="25">{{pastebin_code}}</textarea>
</div>
{{/if}}
{{#if is_created or is_clone}}
<pre><code id="pastebin-code-block" class="language-{{pastebin_language}}">{{pastebin_code}}</code></pre>
{{/if}}
{{/if}}
</main>
<footer class="footer">
<span class="text-muted ml-4">{{hostname}} - pastebin v{{version}}</span>
</footer>
{{#if is_encrypted}}
<div class="modal fade" id="password-modal" tabindex="-1" role="dialog" aria-labelledby="password-modal-label" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="password-modal-label">
<i class="fas fa-key"></i>
Please enter the password for this paste
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="alert alert-danger collapse" role="alert" id="modal-alert">
<i class="fas fa-exclamation-triangle"></i>
Could not decrypt data. Wrong password?
</div>
<div class="form-group">
<input type="password" class="form-control" id="modal-password">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="modal-close-btn">Close</button>
<button type="button" class="btn btn-primary" id="decrypt-btn">Decrypt</button>
</div>
</div>
</div>
</div>
{{/if}}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.4/clipboard.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-notify/0.2.0/js/bootstrap-notify.min.js"></script>
<script src="/static/prism.js"></script>
<script src="/static/custom.js"></script>
</body>
</html>

358
static/prism.css Normal file
View file

@ -0,0 +1,358 @@
/* PrismJS 1.20.0
https://prismjs.com/download.html#themes=prism-coy&languages=markup+css+clike+javascript+abap+abnf+actionscript+ada+al+antlr4+apacheconf+apl+applescript+aql+arduino+arff+asciidoc+asm6502+aspnet+autohotkey+autoit+bash+basic+batch+bbcode+bison+bnf+brainfuck+brightscript+bro+c+concurnas+csharp+cpp+cil+coffeescript+cmake+clojure+crystal+csp+css-extras+d+dart+dax+diff+django+dns-zone-file+docker+ebnf+eiffel+ejs+elixir+elm+etlua+erb+erlang+excel-formula+fsharp+factor+firestore-security-rules+flow+fortran+ftl+gcode+gdscript+gedcom+gherkin+git+glsl+gml+go+graphql+groovy+haml+handlebars+haskell+haxe+hcl+http+hpkp+hsts+ichigojam+icon+iecst+inform7+ini+io+j+java+javadoc+javadoclike+javastacktrace+jolie+jq+jsdoc+js-extras+js-templates+json+jsonp+json5+julia+keyman+kotlin+latex+latte+less+lilypond+liquid+lisp+livescript+llvm+lolcode+lua+makefile+markdown+markup-templating+matlab+mel+mizar+monkey+moonscript+n1ql+n4js+nand2tetris-hdl+nasm+neon+nginx+nim+nix+nsis+objectivec+ocaml+opencl+oz+parigp+parser+pascal+pascaligo+pcaxis+peoplecode+perl+php+phpdoc+php-extras+plsql+powerquery+powershell+processing+prolog+properties+protobuf+pug+puppet+pure+python+q+qml+qore+r+racket+jsx+tsx+renpy+reason+regex+rest+rip+roboconf+robotframework+ruby+rust+sas+sass+scss+scala+scheme+shell-session+smalltalk+smarty+solidity+solution-file+soy+sparql+splunk-spl+sqf+sql+stylus+swift+tap+tcl+textile+toml+tt2+turtle+twig+typescript+t4-cs+t4-vb+t4-templating+unrealscript+vala+vbnet+velocity+verilog+vhdl+vim+visual-basic+warpscript+wasm+wiki+xeora+xojo+xquery+yaml+zig&plugins=line-highlight+remove-initial-line-feed+toolbar+copy-to-clipboard+diff-highlight */
/**
* prism.js Coy theme for JavaScript, CoffeeScript, CSS and HTML
* Based on https://github.com/tshedor/workshop-wp-theme (Example: http://workshop.kansan.com/category/sessions/basics or http://workshop.timshedor.com/category/sessions/basics);
* @author Tim Shedor
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
position: relative;
margin: .5em 0;
overflow: visible;
padding: 0;
}
pre[class*="language-"]>code {
position: relative;
border-left: 10px solid #358ccb;
box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf;
background-color: #fdfdfd;
background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%);
background-size: 3em 3em;
background-origin: content-box;
background-attachment: local;
}
code[class*="language"] {
max-height: inherit;
height: inherit;
padding: 0 1em;
display: block;
overflow: auto;
}
/* Margin bottom to accommodate shadow */
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background-color: #fdfdfd;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
margin-bottom: 1em;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
position: relative;
padding: .2em;
border-radius: 0.3em;
color: #c92c2c;
border: 1px solid rgba(0, 0, 0, 0.1);
display: inline;
white-space: normal;
}
pre[class*="language-"]:before,
pre[class*="language-"]:after {
content: '';
z-index: -2;
display: block;
position: absolute;
bottom: 0.75em;
left: 0.18em;
width: 40%;
height: 20%;
max-height: 13em;
box-shadow: 0px 13px 8px #979797;
-webkit-transform: rotate(-2deg);
-moz-transform: rotate(-2deg);
-ms-transform: rotate(-2deg);
-o-transform: rotate(-2deg);
transform: rotate(-2deg);
}
:not(pre) > code[class*="language-"]:after,
pre[class*="language-"]:after {
right: 0.75em;
left: auto;
-webkit-transform: rotate(2deg);
-moz-transform: rotate(2deg);
-ms-transform: rotate(2deg);
-o-transform: rotate(2deg);
transform: rotate(2deg);
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #7D8B99;
}
.token.punctuation {
color: #5F6364;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.function-name,
.token.constant,
.token.symbol,
.token.deleted {
color: #c92c2c;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.function,
.token.builtin,
.token.inserted {
color: #2f9c0a;
}
.token.operator,
.token.entity,
.token.url,
.token.variable {
color: #a67f59;
background: rgba(255, 255, 255, 0.5);
}
.token.atrule,
.token.attr-value,
.token.keyword,
.token.class-name {
color: #1990b8;
}
.token.regex,
.token.important {
color: #e90;
}
.language-css .token.string,
.style .token.string {
color: #a67f59;
background: rgba(255, 255, 255, 0.5);
}
.token.important {
font-weight: normal;
}
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.token.namespace {
opacity: .7;
}
@media screen and (max-width: 767px) {
pre[class*="language-"]:before,
pre[class*="language-"]:after {
bottom: 14px;
box-shadow: none;
}
}
/* Plugin styles */
.token.tab:not(:empty):before,
.token.cr:before,
.token.lf:before {
color: #e0d7d1;
}
/* Plugin styles: Line Numbers */
pre[class*="language-"].line-numbers.line-numbers {
padding-left: 0;
}
pre[class*="language-"].line-numbers.line-numbers code {
padding-left: 3.8em;
}
pre[class*="language-"].line-numbers.line-numbers .line-numbers-rows {
left: 0;
}
/* Plugin styles: Line Highlight */
pre[class*="language-"][data-line] {
padding-top: 0;
padding-bottom: 0;
padding-left: 0;
}
pre[data-line] code {
position: relative;
padding-left: 4em;
}
pre .line-highlight {
margin-top: 0;
}
pre[data-line] {
position: relative;
padding: 1em 0 1em 3em;
}
.line-highlight {
position: absolute;
left: 0;
right: 0;
padding: inherit 0;
margin-top: 1em; /* Same as .prisms padding-top */
background: hsla(24, 20%, 50%,.08);
background: linear-gradient(to right, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
pointer-events: none;
line-height: inherit;
white-space: pre;
}
.line-highlight:before,
.line-highlight[data-end]:after {
content: attr(data-start);
position: absolute;
top: .4em;
left: .6em;
min-width: 1em;
padding: 0 .5em;
background-color: hsla(24, 20%, 50%,.4);
color: hsl(24, 20%, 95%);
font: bold 65%/1.5 sans-serif;
text-align: center;
vertical-align: .3em;
border-radius: 999px;
text-shadow: none;
box-shadow: 0 1px white;
}
.line-highlight[data-end]:after {
content: attr(data-end);
top: auto;
bottom: .4em;
}
.line-numbers .line-highlight:before,
.line-numbers .line-highlight:after {
content: none;
}
div.code-toolbar {
position: relative;
}
div.code-toolbar > .toolbar {
position: absolute;
top: .3em;
right: .2em;
transition: opacity 0.3s ease-in-out;
opacity: 0;
}
div.code-toolbar:hover > .toolbar {
opacity: 1;
}
/* Separate line b/c rules are thrown out if selector is invalid.
IE11 and old Edge versions don't support :focus-within. */
div.code-toolbar:focus-within > .toolbar {
opacity: 1;
}
div.code-toolbar > .toolbar .toolbar-item {
display: inline-block;
}
div.code-toolbar > .toolbar a {
cursor: pointer;
}
div.code-toolbar > .toolbar button {
background: none;
border: 0;
color: inherit;
font: inherit;
line-height: normal;
overflow: visible;
padding: 0;
-webkit-user-select: none; /* for button */
-moz-user-select: none;
-ms-user-select: none;
}
div.code-toolbar > .toolbar a,
div.code-toolbar > .toolbar button,
div.code-toolbar > .toolbar span {
color: #bbb;
font-size: .8em;
padding: 0 .5em;
background: #f5f2f0;
background: rgba(224, 224, 224, 0.2);
box-shadow: 0 2px 0 0 rgba(0,0,0,0.2);
border-radius: .5em;
}
div.code-toolbar > .toolbar a:hover,
div.code-toolbar > .toolbar a:focus,
div.code-toolbar > .toolbar button:hover,
div.code-toolbar > .toolbar button:focus,
div.code-toolbar > .toolbar span:hover,
div.code-toolbar > .toolbar span:focus {
color: inherit;
text-decoration: none;
}
pre.diff-highlight > code .token.deleted:not(.prefix),
pre > code.diff-highlight .token.deleted:not(.prefix) {
background-color: rgba(255, 0, 0, .1);
color: inherit;
display: block;
}
pre.diff-highlight > code .token.inserted:not(.prefix),
pre > code.diff-highlight .token.inserted:not(.prefix) {
background-color: rgba(0, 255, 128, .1);
color: inherit;
display: block;
}

223
static/prism.js Normal file

File diff suppressed because one or more lines are too long