Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
salvo = { version= "0.41" }
tokio = { version = "1", features = ["macros"] }
salvo = { version = "0.44", features = ["logging"] }
miette = { version = "5.9.0", features = ["fancy"]}
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
serde = { version = "1.0" }
serde_json = "1.0"
reqwest = "0.11.18"
once_cell = "1.17.1"
once_cell = "1.17.1"
thiserror = "1.0.40"
tracing-subscriber = "0.3.17"
cfg-if = "1.0.0"

[features]
# Defines a feature named `webp` that does not enable any other features.
v2 = []
45 changes: 39 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,49 @@
bare-server-rust is a fully compliant Rust implementation of [TompHTTPs' Bare Server specifications](https://github.com/tomphttp/specifications/blob/master/BareServer.md).
This is a server that will receive requests from a service worker (or any client) and forward a request to a specified URL.

# How to use
## Using
TODO: Release builds to docker, create simple install script.

## Contributing
All support and contributions to `bare-server-rust` are appreciated. Including, but not limited to: documentation changes, bugfixes, feature requests, and performance improvements.

### How do I get started?

### Before we start
A quick note before we start, we use unsatable features for rustfmt, requiring the +nightly flag. Make sure you install a nightly toolchain as well.

You can install the nightly rust toolchain with the following `rustup` command.
```
rustup toolchain install nightly
```
git clone https://github.com/NebulaServices/bare-server-rust

cd bare-server-rust
### Installing `rustup`
As with any rust project, you will need to install cargo, rust, and other dependencies. The easiest way to do this is with `rustup`.

cargo build && cargo run
If you are an a Unix based system, or intend to use Windows Subsystem for Linux to develop, you should run the `rustup` installer script below.
```
## To-do
* Websocket support
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
Otherwise, please visit the [`Rustup Website`](https://rustup.rs/) to download it.

After you've finished with downloading rustup, be sure to install the nightly toolchain as shown [above](#before-we-start).
### Building from Sources
If you haven't done so already, you'll need to download the repository using git.
```
git clone [email protected]:NebulaServices/bare-server-rust.git
```
After you've download that, its time to go in and get to work. All you need to do is `cd` into the repository like so:
```
cd bare-server-rust
```
Afterwords, you can either build or run using the respective cargo commands.
```
cargo run
cargo build
```
As an example, to build the `release` profile with the `v2` feature enabled, you can do it like so:
```
cargo run --features v2 --release
```
## Authors
* [UndefinedBHVR](https://github.com/UndefinedBHVR)
3 changes: 3 additions & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# NOTE: Wrapping comments required the nightly toolchain
# A saddening amount of rustfmt features are locked on nightly
# despite being more or less stable.
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
use_field_init_shorthand = true
wrap_comments = true
126 changes: 126 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use miette::{Diagnostic, Report as StackReport};
use reqwest::StatusCode;
use salvo::{
async_trait, Depot, Request, Response, Writer,
__private::tracing::{self},
writer::Json,
};
use serde_json::{json, Value};
use thiserror::Error;
pub type Result<T> = core::result::Result<T, ErrorWithContext>;

pub struct ErrorWithContext {
error: Error,
context: String,
}

impl ErrorWithContext {
pub fn new<T: Into<String>>(error: Error, context: T) -> Self {
Self {
error,
context: context.into(),
}
}

pub fn to_report(&self) -> miette::Report {
miette::Report::new(self.error.to_owned()).wrap_err(self.context.to_owned())
}
}

#[async_trait]
impl Writer for ErrorWithContext {
async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
match self.error {
Error::Unknown
| Error::HostNotFound
| Error::ConnectionReset
| Error::ConnectionRefused
| Error::Generic(_)
| Error::ConnectionTimeout => res.status_code(StatusCode::INTERNAL_SERVER_ERROR),
Error::MissingBareHeader(_)
| Error::InvalidBareHeader(_)
| Error::UnknownBareHeader(_)
| Error::InvalidHeader(_) => res.status_code(StatusCode::BAD_REQUEST),
Error::ForbiddenBareHeader(_) => res.status_code(StatusCode::FORBIDDEN),
};
let report = self.to_report();
tracing::error!("\n {report:?}");
res.render(Json(self.error.to_json()));
}
}

#[derive(Debug, Diagnostic, Error, Clone)]
#[error("oops!")]
pub enum Error {
#[error("The Bare Server could not identify the cause of the issue")]
#[diagnostic(code(UNKNOWN))]
Unknown,
#[error("The request did not include the required header {0}")]
#[diagnostic(code(MISSING_BARE_HEADER))]
MissingBareHeader(String),
#[error("Received an unrecognizable header value: {0}")]
#[diagnostic(code(INVALID_BARE_HEADER))]
InvalidBareHeader(String),
#[error("Received a forbidden header value: {0}")]
#[diagnostic(code(FORBIDDEN_BARE_HEADER))]
ForbiddenBareHeader(String),
// NOTE: This is unused, checking for unknown headers is a waste of compute.
// I may gate this behind a feature flag at a later date.
#[error("Received unknown bare header {0}")]
#[diagnostic(code(UNKNOWN_BARE_HEADER))]
UnknownBareHeader(String),
// Why does this exist? This is a duplicate of InvalidBareHeader...
#[error("Received a blacklisted header value: {0}")]
#[diagnostic(code(INVALID_HEADER))]
InvalidHeader(String),
#[error("The DNS lookup for the host failed.")]
#[diagnostic(code(HOST_NOT_FOUND))]
HostNotFound,
#[error("The connection to the remote was closed early.")]
#[diagnostic(code(CONNECTION_RESET))]
ConnectionReset,
#[error("The connection to the remote was refused.")]
#[diagnostic(code(CONNECTION_REFUSED))]
ConnectionRefused,
#[error("The remote didn't respond with headers/body in time.")]
#[diagnostic(code(CONNECTION_TIMEOUT))]
ConnectionTimeout,
#[error("{0}")]
#[diagnostic(code(UNKNOWN))]
Generic(String),
}

impl Error {
pub fn to_json(&self) -> Value {
let id: String = match self {
Error::Unknown => "unknown".into(),
Error::MissingBareHeader(header) | Error::InvalidBareHeader(header) => {
format!("request.headers.{}", header.to_lowercase())
}
Error::ForbiddenBareHeader(header) => format!("error.temp.forbidden_header.{header}"),
Error::UnknownBareHeader(header) => format!("error.temp.unknown_bare_header.{header}"),
Error::InvalidHeader(header) => format!("error.temp.invalid_header.{header}"),
Error::HostNotFound => "error.http.not_found".to_string(),
Error::ConnectionReset => "error.http.reset".to_string(),
Error::ConnectionRefused => "error.http.refused".to_string(),
Error::ConnectionTimeout => "error.http.timeout".to_string(),
Error::Generic(kind) => format!("request.tomphttp-rs.{kind}"),
};

json!({
"code": format!("{}", self.code().expect("This should always be defined.")),
"id": id,
"message": format!("{self}")

})
}
}

#[async_trait]
impl Writer for Error {
async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
let report: StackReport = self.into();
tracing::error!("\n {report:?}");
res.render(format!("{report:?}"));
}
}
50 changes: 44 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,61 @@
use error::{Error, ErrorWithContext};
use reqwest::Client;
use salvo::prelude::*;
use salvo::{__private::tracing, prelude::*};
use util::REQWEST_CLIENT;
use v3::add_cors_headers_route;
//use util::REQWEST_CLIENT;
use version::VersionData;
#[macro_use]
extern crate cfg_if;
mod v3;

pub mod routes;
pub mod error;
pub mod util;
pub mod version;

#[handler]
async fn versions(res: &mut Response) {
res.render(Json(VersionData::default()));
async fn versions() -> Json<VersionData> {
Json(VersionData::default())
}

#[handler]
async fn error_test() -> Result<(), ErrorWithContext> {
let report = Err(ErrorWithContext::new(
Error::Unknown,
"While testing errors.",
));
report?
}

#[tokio::main]
async fn main() {
REQWEST_CLIENT
.set(Client::new())
.expect("This should never error");
tracing_subscriber::fmt::init();

let acceptor = TcpListener::new("127.0.0.1:5800").bind().await;
Server::new(acceptor).serve(routes::built_routes()).await;
// Compiler will complain.
#[allow(unused_mut)]
let mut app = Router::new()
//.hoop(Logger::new())
.hoop(add_cors_headers_route)
.get(versions)
.push(
Router::with_path("v3")
.hoop(crate::v3::process_headers)
.handle(crate::v3::fetch),
)
.push(Router::with_path("error").get(error_test));
cfg_if! {
if #[cfg(feature = "v2")] {
tracing::info!("server configured to run with the `v2` feature enabled.");
app = app.push(
Router::with_path("v2")
.hoop(crate::v3::process_headers)
.handle(crate::v3::fetch),
);
}
}
let server = TcpListener::new("127.0.0.1:5800").bind().await;
Server::new(server).serve(app).await;
}
Loading