-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a828614
Showing
26 changed files
with
3,020 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
target | ||
.cargo | ||
*.sh | ||
*.tar.gz | ||
Paste.toml | ||
upload/**/* | ||
!upload/README |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
[package] | ||
name = "rktpb" | ||
version = "1.0.0" | ||
authors = ["Sergio Benitez <[email protected]>"] | ||
edition = "2021" | ||
description = "A pastebin that does just enough to be really useful." | ||
repository = "https://github.com/SergioBenitez/rktpb" | ||
readme = "README.md" | ||
keywords = ["pastebin", "rocket", "server", "markdown", "highlight"] | ||
license = "AGPL-3.0-only" | ||
categories = ["web-programming::http-server"] | ||
|
||
[lints.clippy] | ||
large_enum_variant = "allow" | ||
mutable_key_type = "allow" | ||
|
||
[dependencies] | ||
rocket = "0.5" | ||
rocket_dyn_templates = { version = "0.1", features = ["tera"] } | ||
yansi = "1.0.0-rc" | ||
rand = "0.8" | ||
syntect = "5" | ||
comrak = "0.19" | ||
futures = "0.3" | ||
humantime = "2.1" | ||
humantime-serde = "1.1" |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
[default] # default configuration irrespective of compilation mode | ||
id_length = 3 # paste ID length | ||
upload_dir = "upload" # directory to save uploads in | ||
paste_limit = "384KiB" # maximum paste upload file size | ||
max_age = "30 days" # how long a paste is considered fresh | ||
reap_interval = "5 minutes" # how often to reap stale uploads | ||
server_url = "http://127.0.0.1:8000" # URL server is reachable at | ||
address = "127.0.0.1" # address to listen on | ||
port = 8000 # port to listen on | ||
keep_alive = 5 # HTTP keep-alive in seconds | ||
ident = "Rocket" # server `Ident` header | ||
ip_header = "X-Real-IP" # header to inspect for client IP | ||
log_level = "normal" # console log level | ||
cli_colors = true # enable (detect TTY) or disable CLI colors | ||
|
||
[default.cors] # CORS config - one key/value for each allowed host | ||
"https://example.com" = ["options", "get", "post", "delete"] # methods to allow | ||
|
||
[default.shutdown] # settings for graceful shutdown | ||
ctrlc = true # whether `<ctrl-c>` initiates a shutdown | ||
signals = ["term", "hup"] # signals that initiate a shutdown | ||
grace = 5 # grace period length in seconds | ||
mercy = 5 # mercy period length in seconds | ||
|
||
[debug] # overrides for when application is compiled in debug mode | ||
# key/values are identical to `default` | ||
|
||
[release] # overrides for when application is compiled in release mode | ||
# key/values are identical to `default` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
# Rocket Powered Pastebin (`rktpb` | [`paste.rs`]) | ||
|
||
A pastebin that does just enough to be _really_ useful. | ||
|
||
- [x] Really fast, really lightweight. | ||
- [x] Renders _markdown_ like GitHub. | ||
- [x] Highlights `source code`. | ||
- [x] Returns plain text, too. | ||
- [x] Has a simple API usable via CLI. | ||
- [x] Has support for CORS. | ||
- [x] Limits paste upload sizes. | ||
- [x] No database: uses the file system. | ||
- [x] Automatically deletes stale pastes. | ||
|
||
This pastebin powers [`paste.rs`], a public instance. Further usage details can | ||
be found there. | ||
|
||
[`paste.rs`]: https://paste.rs | ||
|
||
## Usage | ||
|
||
**R**ocket **P**owered Paste**b**in (`rktpb`) is written in | ||
[Rust](https://rust-lang.org) with [Rocket](https://rocket.rs). To start the | ||
server, use `cargo`: | ||
|
||
```sh | ||
# clone the repository | ||
git clone https://github.com/SergioBenitez/rktpb | ||
|
||
# change into directory: the `static` folder needs to be in CWD before running | ||
cd rktpb | ||
|
||
# compile and start the server with the default config | ||
cargo run | ||
``` | ||
|
||
## Configuration | ||
|
||
Configuration is provided via [environment variables](#environment-variables) or | ||
a [TOML file](#toml-file). A set of defaults is always provided. | ||
|
||
The complete list of configurable parameters is below: | ||
|
||
| Name | Default Value | Description | | ||
|--------------------|-----------------------------|-------------------------------------------| | ||
| `id_length` | `3` | paste ID length | ||
| `upload_dir` | `"upload"` | directory to save uploads in | | ||
| `paste_limit` | `"384KiB"` | maximum paste upload file size | | ||
| `max_age` | `"30 days"` | how long a paste is considered fresh | | ||
| `reap_interval` | `"5 minutes"` | how often to reap stale uploads | | ||
| `server_url` | `"http://{address}:{port}"` | URL server is reachable at | | ||
| | | | | ||
| `cors.{host}` | `["{HTTP method}"..]` | allow CORS {HTTP methods} for {host} | | ||
| | | | | ||
| `address` | `"127.0.0.1"` | address to listen on | | ||
| `port` | `8000` | port to listen on | | ||
| `keep_alive` | `5` | HTTP keep-alive in seconds | | ||
| `ident` | `"Rocket"` | server `Ident` header | | ||
| `ip_header` | `"X-Real-IP"` | header to inspect for client IP | | ||
| `log_level` | `"normal"` | console log level | | ||
| `cli_colors` | `true` | enable (detect TTY) or disable CLI colors | | ||
| | | | | ||
| `shutdown.ctrlc` | `true` | whether `<ctrl-c>` initiates a shutdown | | ||
| `shutdown.signals` | `["term", "hup"]` | signals that initiate a shutdown | | ||
| `shutdown.grace` | `5` | grace period length in seconds | | ||
| `shutdown.mercy` | `5` | mercy period length in seconds | | ||
|
||
You'll definitely want to configure the values in the first two categories, from | ||
`id_length` to `cors`. | ||
|
||
You should likely use the defaults for the rest. | ||
|
||
### Environment Variables | ||
|
||
Use an environment variable name equivalent to the parameter name prefixed with | ||
`PASTE_`: | ||
|
||
```sh | ||
PASTE_ID_LENGTH=10 PASTE_MAX_AGE="1 year" ./rktpb | ||
``` | ||
|
||
To set structured data via environment variables, such as CORS, use [TOML-like | ||
syntax](https://docs.rs/figment/latest/figment/providers/struct.Env.html): | ||
|
||
```sh | ||
PASTE_CORS='{"http://example.com"=["get", "post"]}' ./rktpb | ||
``` | ||
|
||
### TOML File | ||
|
||
See [`Paste.toml.template`](Paste.toml.template) for a template with all of the | ||
defaults set as well as a dummy `cors` configuration for `http://example.com` | ||
that allows the `options`, `get`, `post`, and `delete` HTTP methods. | ||
|
||
```sh | ||
mv Paste.toml.template Paste.toml | ||
``` | ||
|
||
By default, the application searches for a file called `Paste.toml` in the CWD. | ||
The path to the file can be overridden by setting `PASTE_CONFIG`. For example, | ||
to use a file named `rktpb.toml`, use `PASTE_CONFIG="rktpb.toml" ./rktpb`. | ||
|
||
## Deploying | ||
|
||
To deploy, build in release mode and ship/run the resulting binary along with | ||
`static/`, `templates/`, and any config: | ||
|
||
```sh | ||
# build in release mode for `${TARGET}` | ||
cargo build --release --target ${TARGET} | ||
|
||
# create a tarball of everything that's needed | ||
tar -cvzf "rktpb.tar.gz" \ | ||
Paste.toml static templates \ | ||
-C target/${TARGET}/release rktpb | ||
``` | ||
|
||
However you choose to deploy, you'll need to ensure that the CWD at the time the | ||
server is started contains the `static` and `templates` directories as well as | ||
the config file, if one is used. | ||
|
||
Note that when the server is compiled in `release` mode, the `[release]` section | ||
of a TOML config file can be used to override config values; the same is true | ||
when compiled in `debug` mode with `[debug]`. | ||
|
||
## License | ||
|
||
Rocket Powered Pastebin (`rktpb` | [`paste.rs`]) | ||
Copyright © 2020 Sergio Benitez | ||
|
||
This program is free software: you can redistribute it and/or modify it under | ||
the terms of the [GNU Affero General Public License version 3 (GNU AGPLv3) as | ||
published by the Free Software | ||
Foundation](https://www.gnu.org/licenses/agpl-3.0.en.html#license-text). This | ||
program is distributed in the hope that it will be useful, but **WITHOUT ANY | ||
WARRANTY**; without even the implied warranty of MERCHANTABILITY or FITNESS FOR | ||
A PARTICULAR PURPOSE. See the GNU AGPLv3 [LICENSE](LICENSE) for more details. | ||
|
||
Unless you explicitly state otherwise, any contribution intentionally submitted | ||
for inclusion in this project shall be licensed under the GNU AGPLv3 License, | ||
without any additional terms or conditions. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
use rocket::figment::{Figment, Profile}; | ||
use rocket::figment::providers::{Format, Toml, Serialized, Env}; | ||
use rocket::figment::value::magic::RelativePathBuf; | ||
use rocket::serde::{de, Deserialize, Serialize}; | ||
|
||
use rocket::{Sentinel, Rocket, Ignite, Request}; | ||
use rocket::data::{ByteUnit, ToByteUnit, Limits}; | ||
use rocket::http::{Status, uri::Absolute}; | ||
use rocket::request::{FromRequest, Outcome}; | ||
use rocket::outcome::IntoOutcome; | ||
|
||
use yansi::Paint; | ||
|
||
#[derive(Debug, Deserialize, Serialize)] | ||
#[serde(crate = "rocket::serde")] | ||
pub struct Config { | ||
pub id_length: usize, | ||
pub paste_limit: ByteUnit, | ||
pub server_url: Absolute<'static>, | ||
#[serde(deserialize_with = "directory")] | ||
#[serde(serialize_with = "RelativePathBuf::serialize_original")] | ||
pub upload_dir: RelativePathBuf, | ||
} | ||
|
||
impl Config { | ||
pub fn figment() -> Figment { | ||
#[cfg(debug_assertions)] const DEFAULT_PROFILE: &str = "debug"; | ||
#[cfg(not(debug_assertions))] const DEFAULT_PROFILE: &str = "release"; | ||
|
||
// This the base figment, without our `Config` defaults. | ||
let mut figment = Figment::new() | ||
.join(rocket::Config::default()) | ||
.merge(Toml::file(Env::var_or("PASTE_CONFIG", "Paste.toml")).nested()) | ||
.merge(Env::prefixed("PASTE_").profile(Profile::Global)) | ||
.select(Profile::from_env_or("PASTE_PROFILE", DEFAULT_PROFILE)); | ||
|
||
// Dynamically determine `server_url` default based on address/port. | ||
let default_server_url = match figment.extract::<rocket::Config>() { | ||
Ok(config) => { | ||
let proto = if config.tls_enabled() { "https" } else { "http" }; | ||
let url = format!("{}://{}:{}", proto, config.address, config.port); | ||
Absolute::parse_owned(url).expect("default URL is Absolute") | ||
}, | ||
Err(_) => uri!("http://127.0.0.1:8017"), | ||
}; | ||
|
||
// Now set the `Config` defaults. | ||
figment = figment | ||
.join(Serialized::defaults(Config { | ||
id_length: 3, | ||
paste_limit: 384.kibibytes(), | ||
server_url: default_server_url, | ||
upload_dir: "upload".into(), | ||
})); | ||
|
||
// Configure Rocket based on `Config` settings. If this fails now, it's | ||
// fine - it'll fail when attached too, so we won't miss out. | ||
if let Ok(config) = figment.extract::<Self>() { | ||
figment = figment | ||
.merge((rocket::Config::TEMP_DIR, config.upload_dir)) | ||
.merge((rocket::Config::LIMITS, Limits::default() | ||
.limit("form", config.paste_limit) | ||
.limit("data-form", config.paste_limit) | ||
.limit("file", config.paste_limit) | ||
.limit("string", config.paste_limit) | ||
.limit("bytes", config.paste_limit) | ||
.limit("json", config.paste_limit) | ||
.limit("msgpack", config.paste_limit) | ||
.limit("paste", config.paste_limit), | ||
)); | ||
} | ||
|
||
figment | ||
} | ||
} | ||
|
||
impl Sentinel for Config { | ||
fn abort(rocket: &Rocket<Ignite>) -> bool { | ||
rocket.state::<Self>().is_none() | ||
} | ||
} | ||
|
||
#[rocket::async_trait] | ||
impl<'r> FromRequest<'r> for &'r Config { | ||
type Error = (); | ||
|
||
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> { | ||
req.rocket().state::<Config>().or_error((Status::InternalServerError, ())) | ||
} | ||
} | ||
|
||
fn directory<'de, D: de::Deserializer<'de>>(de: D) -> Result<RelativePathBuf, D::Error> { | ||
let path = RelativePathBuf::deserialize(de)?; | ||
let resolved = path.relative(); | ||
if !resolved.exists() { | ||
let path = resolved.display(); | ||
return Err(de::Error::custom(format!("Path {} does not exist.", path.primary()))); | ||
} | ||
|
||
if !resolved.is_dir() { | ||
let path = resolved.display(); | ||
return Err(de::Error::custom(format!("Path {} is not a directory.", path.primary()))); | ||
} | ||
|
||
Ok(path) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
use std::io::Cursor; | ||
use std::collections::HashMap; | ||
|
||
use rocket::{Rocket, Request, Response, Orbit}; | ||
use rocket::fairing::{AdHoc, Fairing, Info, Kind}; | ||
use rocket::http::{Status, Header, Method, uri::Absolute}; | ||
use rocket::serde::{Serialize, Deserialize}; | ||
|
||
#[derive(Debug, Serialize, Deserialize)] | ||
#[serde(crate = "rocket::serde")] | ||
pub struct Cors { | ||
#[serde(default)] | ||
cors: HashMap<Absolute<'static>, Vec<Method>>, | ||
} | ||
|
||
impl Cors { | ||
pub fn fairing() -> impl Fairing { | ||
AdHoc::try_on_ignite("CORS Configuration", |rocket| async { | ||
match rocket.figment().extract::<Cors>() { | ||
Ok(cors) => Ok(rocket.attach(cors)), | ||
Err(e) => { | ||
let kind = rocket::error::ErrorKind::Config(e); | ||
rocket::Error::from(kind).pretty_print(); | ||
Err(rocket) | ||
}, | ||
} | ||
}) | ||
} | ||
} | ||
|
||
#[rocket::async_trait] | ||
impl Fairing for Cors { | ||
fn info(&self) -> Info { | ||
Info { name: "CORS", kind: Kind::Liftoff | Kind::Response } | ||
} | ||
|
||
async fn on_liftoff(&self, _rocket: &Rocket<Orbit>) { | ||
use yansi::Paint; | ||
|
||
info!("{}{}", "📫 ".mask(), "CORS:".magenta()); | ||
if self.cors.is_empty() { | ||
return info_!("status: {}", "disabled".red()); | ||
} | ||
|
||
info_!("status: {}", "enabled".green()); | ||
for (host, methods) in &self.cors { | ||
info_!("{}: {:?}", host.magenta(), methods.primary()); | ||
} | ||
} | ||
|
||
async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) { | ||
let allowed_host_methods = req.headers().get_one("Origin") | ||
.and_then(|origin| Absolute::parse(origin).ok()) | ||
.and_then(|host| self.cors.get_key_value(&host)) | ||
.filter(|(_, methods)| methods.contains(&req.method())); | ||
|
||
if let Some((host, methods)) = allowed_host_methods { | ||
const ALLOW_ORIGIN: &str = "Access-Control-Allow-Origin"; | ||
const ALLOW_METHODS: &str = "Access-Control-Allow-Methods"; | ||
const ALLOW_HEADERS: &str = "Access-Control-Allow-Headers"; | ||
|
||
let mut allow_methods = String::with_capacity(methods.len() * 8); | ||
for (i, method) in methods.iter().enumerate() { | ||
if i != 0 { allow_methods.push(','); } | ||
allow_methods.push_str(method.as_str()); | ||
} | ||
|
||
res.set_header(Header::new(ALLOW_ORIGIN, host.to_string())); | ||
res.set_header(Header::new(ALLOW_METHODS, allow_methods)); | ||
res.set_header(Header::new(ALLOW_HEADERS, "Content-Type")); | ||
|
||
if req.method() == Method::Options && res.status() == Status::NotFound { | ||
res.set_status(Status::Ok); | ||
res.set_sized_body(0, Cursor::new("")); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.