├── .dockerignore ├── .github └── workflows │ ├── docker-image-armv7.yml │ ├── docker-image-armv8.yml │ └── rust.yml ├── .gitignore ├── .travis.yml ├── API.md ├── Cargo.toml ├── Dockerfile ├── Dockerfile.armv7 ├── Dockerfile.armv8 ├── LICENSE ├── README.md ├── api.fbs ├── docker-compose.yml ├── src ├── api_generated.rs ├── formatter.rs ├── lib.rs ├── main.rs ├── plugins.rs └── plugins │ ├── mermaid.rs │ ├── plugin.rs │ └── prism.rs └── static ├── custom.css ├── custom.js ├── favicon.ico ├── index.html ├── prism.css └── prism.js /.dockerignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target 3 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-armv7.yml: -------------------------------------------------------------------------------- 1 | name: Docker image armv7 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v1 17 | 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v1 20 | 21 | - name: Login to DockerHub 22 | uses: docker/login-action@v1 23 | with: 24 | username: ${{ secrets.DOCKER_USER }} 25 | password: ${{ secrets.DOCKER_PASSWORD }} 26 | 27 | - name: Build and push 28 | id: docker_build 29 | uses: docker/build-push-action@v2 30 | with: 31 | push: true 32 | tags: mkaczanowski/pastebin:armv7 33 | file: Dockerfile.armv7 34 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-armv8.yml: -------------------------------------------------------------------------------- 1 | name: Docker image armv8 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v1 17 | 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v1 20 | 21 | - name: Login to DockerHub 22 | uses: docker/login-action@v1 23 | with: 24 | username: ${{ secrets.DOCKER_USER }} 25 | password: ${{ secrets.DOCKER_PASSWORD }} 26 | 27 | - name: Build and push 28 | id: docker_build 29 | uses: docker/build-push-action@v2 30 | with: 31 | push: true 32 | tags: mkaczanowski/pastebin:armv8 33 | file: Dockerfile.armv8 34 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Test and Build 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [ubuntu-latest] 9 | rust: [nightly] 10 | 11 | steps: 12 | - uses: hecrj/setup-rust-action@v1 13 | with: 14 | rust-version: ${{ matrix.rust }} 15 | - uses: actions/checkout@master 16 | - name: Build 17 | run: cargo build --verbose 18 | - name: Run tests 19 | run: cargo test --verbose 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | pastebin.db 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | script: 5 | - cargo build --verbose --all 6 | - cargo test --verbose --all 7 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ## REST API 2 | ### GET /:id 3 | Returns the contents of selected paste (assuming paste exists, otherwise returns 404) 4 | 5 | | Name | Arg | Type | Description | 6 | | ------------- | :---: | :------: | :----------------------------------------: | 7 | | id | path | string | Unique identifier of the paste | 8 | | lang | query | string | Language (used by the UI, i.e. "markdown") | 9 | 10 | ### GET /raw/:id 11 | Returns the contents of the selected paste with HTTP `text/plain` header 12 | 13 | | Name | Arg | Type | Description | 14 | | ------------- | :---: | :------: | :----------------------------------------: | 15 | | id | path | string | Unique identifier of the paste | 16 | 17 | ### GET /download/:id 18 | Returns the contents of selected paste with HTTP `application/octet-stream` header 19 | 20 | | Name | Arg | Type | Description | 21 | | ------------- | :---: | :------: | :----------------------------------------: | 22 | | id | path | string | Unique identifier of the paste | 23 | 24 | ### GET /static/:resource 25 | Returns static resources, such as javascript or css files compiled in the binary 26 | 27 | | Name | Arg | Type | Description | 28 | | ------------- | :---: | :------: | :----------------------------------------: | 29 | | resource | path | string | Resource name (ie. `custom.js`) | 30 | 31 | ### POST / 32 | Creates new paste, where input data is expected to be of type: 33 | * `application/x-www-form` 34 | * `application/octet-stream` 35 | 36 | | Name | Arg | Type | Description | 37 | | ------------- | :---: | :------: | :----------------------------------------: | 38 | | lang | query | string | Language (used by the UI, i.e. "markdown") | 39 | | ttl | query | int | Expiration time in seconds | 40 | | burn | query | boolean | Whether to delete the paste after reading | 41 | | encrypted | query | boolean | Used by UI to display "decrypt" modal box | 42 | 43 | ### DELETE /:id 44 | Deletes the selected paste from the local database 45 | 46 | | Name | Arg | Type | Description | 47 | | ------------- | :---: | :------: | :----------------------------------------: | 48 | | id | path | string | Unique identifier of the paste | 49 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pastebin" 3 | version = "0.1.3" 4 | authors = ["Kaczanowski Mateusz "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | rocket = { version = "0.4.4", features = ["tls"] } 9 | regex = "1" 10 | humantime = "2.0.1" 11 | rocksdb = "0.13.0" 12 | nanoid = "0.3.0" 13 | flatbuffers = "0.6.1" 14 | structopt = "0.3.17" 15 | structopt-derive = "0.4.10" 16 | num_cpus = "1.0" 17 | handlebars = "3.5.3" 18 | tempdir = "0.3.7" 19 | speculate = "0.1.2" 20 | chrono = "0.4.11" 21 | serde_json = "1.0.57" 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustlang/rust:nightly as builder 2 | 3 | RUN apt-get update && apt-get install -y apt-utils software-properties-common lsb-release 4 | RUN bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" 5 | 6 | WORKDIR /usr/src/pastebin 7 | COPY . . 8 | 9 | RUN cargo install --path . 10 | 11 | FROM debian:buster-slim 12 | COPY --from=builder /usr/local/cargo/bin/pastebin /usr/local/bin/pastebin 13 | 14 | ENTRYPOINT ["pastebin"] 15 | CMD ["--help"] 16 | -------------------------------------------------------------------------------- /Dockerfile.armv7: -------------------------------------------------------------------------------- 1 | FROM mkaczanowski/archlinuxarm:armv7 as builder 2 | 3 | RUN pacman -Sy --noconfirm rustup gcc llvm clang glibc 4 | RUN rustup default nightly 5 | 6 | WORKDIR /usr/src/pastebin 7 | COPY . . 8 | 9 | RUN cargo install --path . --root /tmp/pastebin 10 | 11 | FROM mkaczanowski/archlinuxarm:armv7 12 | RUN pacman -Sy --noconfirm glibc 13 | COPY --from=builder /tmp/pastebin/bin/pastebin /usr/local/bin/pastebin 14 | 15 | ENTRYPOINT ["pastebin"] 16 | CMD ["--help"] 17 | -------------------------------------------------------------------------------- /Dockerfile.armv8: -------------------------------------------------------------------------------- 1 | FROM mkaczanowski/archlinuxarm:armv8 as builder 2 | 3 | RUN pacman -Sy --noconfirm rustup gcc llvm clang glibc 4 | RUN rustup default nightly 5 | 6 | WORKDIR /usr/src/pastebin 7 | COPY . . 8 | 9 | RUN cargo install --path . --root /tmp/pastebin 10 | 11 | FROM mkaczanowski/archlinuxarm:armv8 12 | RUN pacman -Sy --noconfirm glibc 13 | COPY --from=builder /tmp/pastebin/bin/pastebin /usr/local/bin/pastebin 14 | 15 | ENTRYPOINT ["pastebin"] 16 | CMD ["--help"] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mateusz Kaczanowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status][travis-badge]][travis] 2 | [![Test and Build][github-workflow]][github-workflow] 3 | [![Docker Cloud Build Status][docker-cloud-build-status]][docker-hub] 4 | [![Docker Pulls][docker-pulls]][docker-hub] 5 | [![Docker Image Size][docker-size]][docker-hub] 6 | 7 | [travis-badge]: https://travis-ci.org/mkaczanowski/pastebin.svg?branch=master 8 | [travis]: https://travis-ci.org/mkaczanowski/pastebin/ 9 | [docker-hub]: https://hub.docker.com/r/mkaczanowski/pastebin 10 | [docker-cloud-build-status]: https://img.shields.io/docker/cloud/build/mkaczanowski/pastebin 11 | [docker-pulls]: https://img.shields.io/docker/pulls/mkaczanowski/pastebin 12 | [docker-size]: https://img.shields.io/docker/image-size/mkaczanowski/pastebin/latest 13 | [github-workflow]: https://github.com/mkaczanowski/pastebin/workflows/Test%20and%20Build/badge.svg 14 | 15 | # Pastebin 16 | Simple, fast, standalone pastebin service. 17 | 18 | ## Why? 19 | Whenever you need to share a code snippet, diff, logs, or a secret with another human being, the Pastebin service is invaluable. However, using public services such as [pastebin.com](https://pastebin.com), [privnote.com](https://privnote.com), etc. should be avoided when you're sharing data that should be available only for a selected audience (i.e., your company, private network). Instead of trusting external providers, you could host your own Pastebin service and take ownership of all your data! 20 | 21 | **There are numerous [Pastebin implementations](https://github.com/awesome-selfhosted/awesome-selfhosted#pastebins) out there, why would you implement another one?** 22 | 23 | While the other implementations are great, I couldn't find one that would satisfy my requirements: 24 | * no dependencies - one binary is all I want, no python libs, ruby runtime magic, no javascript or external databases to setup 25 | * storage - fast, lightweight, self-hosted key-value storage able to hold a lot of data. 26 | * speed - it must be fast. Once deployed in a mid-sized company you can expect high(er) traffic with low latency expectations from users 27 | * reliability - no one wants to fix things that should just work (and are that simple!) 28 | * cheap - low-cost service that would not steal too much CPU time, thus adding up to your bill 29 | * CLI + GUI - it must be easy to interface from both ends (but still, no deps!) 30 | * other features: 31 | * on-demand encryption 32 | * syntax highlighting 33 | * destroy after reading 34 | * destroy after expiration date 35 | 36 | This Pastebin implementation satisfies all of the above requirements! 37 | 38 | ## Implementation 39 | This is a rust version of Pastebin service with [rocksdb](https://rocksdb.org/) database as storage. In addition to previously mentioned features it's worth to mention: 40 | * all-in-one binary - all the data, including css/javascript files are compiled into the binary. This way you don't need to worry about external dependencies, it's all within. (see: [std::include_bytes](https://doc.rust-lang.org/std/macro.include_bytes.html)) 41 | * [REST endpoint](https://rocket.rs/) - you can add/delete pastes via standard HTTP client (ie. curl) 42 | * [RocksDB compaction filter](https://github.com/facebook/rocksdb/wiki/Compaction-Filter) - the expired pastes will be automatically removed by custom compaction filter 43 | * [flatbuffers](https://google.github.io/flatbuffers/) - data is serialized with flatbuffers (access to serialized data without parsing/unpacking) 44 | * GUI - the UI is a plain HTML with [Bootstrap JS](https://getbootstrap.com/), [jQuery](https://jquery.com/) and [prism.js](https://prismjs.com/) 45 | * Encryption - password-protected pastes are AES encrypted/decprypted in the browser via [CryptoJS](https://code.google.com/archive/p/crypto-js/) 46 | 47 | ### Plugins 48 | The default configuration enables only one plugin, this is syntax highlighting through `prism.js`. This should be enough for p90 of the users but if you need extra features you might want to use the plugin system (`src/plugins`). 49 | 50 | To enable additional plugins, pass: 51 | ``` 52 | --plugins prism 53 | ``` 54 | 55 | Currently supported: 56 | * [prism.js](https://prismjs.com/) 57 | * [mermaid.js](https://github.com/mermaid-js/mermaid) 58 | 59 | 60 | ## Usage 61 | Pastebin builds only with `rust-nightly` version and requires `llvm` compiler (rocksdb deps). To skip the build process, you can use the docker image. 62 | 63 | ### Cargo 64 | ``` 65 | cargo build --release 66 | cargo run 67 | ``` 68 | ### Docker 69 | x86 image: 70 | ``` 71 | docker pull mkaczanowski/pastebin:latest 72 | docker run --init --network host mkaczanowski/pastebin --address localhost --port 8000 73 | ``` 74 | 75 | ARM images: 76 | ``` 77 | docker pull mkaczanowski/pastebin:armv7 78 | docker pull mkaczanowski/pastebin:armv8 79 | ``` 80 | 81 | Compose setup: 82 | ``` 83 | URI="http://localhost" docker-compose up 84 | curl -L "http://localhost" 85 | ``` 86 | ### Client 87 | ``` 88 | alias pastebin="curl -w '\n' -q -L --data-binary @- -o - http://localhost:8000/" 89 | 90 | echo "hello World" | pastebin 91 | http://localhost:8000/T9kGrI5aNkI4Z-PelmQ5U 92 | ``` 93 | 94 | ## Nginx (optional) 95 | The Pastebin service serves `/static` files from memory. To lower down the load on the service you might want to consider setting up nginx with caching and compression enabled, as shown here: 96 | ``` 97 | map $sent_http_content_type $expires { 98 | default off; 99 | text/css 30d; 100 | application/javascript 30d; 101 | image/x-icon 30d; 102 | } 103 | 104 | server { 105 | listen 80; 106 | server_name paste.domain.com; 107 | 108 | gzip on; 109 | gzip_types text/plain application/xml text/css application/javascript; 110 | 111 | expires $expires; 112 | location / { 113 | proxy_pass http://localhost:8000; 114 | include proxy-settings.conf; 115 | } 116 | 117 | access_log /var/log/nginx/access.log; 118 | } 119 | ``` 120 | 121 | ## REST API 122 | See [REST API doc](https://github.com/mkaczanowski/pastebin/blob/master/API.md) 123 | 124 | ## Benchmark 125 | I used [k6.io](https://k6.io/) for benchmarking the read-by-id HTTP endoint. Details: 126 | * CPU: Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz (4 CPUs, 8 threads = 16 rocket workers) 127 | * Mem: 24 GiB 128 | * Storage: NVMe SSD Controller SM981/PM981/PM983 129 | * both client (k6) and server (pastebin) running on the same machine 130 | 131 | ### Setup 132 | ``` 133 | $ cargo run --release 134 | 135 | $ echo "Hello world" | curl -q -L -d @- -o - http://localhost:8000/ 136 | http://localhost:8000/0FWc4aaZXzf6GZBsuW4nv 137 | 138 | $ cat > script.js <"); 143 | }; 144 | EOL 145 | 146 | $ docker pull loadimpact/k6 147 | ``` 148 | 149 | ### Test 1: 5 concurrent clients, duration: 15s 150 | ``` 151 | $ docker run --network=host -i loadimpact/k6 run --vus 5 -d 15s - /etc/nginx/nginx.conf <<'EON' 25 | daemon off; 26 | error_log /dev/stderr info; 27 | 28 | events { 29 | worker_connections 768; 30 | } 31 | 32 | http { 33 | map $$sent_http_content_type $$expires { 34 | default off; 35 | text/css 30d; 36 | application/javascript 30d; 37 | image/x-icon 30d; 38 | } 39 | 40 | server { 41 | listen 80; 42 | server_name 0.0.0.0; 43 | 44 | gzip on; 45 | gzip_types text/plain application/xml text/css application/javascript; 46 | 47 | expires $$expires; 48 | location / { 49 | proxy_pass http://pastebin:8081; 50 | 51 | } 52 | 53 | access_log /dev/stdout; 54 | } 55 | } 56 | EON 57 | set -eux 58 | cat /etc/nginx/nginx.conf 59 | nginx 60 | EOF" 61 | -------------------------------------------------------------------------------- /src/api_generated.rs: -------------------------------------------------------------------------------- 1 | // automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | #![allow(warnings)] 4 | #![allow(clippy)] 5 | #![allow(unknown_lints)] 6 | 7 | use std::mem; 8 | use std::cmp::Ordering; 9 | 10 | extern crate flatbuffers; 11 | use self::flatbuffers::EndianScalar; 12 | 13 | #[allow(unused_imports, dead_code)] 14 | pub mod api { 15 | 16 | use std::mem; 17 | use std::cmp::Ordering; 18 | 19 | extern crate flatbuffers; 20 | use self::flatbuffers::EndianScalar; 21 | 22 | pub enum EntryOffset {} 23 | #[derive(Copy, Clone, Debug, PartialEq)] 24 | 25 | pub struct Entry<'a> { 26 | pub _tab: flatbuffers::Table<'a>, 27 | } 28 | 29 | impl<'a> flatbuffers::Follow<'a> for Entry<'a> { 30 | type Inner = Entry<'a>; 31 | #[inline] 32 | fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { 33 | Self { 34 | _tab: flatbuffers::Table { buf: buf, loc: loc }, 35 | } 36 | } 37 | } 38 | 39 | impl<'a> Entry<'a> { 40 | #[inline] 41 | pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { 42 | Entry { 43 | _tab: table, 44 | } 45 | } 46 | #[allow(unused_mut)] 47 | pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( 48 | _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, 49 | args: &'args EntryArgs<'args>) -> flatbuffers::WIPOffset> { 50 | let mut builder = EntryBuilder::new(_fbb); 51 | builder.add_expiry_timestamp(args.expiry_timestamp); 52 | builder.add_create_timestamp(args.create_timestamp); 53 | if let Some(x) = args.lang { builder.add_lang(x); } 54 | if let Some(x) = args.data { builder.add_data(x); } 55 | builder.add_encrypted(args.encrypted); 56 | builder.add_burn(args.burn); 57 | builder.finish() 58 | } 59 | 60 | pub const VT_CREATE_TIMESTAMP: flatbuffers::VOffsetT = 4; 61 | pub const VT_EXPIRY_TIMESTAMP: flatbuffers::VOffsetT = 6; 62 | pub const VT_DATA: flatbuffers::VOffsetT = 8; 63 | pub const VT_LANG: flatbuffers::VOffsetT = 10; 64 | pub const VT_BURN: flatbuffers::VOffsetT = 12; 65 | pub const VT_ENCRYPTED: flatbuffers::VOffsetT = 14; 66 | 67 | #[inline] 68 | pub fn create_timestamp(&self) -> u64 { 69 | self._tab.get::(Entry::VT_CREATE_TIMESTAMP, Some(0)).unwrap() 70 | } 71 | #[inline] 72 | pub fn expiry_timestamp(&self) -> u64 { 73 | self._tab.get::(Entry::VT_EXPIRY_TIMESTAMP, Some(0)).unwrap() 74 | } 75 | #[inline] 76 | pub fn data(&self) -> Option<&'a [u8]> { 77 | self._tab.get::>>(Entry::VT_DATA, None).map(|v| v.safe_slice()) 78 | } 79 | #[inline] 80 | pub fn lang(&self) -> Option<&'a str> { 81 | self._tab.get::>(Entry::VT_LANG, None) 82 | } 83 | #[inline] 84 | pub fn burn(&self) -> bool { 85 | self._tab.get::(Entry::VT_BURN, Some(false)).unwrap() 86 | } 87 | #[inline] 88 | pub fn encrypted(&self) -> bool { 89 | self._tab.get::(Entry::VT_ENCRYPTED, Some(false)).unwrap() 90 | } 91 | } 92 | 93 | pub struct EntryArgs<'a> { 94 | pub create_timestamp: u64, 95 | pub expiry_timestamp: u64, 96 | pub data: Option>>, 97 | pub lang: Option>, 98 | pub burn: bool, 99 | pub encrypted: bool, 100 | } 101 | impl<'a> Default for EntryArgs<'a> { 102 | #[inline] 103 | fn default() -> Self { 104 | EntryArgs { 105 | create_timestamp: 0, 106 | expiry_timestamp: 0, 107 | data: None, 108 | lang: None, 109 | burn: false, 110 | encrypted: false, 111 | } 112 | } 113 | } 114 | pub struct EntryBuilder<'a: 'b, 'b> { 115 | fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, 116 | start_: flatbuffers::WIPOffset, 117 | } 118 | impl<'a: 'b, 'b> EntryBuilder<'a, 'b> { 119 | #[inline] 120 | pub fn add_create_timestamp(&mut self, create_timestamp: u64) { 121 | self.fbb_.push_slot::(Entry::VT_CREATE_TIMESTAMP, create_timestamp, 0); 122 | } 123 | #[inline] 124 | pub fn add_expiry_timestamp(&mut self, expiry_timestamp: u64) { 125 | self.fbb_.push_slot::(Entry::VT_EXPIRY_TIMESTAMP, expiry_timestamp, 0); 126 | } 127 | #[inline] 128 | pub fn add_data(&mut self, data: flatbuffers::WIPOffset>) { 129 | self.fbb_.push_slot_always::>(Entry::VT_DATA, data); 130 | } 131 | #[inline] 132 | pub fn add_lang(&mut self, lang: flatbuffers::WIPOffset<&'b str>) { 133 | self.fbb_.push_slot_always::>(Entry::VT_LANG, lang); 134 | } 135 | #[inline] 136 | pub fn add_burn(&mut self, burn: bool) { 137 | self.fbb_.push_slot::(Entry::VT_BURN, burn, false); 138 | } 139 | #[inline] 140 | pub fn add_encrypted(&mut self, encrypted: bool) { 141 | self.fbb_.push_slot::(Entry::VT_ENCRYPTED, encrypted, false); 142 | } 143 | #[inline] 144 | pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> EntryBuilder<'a, 'b> { 145 | let start = _fbb.start_table(); 146 | EntryBuilder { 147 | fbb_: _fbb, 148 | start_: start, 149 | } 150 | } 151 | #[inline] 152 | pub fn finish(self) -> flatbuffers::WIPOffset> { 153 | let o = self.fbb_.end_table(self.start_); 154 | flatbuffers::WIPOffset::new(o.value()) 155 | } 156 | } 157 | 158 | #[inline] 159 | pub fn get_root_as_entry<'a>(buf: &'a [u8]) -> Entry<'a> { 160 | flatbuffers::get_root::>(buf) 161 | } 162 | 163 | #[inline] 164 | pub fn get_size_prefixed_root_as_entry<'a>(buf: &'a [u8]) -> Entry<'a> { 165 | flatbuffers::get_size_prefixed_root::>(buf) 166 | } 167 | 168 | #[inline] 169 | pub fn finish_entry_buffer<'a, 'b>( 170 | fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>, 171 | root: flatbuffers::WIPOffset>) { 172 | fbb.finish(root, None); 173 | } 174 | 175 | #[inline] 176 | pub fn finish_size_prefixed_entry_buffer<'a, 'b>(fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>, root: flatbuffers::WIPOffset>) { 177 | fbb.finish_size_prefixed(root, None); 178 | } 179 | } // pub mod api 180 | 181 | -------------------------------------------------------------------------------- /src/formatter.rs: -------------------------------------------------------------------------------- 1 | use handlebars::{Handlebars, JsonRender}; 2 | 3 | pub fn new<'r>() -> Handlebars<'r> { 4 | let mut handlebars = Handlebars::new(); 5 | handlebars.register_helper("format_url", Box::new(format_helper)); 6 | 7 | handlebars 8 | } 9 | 10 | fn format_helper( 11 | h: &handlebars::Helper, 12 | _: &Handlebars, 13 | _: &handlebars::Context, 14 | _: &mut handlebars::RenderContext, 15 | out: &mut dyn handlebars::Output, 16 | ) -> Result<(), handlebars::RenderError> { 17 | let prefix_val = h.param(0).ok_or(handlebars::RenderError::new( 18 | "Param 0 is required for format helper.", 19 | ))?; 20 | 21 | let uri_val = h.param(1).ok_or(handlebars::RenderError::new( 22 | "Param 1 is required for format helper.", 23 | ))?; 24 | 25 | let prefix = prefix_val.value().render(); 26 | let uri = uri_val.value().render(); 27 | 28 | let rendered = match uri.starts_with("/") { 29 | true => format!("{}{}", prefix, uri), 30 | false => uri, 31 | }; 32 | 33 | out.write(rendered.as_ref())?; 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate flatbuffers; 2 | 3 | use std::io; 4 | use std::time::SystemTime; 5 | 6 | use flatbuffers::FlatBufferBuilder; 7 | use rocket::State; 8 | use rocksdb::{compaction_filter, DB}; 9 | 10 | #[path = "api_generated.rs"] 11 | mod api_generated; 12 | use crate::api_generated::api::{finish_entry_buffer, get_root_as_entry, Entry, EntryArgs}; 13 | 14 | #[macro_export] 15 | macro_rules! load_static_resources( 16 | { $($key:expr => $value:expr),+ } => { 17 | { 18 | let mut resources: HashMap<&'static str, &'static [u8]> = HashMap::new(); 19 | $( 20 | resources.insert($key, include_bytes!($value)); 21 | )* 22 | 23 | resources 24 | } 25 | }; 26 | ); 27 | 28 | pub fn compaction_filter_expired_entries( 29 | _: u32, 30 | _: &[u8], 31 | value: &[u8], 32 | ) -> compaction_filter::Decision { 33 | use compaction_filter::Decision::*; 34 | 35 | let entry = get_root_as_entry(value); 36 | let now = SystemTime::now() 37 | .duration_since(SystemTime::UNIX_EPOCH) 38 | .expect("time went backwards") 39 | .as_secs(); 40 | 41 | if entry.expiry_timestamp() != 0 && now >= entry.expiry_timestamp() { 42 | Remove 43 | } else { 44 | Keep 45 | } 46 | } 47 | 48 | pub fn get_extension(filename: &str) -> &str { 49 | filename 50 | .rfind('.') 51 | .map(|idx| &filename[idx..]) 52 | .filter(|ext| ext.chars().skip(1).all(|c| c.is_ascii_alphanumeric())) 53 | .unwrap_or("") 54 | } 55 | 56 | pub fn get_entry_data<'r>(id: &str, state: &'r State<'r, DB>) -> Result, io::Error> { 57 | // read data from DB to Entry struct 58 | let root = match state.get(id).unwrap() { 59 | Some(root) => root, 60 | None => return Err(io::Error::new(io::ErrorKind::NotFound, "record not found")), 61 | }; 62 | let entry = get_root_as_entry(&root); 63 | 64 | // check if data expired (might not be yet deleted by rocksb compaction hook) 65 | let now = SystemTime::now() 66 | .duration_since(SystemTime::UNIX_EPOCH) 67 | .expect("time went backwards") 68 | .as_secs(); 69 | 70 | if entry.expiry_timestamp() != 0 && now >= entry.expiry_timestamp() { 71 | state.delete(id).unwrap(); 72 | return Err(io::Error::new(io::ErrorKind::NotFound, "record not found")); 73 | } 74 | 75 | // "burn" one time only pastebin content 76 | if entry.burn() { 77 | state.delete(id).unwrap(); 78 | } 79 | 80 | Ok(root) 81 | } 82 | 83 | pub fn new_entry( 84 | dest: &mut Vec, 85 | data: &mut rocket::data::DataStream, 86 | lang: String, 87 | ttl: u64, 88 | burn: bool, 89 | encrypted: bool, 90 | ) { 91 | let mut bldr = FlatBufferBuilder::new(); 92 | 93 | dest.clear(); 94 | bldr.reset(); 95 | 96 | // potential speed improvement over the create_vector: 97 | // https://docs.rs/flatbuffers/0.6.1/flatbuffers/struct.FlatBufferBuilder.html#method.create_vector 98 | let mut tmp_vec: Vec = vec![]; 99 | std::io::copy(data, &mut tmp_vec).unwrap(); 100 | 101 | bldr.start_vector::(tmp_vec.len()); 102 | for byte in tmp_vec.iter().rev() { 103 | bldr.push::(*byte); 104 | } 105 | let data_vec = bldr.end_vector::(tmp_vec.len()); 106 | 107 | // calc expiry datetime 108 | let now = SystemTime::now() 109 | .duration_since(SystemTime::UNIX_EPOCH) 110 | .expect("time went backwards") 111 | .as_secs(); 112 | let expiry = if ttl == 0 { ttl } else { now + ttl }; 113 | 114 | // setup actual struct 115 | let args = EntryArgs { 116 | create_timestamp: now, 117 | expiry_timestamp: expiry, 118 | data: Some(data_vec), 119 | lang: Some(bldr.create_string(&lang)), 120 | burn, 121 | encrypted, 122 | }; 123 | 124 | let user_offset = Entry::create(&mut bldr, &args); 125 | finish_entry_buffer(&mut bldr, user_offset); 126 | 127 | let finished_data = bldr.finished_data(); 128 | dest.extend_from_slice(finished_data); 129 | } 130 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene, decl_macro)] 2 | #![allow(clippy::too_many_arguments)] 3 | 4 | #[macro_use] 5 | extern crate rocket; 6 | #[macro_use] 7 | extern crate structopt_derive; 8 | extern crate chrono; 9 | extern crate flatbuffers; 10 | extern crate handlebars; 11 | extern crate nanoid; 12 | extern crate num_cpus; 13 | extern crate regex; 14 | extern crate speculate; 15 | extern crate structopt; 16 | 17 | mod formatter; 18 | 19 | #[macro_use] 20 | mod lib; 21 | use lib::{compaction_filter_expired_entries, get_entry_data, get_extension, new_entry}; 22 | 23 | mod plugins; 24 | use plugins::plugin::{Plugin, PluginManager}; 25 | 26 | mod api_generated; 27 | use api_generated::api::get_root_as_entry; 28 | 29 | use std::io; 30 | use std::io::Cursor; 31 | use std::path::Path; 32 | 33 | use rocket::config::{Config, Environment}; 34 | use rocket::http::{ContentType, Status}; 35 | use rocket::response::{Redirect, Response}; 36 | use rocket::{Data, State}; 37 | 38 | use chrono::NaiveDateTime; 39 | use handlebars::Handlebars; 40 | use humantime::parse_duration; 41 | use nanoid::nanoid; 42 | use regex::Regex; 43 | use rocksdb::{Options, DB}; 44 | use serde_json::json; 45 | use speculate::speculate; 46 | use structopt::StructOpt; 47 | 48 | speculate! { 49 | use super::rocket; 50 | use rocket::local::Client; 51 | use rocket::http::Status; 52 | 53 | before { 54 | use tempdir::TempDir; 55 | 56 | // setup temporary database 57 | let tmp_dir = TempDir::new("rocks_db_test").unwrap(); 58 | let file_path = tmp_dir.path().join("database"); 59 | let mut pastebin_config = PastebinConfig::from_args(); 60 | pastebin_config.db_path = file_path.to_str().unwrap().to_string(); 61 | let rocket = rocket(pastebin_config); 62 | 63 | // init rocket client 64 | let client = Client::new(rocket).expect("invalid rocket instance"); 65 | } 66 | 67 | #[allow(dead_code)] 68 | fn insert_data<'r>(client: &'r Client, data: &str, path: &str) -> String { 69 | let mut response = client.post(path) 70 | .body(data) 71 | .dispatch(); 72 | assert_eq!(response.status(), Status::Ok); 73 | 74 | // retrieve paste ID 75 | let url = response.body_string().unwrap(); 76 | let id = url.split('/').collect::>().last().cloned().unwrap(); 77 | 78 | id.to_string() 79 | } 80 | 81 | #[allow(dead_code)] 82 | fn get_data(client: &Client, path: String) -> rocket::local::LocalResponse { 83 | client.get(format!("/{}", path)).dispatch() 84 | } 85 | 86 | it "can get create and fetch paste" { 87 | // store data via post request 88 | let id = insert_data(&client, "random_test_data_to_be_checked", "/"); 89 | 90 | // retrieve the data via get request 91 | let mut response = get_data(&client, id); 92 | assert_eq!(response.status(), Status::Ok); 93 | assert_eq!(response.content_type(), Some(ContentType::HTML)); 94 | assert!(response.body_string().unwrap().contains("random_test_data_to_be_checked")); 95 | } 96 | 97 | it "can remove paste by id" { 98 | let response = client.delete("/some_id").dispatch(); 99 | assert_eq!(response.status(), Status::Ok); 100 | 101 | let response = get_data(&client, "some_id".to_string()); 102 | assert_eq!(response.status(), Status::NotFound); 103 | } 104 | 105 | it "can remove non-existing paste" { 106 | let response = get_data(&client, "some_fake_id".to_string()); 107 | assert_eq!(response.status(), Status::NotFound); 108 | 109 | let response = client.delete("/some_fake_id").dispatch(); 110 | assert_eq!(response.status(), Status::Ok); 111 | 112 | let response = get_data(&client, "some_fake_id".to_string()); 113 | assert_eq!(response.status(), Status::NotFound); 114 | } 115 | 116 | it "can get raw contents" { 117 | // store data via post request 118 | let id = insert_data(&client, "random_test_data_to_be_checked", "/"); 119 | 120 | // retrieve the data via get request 121 | let mut response = get_data(&client, format!("raw/{}", id)); 122 | assert_eq!(response.status(), Status::Ok); 123 | assert_eq!(response.content_type(), Some(ContentType::Plain)); 124 | assert!(response.body_string().unwrap().contains("random_test_data_to_be_checked")); 125 | } 126 | 127 | it "can download contents" { 128 | // store data via post request 129 | let id = insert_data(&client, "random_test_data_to_be_checked", "/"); 130 | 131 | // retrieve the data via get request 132 | let mut response = get_data(&client, format!("download/{}", id)); 133 | assert_eq!(response.status(), Status::Ok); 134 | assert_eq!(response.content_type(), Some(ContentType::Binary)); 135 | assert!(response.body_string().unwrap().contains("random_test_data_to_be_checked")); 136 | } 137 | 138 | it "can clone contents" { 139 | // store data via post request 140 | let id = insert_data(&client, "random_test_data_to_be_checked", "/"); 141 | 142 | // retrieve the data via get request 143 | let mut response = get_data(&client, format!("new?id={}", id)); 144 | assert_eq!(response.status(), Status::Ok); 145 | assert_eq!(response.content_type(), Some(ContentType::HTML)); 146 | assert!(response.body_string().unwrap().contains("random_test_data_to_be_checked")); 147 | } 148 | 149 | it "can't get burned paste" { 150 | // store data via post request 151 | let id = insert_data(&client, "random_test_data_to_be_checked", "/?burn=true"); 152 | let response = get_data(&client, id.clone()); 153 | assert_eq!(response.status(), Status::Ok); 154 | 155 | // retrieve the data via get request 156 | let response = get_data(&client, id); 157 | assert_eq!(response.status(), Status::NotFound); 158 | } 159 | 160 | it "can't get expired paste" { 161 | use std::{thread, time}; 162 | 163 | // store data via post request 164 | let id = insert_data(&client, "random_test_data_to_be_checked", "/?ttl=1"); 165 | let response = get_data(&client, id.clone()); 166 | assert_eq!(response.status(), Status::Ok); 167 | 168 | thread::sleep(time::Duration::from_secs(1)); 169 | 170 | // retrieve the data via get request 171 | let response = get_data(&client, id); 172 | assert_eq!(response.status(), Status::NotFound); 173 | } 174 | 175 | it "can get static contents" { 176 | let mut response = client.get("/static/favicon.ico").dispatch(); 177 | let contents = std::fs::read("static/favicon.ico").unwrap(); 178 | 179 | assert_eq!(response.status(), Status::Ok); 180 | assert_eq!(response.body_bytes(), Some(contents)); 181 | } 182 | 183 | it "can cope with invalid unicode data" { 184 | let invalid_data = unsafe { 185 | String::from_utf8_unchecked(b"Hello \xF0\x90\x80World".to_vec()) 186 | }; 187 | let id = insert_data(&client, &invalid_data, "/"); 188 | 189 | let response = get_data(&client, id); 190 | assert_eq!(response.status(), Status::Ok); 191 | } 192 | } 193 | 194 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 195 | 196 | #[derive(StructOpt, Debug)] 197 | #[structopt( 198 | name = "pastebin", 199 | about = "Simple, standalone and fast pastebin service." 200 | )] 201 | struct PastebinConfig { 202 | #[structopt( 203 | long = "address", 204 | help = "IP address or host to listen on", 205 | default_value = "localhost" 206 | )] 207 | address: String, 208 | 209 | #[structopt( 210 | long = "port", 211 | help = "Port number to listen on", 212 | default_value = "8000" 213 | )] 214 | port: u16, 215 | 216 | #[structopt( 217 | long = "environment", 218 | help = "Rocket server environment", 219 | default_value = "production" 220 | )] 221 | environment: String, 222 | 223 | #[structopt( 224 | long = "workers", 225 | help = "Number of concurrent thread workers", 226 | default_value = "0" 227 | )] 228 | workers: u16, 229 | 230 | #[structopt( 231 | long = "keep-alive", 232 | help = "Keep-alive timeout in seconds", 233 | default_value = "5" 234 | )] 235 | keep_alive: u32, 236 | 237 | #[structopt(long = "log", help = "Max log level", default_value = "normal")] 238 | log: rocket::config::LoggingLevel, 239 | 240 | #[structopt( 241 | long = "ttl", 242 | help = "Time to live for entries, by default kept forever", 243 | default_value = "0" 244 | )] 245 | ttl: u64, 246 | 247 | #[structopt( 248 | long = "db", 249 | help = "Database file path", 250 | default_value = "./pastebin.db" 251 | )] 252 | db_path: String, 253 | 254 | #[structopt(long = "tls-certs", help = "Path to certificate chain in PEM format")] 255 | tls_certs: Option, 256 | 257 | #[structopt( 258 | long = "tls-key", 259 | help = "Path to private key for tls-certs in PEM format" 260 | )] 261 | tls_key: Option, 262 | 263 | #[structopt(long = "uri", help = "Override default URI")] 264 | uri: Option, 265 | 266 | #[structopt( 267 | long = "uri-prefix", 268 | help = "Prefix appended to the URI (ie. '/pastebin')", 269 | default_value = "" 270 | )] 271 | uri_prefix: String, 272 | 273 | #[structopt( 274 | long = "slug-charset", 275 | help = "Character set (expressed as rust compatible regex) to use for generating the URL slug", 276 | default_value = "[A-Za-z0-9_-]" 277 | )] 278 | slug_charset: String, 279 | 280 | #[structopt(long = "slug-len", help = "Length of URL slug", default_value = "21")] 281 | slug_len: usize, 282 | 283 | #[structopt( 284 | long = "ui-expiry-times", 285 | help = "List of paste expiry times redered in the UI dropdown selector", 286 | default_value = "5 minutes, 10 minutes, 1 hour, 1 day, 1 week, 1 month, 1 year, Never" 287 | )] 288 | ui_expiry_times: Vec, 289 | 290 | #[structopt(long = "ui-line-numbers", help = "Display line numbers")] 291 | ui_line_numbers: bool, 292 | 293 | #[structopt( 294 | long = "plugins", 295 | help = "Enable additional functionalities (ie. prism, mermaid)", 296 | default_value = "prism" 297 | )] 298 | plugins: Vec, 299 | } 300 | 301 | fn get_url(cfg: &PastebinConfig) -> String { 302 | let port = if vec![443, 80].contains(&cfg.port) { 303 | String::from("") 304 | } else { 305 | format!(":{}", cfg.port) 306 | }; 307 | let scheme = if cfg.tls_certs.is_some() { 308 | "https" 309 | } else { 310 | "http" 311 | }; 312 | 313 | if cfg.uri.is_some() { 314 | cfg.uri.clone().unwrap() 315 | } else { 316 | format!( 317 | "{scheme}://{address}{port}", 318 | scheme = scheme, 319 | port = port, 320 | address = cfg.address, 321 | ) 322 | } 323 | } 324 | 325 | fn get_error_response<'r>( 326 | handlebars: &Handlebars<'r>, 327 | uri_prefix: String, 328 | html: String, 329 | status: Status, 330 | ) -> Response<'r> { 331 | let map = json!({ 332 | "version": VERSION, 333 | "is_error": "true", 334 | "uri_prefix": uri_prefix, 335 | }); 336 | 337 | let content = handlebars.render_template(html.as_str(), &map).unwrap(); 338 | 339 | Response::build() 340 | .status(status) 341 | .sized_body(Cursor::new(content)) 342 | .finalize() 343 | } 344 | 345 | #[post("/?&&&", data = "")] 346 | fn create( 347 | paste: Data, 348 | state: State, 349 | cfg: State, 350 | alphabet: State>, 351 | lang: Option, 352 | ttl: Option, 353 | burn: Option, 354 | encrypted: Option, 355 | ) -> Result { 356 | let slug_len = cfg.inner().slug_len; 357 | let id = nanoid!(slug_len, alphabet.inner()); 358 | let url = format!("{url}/{id}", url = get_url(cfg.inner()), id = id); 359 | 360 | let mut writer: Vec = vec![]; 361 | new_entry( 362 | &mut writer, 363 | &mut paste.open(), 364 | lang.unwrap_or_else(|| String::from("markup")), 365 | ttl.unwrap_or(cfg.ttl), 366 | burn.unwrap_or(false), 367 | encrypted.unwrap_or(false), 368 | ); 369 | 370 | state.put(id, writer).unwrap(); 371 | 372 | Ok(url) 373 | } 374 | 375 | #[delete("/")] 376 | fn remove(id: String, state: State) -> Result<(), rocksdb::Error> { 377 | state.delete(id) 378 | } 379 | 380 | #[get("/?")] 381 | fn get<'r>( 382 | id: String, 383 | lang: Option, 384 | state: State<'r, DB>, 385 | handlebars: State<'r, Handlebars>, 386 | plugin_manager: State, 387 | ui_expiry_times: State<'r, Vec<(String, u64)>>, 388 | ui_expiry_default: State<'r, String>, 389 | cfg: State, 390 | ) -> Response<'r> { 391 | let resources = plugin_manager.static_resources(); 392 | let html = String::from_utf8_lossy(resources.get("/static/index.html").unwrap()).to_string(); 393 | 394 | // handle missing entry 395 | let root = match get_entry_data(&id, &state) { 396 | Ok(x) => x, 397 | Err(e) => { 398 | let err_kind = match e.kind() { 399 | io::ErrorKind::NotFound => Status::NotFound, 400 | _ => Status::InternalServerError, 401 | }; 402 | 403 | let map = json!({ 404 | "version": VERSION, 405 | "is_error": "true", 406 | "uri_prefix": cfg.uri_prefix, 407 | "js_imports": plugin_manager.js_imports(), 408 | "css_imports": plugin_manager.css_imports(), 409 | "js_init": plugin_manager.js_init(), 410 | }); 411 | 412 | let content = handlebars.render_template(html.as_str(), &map).unwrap(); 413 | 414 | return Response::build() 415 | .status(err_kind) 416 | .sized_body(Cursor::new(content)) 417 | .finalize(); 418 | } 419 | }; 420 | 421 | // handle existing entry 422 | let entry = get_root_as_entry(&root); 423 | let selected_lang = lang 424 | .unwrap_or_else(|| entry.lang().unwrap().to_string()) 425 | .to_lowercase(); 426 | 427 | let mut pastebin_cls = Vec::new(); 428 | if cfg.ui_line_numbers { 429 | pastebin_cls.push("line-numbers".to_string()); 430 | } 431 | 432 | pastebin_cls.push(format!("language-{}", selected_lang)); 433 | 434 | let mut map = json!({ 435 | "is_created": "true", 436 | "pastebin_code": String::from_utf8_lossy(entry.data().unwrap()), 437 | "pastebin_id": id, 438 | "pastebin_cls": pastebin_cls.join(" "), 439 | "version": VERSION, 440 | "uri_prefix": cfg.uri_prefix, 441 | "ui_expiry_times": ui_expiry_times.inner(), 442 | "ui_expiry_default": ui_expiry_default.inner(), 443 | "js_imports": plugin_manager.js_imports(), 444 | "css_imports": plugin_manager.css_imports(), 445 | "js_init": plugin_manager.js_init(), 446 | }); 447 | 448 | if entry.burn() { 449 | map["msg"] = json!("FOR YOUR EYES ONLY. The paste is gone, after you close this window."); 450 | map["level"] = json!("warning"); 451 | map["is_burned"] = json!("true"); 452 | map["glyph"] = json!("fa fa-fire"); 453 | } else if entry.expiry_timestamp() != 0 { 454 | let time = NaiveDateTime::from_timestamp(entry.expiry_timestamp() as i64, 0) 455 | .format("%Y-%m-%d %H:%M:%S"); 456 | map["msg"] = json!(format!("This paste will expire on {}.", time)); 457 | map["level"] = json!("info"); 458 | map["glyph"] = json!("far fa-clock"); 459 | } 460 | 461 | if entry.encrypted() { 462 | map["is_encrypted"] = json!("true"); 463 | } 464 | 465 | let content = handlebars.render_template(html.as_str(), &map).unwrap(); 466 | 467 | Response::build() 468 | .status(Status::Ok) 469 | .header(ContentType::HTML) 470 | .sized_body(Cursor::new(content)) 471 | .finalize() 472 | } 473 | 474 | #[get("/new?&&&&")] 475 | fn get_new<'r>( 476 | state: State<'r, DB>, 477 | handlebars: State, 478 | cfg: State, 479 | plugin_manager: State, 480 | ui_expiry_times: State<'r, Vec<(String, u64)>>, 481 | ui_expiry_default: State<'r, String>, 482 | id: Option, 483 | level: Option, 484 | glyph: Option, 485 | msg: Option, 486 | url: Option, 487 | ) -> Response<'r> { 488 | let resources = plugin_manager.static_resources(); 489 | let html = String::from_utf8_lossy(resources.get("/static/index.html").unwrap()).to_string(); 490 | let msg = msg.unwrap_or_else(|| String::from("")); 491 | let level = level.unwrap_or_else(|| String::from("secondary")); 492 | let glyph = glyph.unwrap_or_else(|| String::from("")); 493 | let url = url.unwrap_or_else(|| String::from("")); 494 | let root: Vec; 495 | 496 | let mut map = json!({ 497 | "is_editable": "true", 498 | "version": VERSION, 499 | "msg": msg, 500 | "level": level, 501 | "glyph": glyph, 502 | "url": url, 503 | "uri_prefix": cfg.uri_prefix, 504 | "ui_expiry_times": ui_expiry_times.inner(), 505 | "ui_expiry_default": ui_expiry_default.inner(), 506 | "js_imports": plugin_manager.js_imports(), 507 | "css_imports": plugin_manager.css_imports(), 508 | "js_init": plugin_manager.js_init(), 509 | }); 510 | 511 | if let Some(id) = id { 512 | root = get_entry_data(&id, &state).unwrap(); 513 | let entry = get_root_as_entry(&root); 514 | 515 | if entry.encrypted() { 516 | map["is_encrypted"] = json!("true"); 517 | } 518 | 519 | map["pastebin_code"] = json!(std::str::from_utf8(entry.data().unwrap()).unwrap()); 520 | } 521 | 522 | let content = handlebars.render_template(html.as_str(), &map).unwrap(); 523 | 524 | Response::build() 525 | .status(Status::Ok) 526 | .header(ContentType::HTML) 527 | .sized_body(Cursor::new(content)) 528 | .finalize() 529 | } 530 | 531 | #[get("/raw/")] 532 | fn get_raw(id: String, state: State) -> Response { 533 | // handle missing entry 534 | let root = match get_entry_data(&id, &state) { 535 | Ok(x) => x, 536 | Err(e) => { 537 | let err_kind = match e.kind() { 538 | io::ErrorKind::NotFound => Status::NotFound, 539 | _ => Status::InternalServerError, 540 | }; 541 | 542 | return Response::build().status(err_kind).finalize(); 543 | } 544 | }; 545 | 546 | let entry = get_root_as_entry(&root); 547 | let mut data: Vec = vec![]; 548 | 549 | io::copy(&mut entry.data().unwrap(), &mut data).unwrap(); 550 | 551 | Response::build() 552 | .status(Status::Ok) 553 | .header(ContentType::Plain) 554 | .sized_body(Cursor::new(data)) 555 | .finalize() 556 | } 557 | 558 | #[get("/download/")] 559 | fn get_binary(id: String, state: State) -> Response { 560 | let response = get_raw(id, state); 561 | Response::build_from(response) 562 | .header(ContentType::Binary) 563 | .finalize() 564 | } 565 | 566 | #[get("/static/")] 567 | fn get_static<'r>( 568 | resource: String, 569 | handlebars: State, 570 | plugin_manager: State, 571 | cfg: State, 572 | ) -> Response<'r> { 573 | let resources = plugin_manager.static_resources(); 574 | let pth = format!("/static/{}", resource); 575 | let ext = get_extension(resource.as_str()).replace(".", ""); 576 | 577 | let content = match resources.get(pth.as_str()) { 578 | Some(data) => data, 579 | None => { 580 | let html = 581 | String::from_utf8_lossy(resources.get("/static/index.html").unwrap()).to_string(); 582 | 583 | return get_error_response( 584 | handlebars.inner(), 585 | cfg.uri_prefix.clone(), 586 | html, 587 | Status::NotFound, 588 | ); 589 | } 590 | }; 591 | let content_type = ContentType::from_extension(ext.as_str()).unwrap(); 592 | 593 | Response::build() 594 | .status(Status::Ok) 595 | .header(content_type) 596 | .sized_body(Cursor::new(content.iter())) 597 | .finalize() 598 | } 599 | 600 | #[get("/")] 601 | fn index(cfg: State) -> Redirect { 602 | let url = String::from( 603 | Path::new(cfg.uri_prefix.as_str()) 604 | .join("new") 605 | .to_str() 606 | .unwrap(), 607 | ); 608 | 609 | Redirect::to(url) 610 | } 611 | 612 | fn rocket(pastebin_config: PastebinConfig) -> rocket::Rocket { 613 | // parse command line opts 614 | let environ: Environment = pastebin_config.environment.parse().unwrap(); 615 | let workers = if pastebin_config.workers != 0 { 616 | pastebin_config.workers 617 | } else { 618 | num_cpus::get() as u16 * 2 619 | }; 620 | let mut rocket_config = Config::build(environ) 621 | .address(pastebin_config.address.clone()) 622 | .port(pastebin_config.port) 623 | .workers(workers) 624 | .keep_alive(pastebin_config.keep_alive) 625 | .log_level(pastebin_config.log) 626 | .finalize() 627 | .unwrap(); 628 | 629 | // handle tls cert setup 630 | if pastebin_config.tls_certs.is_some() && pastebin_config.tls_key.is_some() { 631 | rocket_config 632 | .set_tls( 633 | pastebin_config.tls_certs.clone().unwrap().as_str(), 634 | pastebin_config.tls_key.clone().unwrap().as_str(), 635 | ) 636 | .unwrap(); 637 | } 638 | 639 | // setup db 640 | let db = DB::open_default(pastebin_config.db_path.clone()).unwrap(); 641 | let mut db_opts = Options::default(); 642 | 643 | db_opts.create_if_missing(true); 644 | db_opts.set_compaction_filter("ttl_entries", compaction_filter_expired_entries); 645 | 646 | // define slug URL alphabet 647 | let alphabet = { 648 | let re = Regex::new(&pastebin_config.slug_charset).unwrap(); 649 | 650 | let mut tmp = [0; 4]; 651 | let mut alphabet: Vec = vec![]; 652 | 653 | // match all printable ASCII characters 654 | for i in 0x20..0x7e as u8 { 655 | let c = i as char; 656 | 657 | if re.is_match(c.encode_utf8(&mut tmp)) { 658 | alphabet.push(c.clone()); 659 | } 660 | } 661 | 662 | alphabet 663 | }; 664 | 665 | // setup drop down expiry menu (for instance 1m, 20m, 1 year, never) 666 | let ui_expiry_times = { 667 | let mut all = vec![]; 668 | for item in pastebin_config.ui_expiry_times.clone() { 669 | for sub_elem in item.split(',') { 670 | if sub_elem.trim().to_lowercase() == "never" { 671 | all.push((sub_elem.trim().to_string(), 0)); 672 | } else { 673 | all.push(( 674 | sub_elem.trim().to_string(), 675 | parse_duration(sub_elem).unwrap().as_secs() 676 | )); 677 | } 678 | } 679 | } 680 | 681 | all 682 | }; 683 | 684 | let ui_expiry_default: String = ui_expiry_times 685 | .iter() 686 | .filter_map(|(name, val)| { 687 | if *val == pastebin_config.ttl { 688 | Some(name.clone()) 689 | } else { 690 | None 691 | } 692 | }) 693 | .collect(); 694 | 695 | if ui_expiry_default.is_empty() { 696 | panic!("the TTL flag should match one of the ui-expiry-times option"); 697 | } 698 | 699 | if pastebin_config.slug_len == 0 { 700 | panic!("slug_len must be larger than zero"); 701 | } 702 | 703 | if alphabet.len() == 0 { 704 | panic!("selected slug alphabet is empty, please check if slug_charset is a valid regex"); 705 | } 706 | 707 | let plugins: Vec> = pastebin_config 708 | .plugins 709 | .iter() 710 | .map(|t| match t.as_str() { 711 | "prism" => Box::new(plugins::prism::new()), 712 | "mermaid" => Box::new(plugins::mermaid::new()), 713 | _ => panic!("unknown plugin provided"), 714 | }) 715 | .map(|x| x as Box) 716 | .collect(); 717 | 718 | let plugin_manager = plugins::new(plugins); 719 | let uri_prefix = pastebin_config.uri_prefix.clone(); 720 | 721 | // run rocket 722 | rocket::custom(rocket_config) 723 | .manage(pastebin_config) 724 | .manage(db) 725 | .manage(formatter::new()) 726 | .manage(plugin_manager) 727 | .manage(alphabet) 728 | .manage(ui_expiry_times) 729 | .manage(ui_expiry_default) 730 | .mount( 731 | if uri_prefix == "" { 732 | "/" 733 | } else { 734 | uri_prefix.as_str() 735 | }, 736 | routes![index, create, remove, get, get_new, get_raw, get_binary, get_static], 737 | ) 738 | } 739 | 740 | fn main() { 741 | let pastebin_config = PastebinConfig::from_args(); 742 | rocket(pastebin_config).launch(); 743 | } 744 | -------------------------------------------------------------------------------- /src/plugins.rs: -------------------------------------------------------------------------------- 1 | pub mod mermaid; 2 | pub mod plugin; 3 | pub mod prism; 4 | 5 | use std::collections::HashMap; 6 | 7 | pub fn new<'r>(plugins: Vec>>) -> plugin::PluginManager<'r> { 8 | let base_static_resources = load_static_resources!( 9 | "/static/index.html" => "../static/index.html", 10 | "/static/custom.js" => "../static/custom.js", 11 | "/static/custom.css" => "../static/custom.css", 12 | "/static/favicon.ico" => "../static/favicon.ico" 13 | ); 14 | 15 | let base_css_imports = vec![ 16 | "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/css/bootstrap.min.css", 17 | "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css", 18 | "/static/custom.css", 19 | ]; 20 | 21 | let base_js_imports = vec![ 22 | "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js", 23 | "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js", 24 | "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js", 25 | "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/js/bootstrap.min.js", 26 | "https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.4/clipboard.min.js", 27 | "https://cdnjs.cloudflare.com/ajax/libs/bootstrap-notify/0.2.0/js/bootstrap-notify.min.js", 28 | "/static/custom.js", 29 | ]; 30 | 31 | plugin::PluginManager::build() 32 | .plugins(plugins) 33 | .static_resources(base_static_resources) 34 | .css_imports(base_css_imports) 35 | .js_imports(base_js_imports) 36 | .finalize() 37 | } 38 | -------------------------------------------------------------------------------- /src/plugins/mermaid.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::plugins::plugin::PastebinPlugin; 4 | 5 | pub fn new<'r>() -> PastebinPlugin<'r> { 6 | PastebinPlugin { 7 | css_imports: vec![], 8 | js_imports: vec!["https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.8.2/mermaid.min.js"], 9 | js_init: Some("mermaid.init(undefined, '.language-mermaid');"), 10 | static_resources: HashMap::new(), 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/plugins/plugin.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | pub trait Plugin<'r>: Sync + Send { 4 | fn css_imports(&self) -> &Vec<&'r str>; 5 | fn js_imports(&self) -> &Vec<&'r str>; 6 | fn js_init(&self) -> Option<&'r str>; 7 | fn static_resources(&self) -> &HashMap<&'r str, &'r [u8]>; 8 | } 9 | 10 | #[derive(Debug)] 11 | pub struct PastebinPlugin<'r> { 12 | pub css_imports: Vec<&'r str>, 13 | pub js_imports: Vec<&'r str>, 14 | pub js_init: Option<&'r str>, 15 | pub static_resources: HashMap<&'r str, &'r [u8]>, 16 | } 17 | 18 | impl<'r> Plugin<'r> for PastebinPlugin<'r> { 19 | fn css_imports(&self) -> &Vec<&'r str> { 20 | &self.css_imports 21 | } 22 | 23 | fn js_imports(&self) -> &Vec<&'r str> { 24 | &self.js_imports 25 | } 26 | 27 | fn js_init(&self) -> Option<&'r str> { 28 | self.js_init 29 | } 30 | 31 | fn static_resources(&self) -> &HashMap<&'r str, &'r [u8]> { 32 | &self.static_resources 33 | } 34 | } 35 | 36 | pub struct PluginManagerBuilder<'r> { 37 | manager: PluginManager<'r>, 38 | } 39 | 40 | impl<'r> PluginManagerBuilder<'r> { 41 | pub fn plugins(&mut self, plugins: Vec>>) -> &mut PluginManagerBuilder<'r> { 42 | self.manager.set_plugins(plugins); 43 | self 44 | } 45 | 46 | pub fn css_imports(&mut self, css_imports: Vec<&'r str>) -> &mut PluginManagerBuilder<'r> { 47 | self.manager.set_css_imports(css_imports); 48 | self 49 | } 50 | 51 | pub fn js_imports(&mut self, js_imports: Vec<&'r str>) -> &mut PluginManagerBuilder<'r> { 52 | self.manager.set_js_imports(js_imports); 53 | self 54 | } 55 | 56 | pub fn static_resources( 57 | &mut self, 58 | static_resources: HashMap<&'r str, &'r [u8]>, 59 | ) -> &mut PluginManagerBuilder<'r> { 60 | self.manager.set_static_resources(static_resources); 61 | self 62 | } 63 | 64 | pub fn finalize(&mut self) -> PluginManager<'r> { 65 | self.manager.build_css_imports(); 66 | self.manager.build_js_imports(); 67 | self.manager.build_js_init(); 68 | self.manager.build_static_resources(); 69 | 70 | std::mem::replace(&mut self.manager, PluginManager::new()) 71 | } 72 | } 73 | 74 | pub struct PluginManager<'r> { 75 | // plugins are used to build up the static members of the struct, for instance: 76 | // * js_imports (ie. "{{uri_prefix}}/static/prism.js") 77 | // * static_resources (files under static/ directory - compiled with the binary) 78 | plugins: Vec>>, 79 | 80 | css_imports: Vec<&'r str>, 81 | js_imports: Vec<&'r str>, 82 | js_init: Vec<&'r str>, 83 | static_resources: HashMap<&'r str, &'r [u8]>, 84 | } 85 | 86 | impl<'r> PluginManager<'r> { 87 | pub fn new() -> PluginManager<'r> { 88 | PluginManager { 89 | plugins: vec![], 90 | css_imports: vec![], 91 | js_imports: vec![], 92 | js_init: vec![], 93 | static_resources: HashMap::new(), 94 | } 95 | } 96 | 97 | pub fn build() -> PluginManagerBuilder<'r> { 98 | PluginManagerBuilder { 99 | manager: PluginManager::new(), 100 | } 101 | } 102 | 103 | pub fn set_plugins(&mut self, plugins: Vec>>) { 104 | self.plugins = plugins; 105 | } 106 | 107 | pub fn set_css_imports(&mut self, css_imports: Vec<&'r str>) { 108 | self.css_imports = css_imports; 109 | } 110 | 111 | pub fn css_imports(&self) -> Vec<&'r str> { 112 | self.css_imports.clone() 113 | } 114 | 115 | pub fn set_js_imports(&mut self, js_imports: Vec<&'r str>) { 116 | self.js_imports = js_imports; 117 | } 118 | 119 | pub fn js_imports(&self) -> Vec<&'r str> { 120 | self.js_imports.clone() 121 | } 122 | 123 | pub fn set_js_init(&mut self, js_init: Vec<&'r str>) { 124 | self.js_init = js_init; 125 | } 126 | 127 | pub fn js_init(&self) -> Vec<&'r str> { 128 | self.js_init.clone() 129 | } 130 | 131 | pub fn set_static_resources(&mut self, static_resources: HashMap<&'r str, &'r [u8]>) { 132 | self.static_resources = static_resources; 133 | } 134 | 135 | pub fn static_resources(&self) -> HashMap<&'r str, &'r [u8]> { 136 | self.static_resources.clone() 137 | } 138 | 139 | fn build_css_imports(&mut self) { 140 | self.set_css_imports( 141 | self.plugins 142 | .iter() 143 | .flat_map(|p| p.css_imports().into_iter()) 144 | .chain((&self.css_imports).into_iter()) 145 | .map(|&val| val) 146 | .collect(), 147 | ) 148 | } 149 | 150 | fn build_js_imports(&mut self) { 151 | self.set_js_imports( 152 | self.plugins 153 | .iter() 154 | .flat_map(|p| p.js_imports().into_iter()) 155 | .chain((&self.js_imports).into_iter()) 156 | .map(|&val| val) 157 | .collect(), 158 | ) 159 | } 160 | 161 | fn build_js_init(&mut self) { 162 | self.set_js_init( 163 | self.plugins 164 | .iter() 165 | .flat_map(|p| p.js_init().into_iter()) 166 | .chain((&self.js_init).into_iter().map(|&val| val)) 167 | .collect(), 168 | ) 169 | } 170 | 171 | fn build_static_resources(&mut self) { 172 | self.set_static_resources( 173 | self.plugins 174 | .iter() 175 | .flat_map(|p| p.static_resources().into_iter()) 176 | .chain((&self.static_resources).into_iter()) 177 | .map(|(&key, &val)| (key, val)) 178 | .collect(), 179 | ) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/plugins/prism.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::plugins::plugin::PastebinPlugin; 4 | 5 | pub fn new<'r>() -> PastebinPlugin<'r> { 6 | PastebinPlugin { 7 | css_imports: vec!["/static/prism.css"], 8 | js_imports: vec!["/static/prism.js"], 9 | js_init: Some( 10 | "var holder = $('#pastebin-code-block:first').get(0); \ 11 | if (holder) { Prism.highlightElement(holder); }", 12 | ), 13 | static_resources: load_static_resources! { 14 | "/static/prism.js" => "../../static/prism.js", 15 | "/static/prism.css" =>"../../static/prism.css" 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /static/custom.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | 6 | body { 7 | min-height: 75rem; /* Minimum navbar */ 8 | padding-top: 3.5rem; /* Margin top by nav bar */ 9 | padding-bottom: 3.5rem; /* Margin bottom by footer height */ 10 | } 11 | 12 | .footer { 13 | position: absolute; 14 | bottom: 0; 15 | width: 100%; 16 | height: 60px; /* Set the fixed height of the footer here */ 17 | line-height: 60px; /* Vertically center the text there */ 18 | background-color: #f5f5f5; 19 | } 20 | 21 | .footer a { 22 | color: #000; 23 | } 24 | 25 | .form-group textarea { 26 | margin: 0.5em 0px; 27 | } 28 | 29 | pre[class*="language-"]:before, 30 | pre[class*="language-"]:after { 31 | display: none; 32 | } 33 | -------------------------------------------------------------------------------- /static/custom.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | function replaceUrlParam(url, param, value) { 3 | if (value == null) { 4 | value = ''; 5 | } 6 | 7 | var pattern = new RegExp('\\b('+param+'=).*?(&|#|$)'); 8 | if (url.search(pattern)>=0) { 9 | return url.replace(pattern,'$1' + value + '$2'); 10 | } 11 | 12 | url = url.replace(/[?#]$/,''); 13 | return url + (url.indexOf('?')>0 ? '&' : '?') + param + '=' + value; 14 | } 15 | 16 | function resetLanguageSelector() { 17 | var url = new URL(document.location); 18 | var params = url.searchParams; 19 | var lang = params.get("lang"); 20 | 21 | if (lang != null) { 22 | $("#language-selector").val(lang); 23 | } else { 24 | if($("#pastebin-code-block").length) { 25 | $("#language-selector").val( 26 | $("#pastebin-code-block").prop("class").trim().split('-')[1] 27 | ); 28 | } 29 | } 30 | } 31 | 32 | function getDefaultExpiryTime() { 33 | var expiry = $("#expiry-dropdown-btn").text().split("Expires: ")[1]; 34 | return $("#expiry-dropdown a:contains('"+ expiry +"')").attr('href'); 35 | } 36 | 37 | function checkPasswordModal() { 38 | if ($("#password-modal").length) { 39 | $('#password-modal').modal('toggle'); 40 | } 41 | } 42 | 43 | resetLanguageSelector(); 44 | checkPasswordModal(); 45 | init_plugins(); 46 | 47 | var state = { 48 | expiry: getDefaultExpiryTime(), 49 | burn: 0, 50 | }; 51 | 52 | $("#language-selector").change(function() { 53 | if ($("#pastebin-code-block").length) { 54 | $('#pastebin-code-block').attr('class', 'language-' + $("#language-selector").val()); 55 | init_plugins(); 56 | } 57 | }); 58 | 59 | $("#remove-btn").on("click", function(event) { 60 | event.preventDefault(); 61 | $('#deletion-modal').modal('show'); 62 | }); 63 | 64 | $("#deletion-confirm-btn").on("click", function(event) { 65 | event.preventDefault(); 66 | 67 | $.ajax({ 68 | url: window.location.pathname, 69 | type: 'DELETE', 70 | success: function(result) { 71 | uri = uri_prefix + "/new"; 72 | uri = replaceUrlParam(uri, 'level', "info"); 73 | uri = replaceUrlParam(uri, 'glyph', "fas fa-info-circle"); 74 | uri = replaceUrlParam(uri, 'msg', "The paste has been successfully removed."); 75 | window.location.href = encodeURI(uri); 76 | } 77 | }); 78 | }); 79 | 80 | $("#copy-btn").on("click", function(event) { 81 | event.preventDefault(); 82 | 83 | $(".toolbar-item button").get(0).click(); 84 | 85 | var $this = $(this); 86 | $this.text("Copied!"); 87 | $this.attr("disabled", "disabled"); 88 | 89 | setTimeout(function() { 90 | $this.text("Copy"); 91 | $this.removeAttr("disabled"); 92 | }, 800); 93 | 94 | }); 95 | 96 | $("#send-btn").on("click", function(event) { 97 | event.preventDefault(); 98 | 99 | uri = uri_prefix == "" ? "/" : uri_prefix; 100 | uri = replaceUrlParam(uri, 'lang', $("#language-selector").val()); 101 | uri = replaceUrlParam(uri, 'ttl', state.expiry); 102 | uri = replaceUrlParam(uri, 'burn', state.burn); 103 | 104 | var data = $("#content-textarea").val(); 105 | var pass = $("#pastebin-password").val(); 106 | 107 | if ($("#pastebin-password").val().length > 0) { 108 | data = CryptoJS.AES.encrypt(data, pass).toString(); 109 | uri = replaceUrlParam(uri, 'encrypted', true); 110 | } 111 | 112 | $.ajax({ 113 | url: uri, 114 | type: 'POST', 115 | data: data, 116 | success: function(result) { 117 | uri = uri_prefix + "/new"; 118 | uri = replaceUrlParam(uri, 'level', "success"); 119 | uri = replaceUrlParam(uri, 'glyph', "fas fa-check"); 120 | uri = replaceUrlParam(uri, 'msg', "The paste has been successfully created:"); 121 | uri = replaceUrlParam(uri, 'url', result); 122 | 123 | window.location.href = encodeURI(uri); 124 | } 125 | }); 126 | }); 127 | 128 | $('#expiry-dropdown a').click(function(event){ 129 | event.preventDefault(); 130 | 131 | state.expiry = $(this).attr("href"); 132 | $('#expiry-dropdown-btn').text("Expires: " + this.innerHTML); 133 | }); 134 | 135 | $('#burn-dropdown a').click(function(event){ 136 | event.preventDefault(); 137 | 138 | state.burn = $(this).attr("href"); 139 | $('#burn-dropdown-btn').text("Burn: " + this.innerHTML); 140 | }); 141 | 142 | $('#password-modal').on('shown.bs.modal', function () { 143 | $('#modal-password').trigger('focus'); 144 | }) 145 | 146 | $('#password-modal form').submit(function(event) { 147 | event.preventDefault(); 148 | $('#decrypt-btn').click(); 149 | }); 150 | 151 | $('#decrypt-btn').click(function(event) { 152 | var pass = $("#modal-password").val(); 153 | var data = ""; 154 | 155 | if ($("#pastebin-code-block").length) { 156 | data = $("#pastebin-code-block").text(); 157 | } else { 158 | data = $("#content-textarea").text(); 159 | } 160 | 161 | var decrypted = CryptoJS.AES.decrypt(data, pass).toString(CryptoJS.enc.Utf8); 162 | if (decrypted.length == 0) { 163 | $("#modal-alert").removeClass("collapse"); 164 | } else { 165 | if ($("#pastebin-code-block").length) { 166 | $("#pastebin-code-block").text(decrypted); 167 | init_plugins(); 168 | } else { 169 | $("#content-textarea").text(decrypted); 170 | } 171 | 172 | $("#modal-close-btn").click(); 173 | $("#modal-alert").alert('close'); 174 | } 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaczanowski/pastebin/39758cab54377a9c5f60b7d795464a736b6ace47/static/favicon.ico -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Pastebin 10 | 11 | 12 | 13 | {{#each css_imports as |url|}} 14 | 15 | {{/each}} 16 | 17 | 18 | 19 | 232 | 233 |
234 | {{# if is_error}} 235 |
236 |
237 |
238 | 404 239 |
The page you are looking for was not found.
240 | Back to Home 241 |
242 |
243 |
244 | {{ else }} 245 | {{#if msg}} 246 | 253 | {{/if}} 254 | 255 | {{#if is_editable}} 256 |
257 | 258 |
259 | {{/if}} 260 | {{#if is_created or is_clone}} 261 |
{{pastebin_code}}
262 | {{/if}} 263 | {{/if}} 264 |
265 | 266 |
267 | - pastebin v{{version}} 268 |
269 | 270 | {{#if is_encrypted}} 271 | 301 | {{/if}} 302 | 303 | {{#if is_created}} 304 | 323 | {{/if}} 324 | 325 | 334 | {{#each js_imports as |url|}} 335 | 336 | {{/each}} 337 | 338 | 339 | -------------------------------------------------------------------------------- /static/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.23.0 2 | 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+aspnet+asm6502+autohotkey+autoit+bash+basic+batch+bbcode+bison+bnf+brainfuck+brightscript+bro+c+csharp+cpp+cil+clojure+cmake+coffeescript+concurnas+csp+crystal+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+gml+gcode+gdscript+gedcom+gherkin+git+glsl+go+graphql+groovy+haml+handlebars+haskell+haxe+hcl+http+hpkp+hsts+ichigojam+icon+inform7+ini+io+j+java+javadoc+javadoclike+javastacktrace+jolie+jq+jsdoc+js-extras+json+json5+jsonp+js-templates+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+reason+regex+renpy+rest+rip+roboconf+robotframework+ruby+rust+sas+sass+scss+scala+scheme+shell-session+smalltalk+smarty+solidity+solution-file+soy+sparql+splunk-spl+sqf+sql+iecst+stylus+swift+t4-templating+t4-cs+t4-vb+tap+tcl+tt2+textile+toml+turtle+twig+typescript+unrealscript+vala+vbnet+velocity+verilog+vhdl+vim+visual-basic+warpscript+wasm+wiki+xeora+xojo+xquery+yaml+zig&plugins=line-highlight+line-numbers+remove-initial-line-feed+toolbar+copy-to-clipboard+diff-highlight */ 3 | /** 4 | * prism.js Coy theme for JavaScript, CoffeeScript, CSS and HTML 5 | * 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); 6 | * @author Tim Shedor 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: black; 12 | background: none; 13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 14 | font-size: 1em; 15 | text-align: left; 16 | white-space: pre; 17 | word-spacing: normal; 18 | word-break: normal; 19 | word-wrap: normal; 20 | line-height: 1.5; 21 | 22 | -moz-tab-size: 4; 23 | -o-tab-size: 4; 24 | tab-size: 4; 25 | 26 | -webkit-hyphens: none; 27 | -moz-hyphens: none; 28 | -ms-hyphens: none; 29 | hyphens: none; 30 | } 31 | 32 | /* Code blocks */ 33 | pre[class*="language-"] { 34 | position: relative; 35 | margin: .5em 0; 36 | overflow: visible; 37 | padding: 0; 38 | } 39 | pre[class*="language-"]>code { 40 | position: relative; 41 | border-left: 10px solid #358ccb; 42 | box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf; 43 | background-color: #fdfdfd; 44 | background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%); 45 | background-size: 3em 3em; 46 | background-origin: content-box; 47 | background-attachment: local; 48 | } 49 | 50 | code[class*="language-"] { 51 | max-height: inherit; 52 | height: inherit; 53 | padding: 0 1em; 54 | display: block; 55 | overflow: auto; 56 | } 57 | 58 | /* Margin bottom to accommodate shadow */ 59 | :not(pre) > code[class*="language-"], 60 | pre[class*="language-"] { 61 | background-color: #fdfdfd; 62 | -webkit-box-sizing: border-box; 63 | -moz-box-sizing: border-box; 64 | box-sizing: border-box; 65 | margin-bottom: 1em; 66 | } 67 | 68 | /* Inline code */ 69 | :not(pre) > code[class*="language-"] { 70 | position: relative; 71 | padding: .2em; 72 | border-radius: 0.3em; 73 | color: #c92c2c; 74 | border: 1px solid rgba(0, 0, 0, 0.1); 75 | display: inline; 76 | white-space: normal; 77 | } 78 | 79 | pre[class*="language-"]:before, 80 | pre[class*="language-"]:after { 81 | content: ''; 82 | z-index: -2; 83 | display: block; 84 | position: absolute; 85 | bottom: 0.75em; 86 | left: 0.18em; 87 | width: 40%; 88 | height: 20%; 89 | max-height: 13em; 90 | box-shadow: 0px 13px 8px #979797; 91 | -webkit-transform: rotate(-2deg); 92 | -moz-transform: rotate(-2deg); 93 | -ms-transform: rotate(-2deg); 94 | -o-transform: rotate(-2deg); 95 | transform: rotate(-2deg); 96 | } 97 | 98 | pre[class*="language-"]:after { 99 | right: 0.75em; 100 | left: auto; 101 | -webkit-transform: rotate(2deg); 102 | -moz-transform: rotate(2deg); 103 | -ms-transform: rotate(2deg); 104 | -o-transform: rotate(2deg); 105 | transform: rotate(2deg); 106 | } 107 | 108 | .token.comment, 109 | .token.block-comment, 110 | .token.prolog, 111 | .token.doctype, 112 | .token.cdata { 113 | color: #7D8B99; 114 | } 115 | 116 | .token.punctuation { 117 | color: #5F6364; 118 | } 119 | 120 | .token.property, 121 | .token.tag, 122 | .token.boolean, 123 | .token.number, 124 | .token.function-name, 125 | .token.constant, 126 | .token.symbol, 127 | .token.deleted { 128 | color: #c92c2c; 129 | } 130 | 131 | .token.selector, 132 | .token.attr-name, 133 | .token.string, 134 | .token.char, 135 | .token.function, 136 | .token.builtin, 137 | .token.inserted { 138 | color: #2f9c0a; 139 | } 140 | 141 | .token.operator, 142 | .token.entity, 143 | .token.url, 144 | .token.variable { 145 | color: #a67f59; 146 | background: rgba(255, 255, 255, 0.5); 147 | } 148 | 149 | .token.atrule, 150 | .token.attr-value, 151 | .token.keyword, 152 | .token.class-name { 153 | color: #1990b8; 154 | } 155 | 156 | .token.regex, 157 | .token.important { 158 | color: #e90; 159 | } 160 | 161 | .language-css .token.string, 162 | .style .token.string { 163 | color: #a67f59; 164 | background: rgba(255, 255, 255, 0.5); 165 | } 166 | 167 | .token.important { 168 | font-weight: normal; 169 | } 170 | 171 | .token.bold { 172 | font-weight: bold; 173 | } 174 | .token.italic { 175 | font-style: italic; 176 | } 177 | 178 | .token.entity { 179 | cursor: help; 180 | } 181 | 182 | .token.namespace { 183 | opacity: .7; 184 | } 185 | 186 | @media screen and (max-width: 767px) { 187 | pre[class*="language-"]:before, 188 | pre[class*="language-"]:after { 189 | bottom: 14px; 190 | box-shadow: none; 191 | } 192 | 193 | } 194 | 195 | /* Plugin styles: Line Numbers */ 196 | pre[class*="language-"].line-numbers.line-numbers { 197 | padding-left: 0; 198 | } 199 | 200 | pre[class*="language-"].line-numbers.line-numbers code { 201 | padding-left: 3.8em; 202 | } 203 | 204 | pre[class*="language-"].line-numbers.line-numbers .line-numbers-rows { 205 | left: 0; 206 | } 207 | 208 | /* Plugin styles: Line Highlight */ 209 | pre[class*="language-"][data-line] { 210 | padding-top: 0; 211 | padding-bottom: 0; 212 | padding-left: 0; 213 | } 214 | pre[data-line] code { 215 | position: relative; 216 | padding-left: 4em; 217 | } 218 | pre .line-highlight { 219 | margin-top: 0; 220 | } 221 | 222 | pre[data-line] { 223 | position: relative; 224 | padding: 1em 0 1em 3em; 225 | } 226 | 227 | .line-highlight { 228 | position: absolute; 229 | left: 0; 230 | right: 0; 231 | padding: inherit 0; 232 | margin-top: 1em; /* Same as .prism’s padding-top */ 233 | 234 | background: hsla(24, 20%, 50%,.08); 235 | background: linear-gradient(to right, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0)); 236 | 237 | pointer-events: none; 238 | 239 | line-height: inherit; 240 | white-space: pre; 241 | } 242 | 243 | @media print { 244 | .line-highlight { 245 | /* 246 | * This will prevent browsers from replacing the background color with white. 247 | * It's necessary because the element is layered on top of the displayed code. 248 | */ 249 | -webkit-print-color-adjust: exact; 250 | color-adjust: exact; 251 | } 252 | } 253 | 254 | .line-highlight:before, 255 | .line-highlight[data-end]:after { 256 | content: attr(data-start); 257 | position: absolute; 258 | top: .4em; 259 | left: .6em; 260 | min-width: 1em; 261 | padding: 0 .5em; 262 | background-color: hsla(24, 20%, 50%,.4); 263 | color: hsl(24, 20%, 95%); 264 | font: bold 65%/1.5 sans-serif; 265 | text-align: center; 266 | vertical-align: .3em; 267 | border-radius: 999px; 268 | text-shadow: none; 269 | box-shadow: 0 1px white; 270 | } 271 | 272 | .line-highlight[data-end]:after { 273 | content: attr(data-end); 274 | top: auto; 275 | bottom: .4em; 276 | } 277 | 278 | .line-numbers .line-highlight:before, 279 | .line-numbers .line-highlight:after { 280 | content: none; 281 | } 282 | 283 | pre[id].linkable-line-numbers span.line-numbers-rows { 284 | pointer-events: all; 285 | } 286 | pre[id].linkable-line-numbers span.line-numbers-rows > span:before { 287 | cursor: pointer; 288 | } 289 | pre[id].linkable-line-numbers span.line-numbers-rows > span:hover:before { 290 | background-color: rgba(128, 128, 128, .2); 291 | } 292 | 293 | pre[class*="language-"].line-numbers { 294 | position: relative; 295 | padding-left: 3.8em; 296 | counter-reset: linenumber; 297 | } 298 | 299 | pre[class*="language-"].line-numbers > code { 300 | position: relative; 301 | white-space: inherit; 302 | } 303 | 304 | .line-numbers .line-numbers-rows { 305 | position: absolute; 306 | pointer-events: none; 307 | top: 0; 308 | font-size: 100%; 309 | left: -3.8em; 310 | width: 3em; /* works for line-numbers below 1000 lines */ 311 | letter-spacing: -1px; 312 | border-right: 1px solid #999; 313 | 314 | -webkit-user-select: none; 315 | -moz-user-select: none; 316 | -ms-user-select: none; 317 | user-select: none; 318 | 319 | } 320 | 321 | .line-numbers-rows > span { 322 | display: block; 323 | counter-increment: linenumber; 324 | } 325 | 326 | .line-numbers-rows > span:before { 327 | content: counter(linenumber); 328 | color: #999; 329 | display: block; 330 | padding-right: 0.8em; 331 | text-align: right; 332 | } 333 | 334 | div.code-toolbar { 335 | position: relative; 336 | } 337 | 338 | div.code-toolbar > .toolbar { 339 | position: absolute; 340 | top: .3em; 341 | right: .2em; 342 | transition: opacity 0.3s ease-in-out; 343 | opacity: 0; 344 | } 345 | 346 | div.code-toolbar:hover > .toolbar { 347 | opacity: 1; 348 | } 349 | 350 | /* Separate line b/c rules are thrown out if selector is invalid. 351 | IE11 and old Edge versions don't support :focus-within. */ 352 | div.code-toolbar:focus-within > .toolbar { 353 | opacity: 1; 354 | } 355 | 356 | div.code-toolbar > .toolbar .toolbar-item { 357 | display: inline-block; 358 | } 359 | 360 | div.code-toolbar > .toolbar a { 361 | cursor: pointer; 362 | } 363 | 364 | div.code-toolbar > .toolbar button { 365 | background: none; 366 | border: 0; 367 | color: inherit; 368 | font: inherit; 369 | line-height: normal; 370 | overflow: visible; 371 | padding: 0; 372 | -webkit-user-select: none; /* for button */ 373 | -moz-user-select: none; 374 | -ms-user-select: none; 375 | } 376 | 377 | div.code-toolbar > .toolbar a, 378 | div.code-toolbar > .toolbar button, 379 | div.code-toolbar > .toolbar span { 380 | color: #bbb; 381 | font-size: .8em; 382 | padding: 0 .5em; 383 | background: #f5f2f0; 384 | background: rgba(224, 224, 224, 0.2); 385 | box-shadow: 0 2px 0 0 rgba(0,0,0,0.2); 386 | border-radius: .5em; 387 | } 388 | 389 | div.code-toolbar > .toolbar a:hover, 390 | div.code-toolbar > .toolbar a:focus, 391 | div.code-toolbar > .toolbar button:hover, 392 | div.code-toolbar > .toolbar button:focus, 393 | div.code-toolbar > .toolbar span:hover, 394 | div.code-toolbar > .toolbar span:focus { 395 | color: inherit; 396 | text-decoration: none; 397 | } 398 | 399 | pre.diff-highlight > code .token.deleted:not(.prefix), 400 | pre > code.diff-highlight .token.deleted:not(.prefix) { 401 | background-color: rgba(255, 0, 0, .1); 402 | color: inherit; 403 | display: block; 404 | } 405 | 406 | pre.diff-highlight > code .token.inserted:not(.prefix), 407 | pre > code.diff-highlight .token.inserted:not(.prefix) { 408 | background-color: rgba(0, 255, 128, .1); 409 | color: inherit; 410 | display: block; 411 | } 412 | 413 | --------------------------------------------------------------------------------