aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2022-04-01 01:59:13 +0000
committerFuwn <[email protected]>2022-04-01 01:59:13 +0000
commita36a9c15900be9f2aeb15249595cd81d5c35f9e4 (patch)
tree1df8260acc1feb9cbcbe9628873f45e7aa8c5ac7
parentdocs(license): add license (diff)
downloadseptember-a36a9c15900be9f2aeb15249595cd81d5c35f9e4.tar.xz
september-a36a9c15900be9f2aeb15249595cd81d5c35f9e4.zip
feat: 0.0.0 :star:
-rw-r--r--.dockerignore10
-rw-r--r--.env.example6
-rw-r--r--.github/workflows/check.yaml35
-rw-r--r--.gitignore10
-rw-r--r--.license_template17
-rw-r--r--Cargo.toml42
-rw-r--r--Dockerfile24
-rw-r--r--Makefile.toml36
-rw-r--r--README.md130
-rw-r--r--build.rs19
-rw-r--r--docker-compose.yaml11
-rw-r--r--rust-toolchain.toml2
-rw-r--r--rustfmt.toml30
-rw-r--r--src/main.rs325
14 files changed, 697 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..ed926f1
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,10 @@
+# Rust
+target/
+**/*.rs.bk
+Cargo.lock
+
+# IDE
+.idea/
+
+# Development
+.env
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..75bcb55
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,6 @@
+PORT=8080
+ROOT=gemini://fuwn.me
+CSS_EXTERNAL=https://example.com/style.css
+KEEP_GEMINI_EXACT=gemini://fuwn.me/skills
+# KEEP_GEMINI_DOMAIN=fuwn.me
+# PROXY_BY_DEFAULT=true
diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml
new file mode 100644
index 0000000..00d8c34
--- /dev/null
+++ b/.github/workflows/check.yaml
@@ -0,0 +1,35 @@
+name: Check ✅
+
+on:
+ workflow_dispatch:
+ push:
+ paths:
+ - "*"
+ pull_request:
+ paths:
+ - "*"
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout 🛒
+ uses: actions/checkout@v3
+
+ - name: Toolchain 🧰
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: nightly-2022-03-08
+ components: rustfmt, clippy
+ override: true
+
+ - name: Check ✅
+ uses: actions-rs/cargo@v1
+ continue-on-error: false
+ with:
+ command: check
+ args: --verbose
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ed926f1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+# Rust
+target/
+**/*.rs.bk
+Cargo.lock
+
+# IDE
+.idea/
+
+# Development
+.env
diff --git a/.license_template b/.license_template
new file mode 100644
index 0000000..c3c7133
--- /dev/null
+++ b/.license_template
@@ -0,0 +1,17 @@
+// This file is part of September <https://github.com/gemrest/september>.
+// Copyright (C) {20\d{2}(-20\d{2})?} Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// 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
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) {20\d{2}(-20\d{2})?} Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..3e29e5f
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,42 @@
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[package]
+name = "september"
+version = "0.0.0"
+authors = ["Fuwn <[email protected]>"]
+edition = "2021"
+description = "A simple and efficient Gemini-to-HTTP proxy."
+readme = "README.rst"
+homepage = "https://github.com/gemrest/september"
+repository = "https://github.com/gemrest/september"
+license = "GPL-3.0-only"
+keywords = ["rust", "gemini", "proxy"]
+categories = ["web-programming", "web-programming::http-server"]
+publish = false
+
+# Slower builds, faster executables
+[profile.release]
+lto = "fat"
+codegen-units = 1
+opt-level = 3
+
+[dependencies]
+# Gemini
+gmi = "0.2.1"
+
+# HTTP
+actix-web = "4.0.1"
+
+# Logging
+pretty_env_logger = "0.4.0"
+log = "0.4.16"
+
+# Environment Variables
+dotenv = "0.15.0"
+
+[build-dependencies]
+# Environment Variables
+vergen = "7.0.0"
+
+# `Result`
+anyhow = "1.0.56"
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..1f6cb13
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,24 @@
+FROM rust:latest as builder
+
+RUN update-ca-certificates
+
+WORKDIR /september
+
+COPY ./ ./
+
+RUN cargo build --release
+
+RUN strip -s /september/target/release/september
+
+FROM debian:buster-slim
+
+COPY --from=builder /etc/passwd /etc/passwd
+COPY --from=builder /etc/group /etc/group
+
+WORKDIR /september
+
+COPY --from=builder /september/target/release/september ./
+
+EXPOSE 80
+
+CMD ["./september"]
diff --git a/Makefile.toml b/Makefile.toml
new file mode 100644
index 0000000..60b05a7
--- /dev/null
+++ b/Makefile.toml
@@ -0,0 +1,36 @@
+# ------------
+# | Wrappers |
+# ------------
+[tasks.fmt]
+command = "cargo"
+args = ["fmt"]
+private = true
+
+[tasks.check]
+command = "cargo"
+args = ["check"]
+private = true
+
+[tasks.clippy]
+command = "cargo"
+args = ["clippy"]
+private = true
+
+# -------------
+# | Executors |
+# -------------
+[tasks.checkf]
+dependencies = ["fmt", "check"]
+
+[tasks.checkfc]
+dependencies = ["fmt", "check", "clippy"]
+
+[tasks.run]
+dependencies = ["checkfc"]
+command = "cargo"
+args = ["run"]
+
+[tasks.docker-build]
+dependencies = ["checkfc"]
+command = "docker"
+args = ["build", "-t", "fuwn/september", "."]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..91ea929
--- /dev/null
+++ b/README.md
@@ -0,0 +1,130 @@
+# September
+
+[![github.com](https://github.com/gemrest/september/actions/workflows/check.yaml/badge.svg?branch=main)](https://github.com/gemrest/september/actions/workflows/check.yaml)
+
+A simple and efficient Gemini-to-HTTP proxy written in Rust.
+
+## Usage
+
+### Docker
+
+```shell
+$ docker run -d [ -e ROOT="gemini://fuwn.me" ] [ -e PORT="8080"] [ -e CSS_EXTERNAL="https://example.com/style.css"] fuwn/september:latest
+```
+
+### Docker Compose
+
+Edit the `docker-compose.yaml` file to your liking, and then
+
+```shell
+$ docker-compose up -d
+```
+
+### Executable
+
+```shell
+$ [ ROOT="gemini://fuwn.me" ] [ PORT="8080"] [ CSS_EXTERNAL="https://example.com/style.css"] ./september
+```
+
+or use a `.env` file
+
+```dotenv
+# .env
+
+ROOT=gemini://fuwn.me
+PORT=8080
+CSS_EXTERNAL=https://example.com/style.css
+```
+
+and then
+
+```shell
+$ ./september
+```
+
+## Configuration
+
+Configuration for September is done solely via environment variables, for
+simplicity, and Docker support.
+
+### `PORT`
+
+Bind September to a custom port.
+
+Generally, you shouldn't touch this if you are deploying using Docker.
+
+If no `PORT` is provided or the `PORT` could not be properly parsed as a `u16`;
+port `80` will be assumed.
+
+```dotenv
+PORT=8080
+```
+
+### `ROOT`
+
+The root Gemini capsule to proxy when not visiting a "/proxy" route.
+
+If no `ROOT` is provided, `"gemini://fuwn.me"` will be assumed.
+
+```dotenv
+ROOT=gemini://fuwn.me
+```
+
+### `CSS_EXTERNAL`
+
+An external CSS file to apply to the HTML response.
+
+If no `CSS_EXTERNAL` is provided, there will be no styling done to the HTML
+response.
+
+```dotenv
+CSS_EXTERNAL=https://cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.1/mini-default.min.css
+```
+
+### `KEEP_GEMINI_EXACT`
+
+Keeps exactly matching URLs as a Gemini URL.
+
+#### Examples
+
+If `KEEP_GEMINI_EXACT` is equal to `KEEP_GEMINI_EXACT=gemini://fuwn.me/gemini`,
+all routes will be proxied their "/proxy" equivalent (e.g.,
+"https://fuwn.me/proxy/fuwn.me/gopher"), except occurrences of
+"gemini://fuwn.me/skills" will be kept as is.
+
+```dotenv
+KEEP_GEMINI_EXACT=gemini://fuwn.me/skills
+```
+
+### `KEEP_GEMINI_DOMAIN`
+
+Similar to `KEEP_GEMINI_EXACT`, except proxies based on entire domains instead
+of exact matches.
+
+```dotenv
+KEEP_GEMINI_DOMAIN=fuwn.me
+```
+
+### `PROXY_BY_DEFAULT`
+
+Control weather or not all Gemini URLs will be proxied.
+
+Similar to `KEEP_GEMINI_EXACT` and `KEEP_GEMINI_DOMAIN` but global.
+
+Defaults to `true`.
+
+```dotenv
+PROXY_BY_DEFAULT=false
+```
+
+## Capsules using September
+
+[Add yours!](https://github.com/gemrest/september/edit/main/README.md)
+
+- <https://fuwn.me/>
+- <https://gem.rest/>
+
+## License
+
+This project is licensed with the
+[GNU General Public License v3.0](https://github.com/gemrest/september/blob/main/LICENSE).
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..3a31cf4
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,19 @@
+// This file is part of September <https://github.com/gemrest/september>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// 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
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+fn main() -> anyhow::Result<()> { vergen::vergen(vergen::Config::default()) }
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..896f490
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,11 @@
+services:
+ september:
+ ports:
+ - "8080:80"
+ environment:
+ - "ROOT=gemini://fuwn.me"
+ - "CSS_EXTERNAL=https://example.com/style.css"
+ - "KEEP_GEMINI_EXACT=gemini://fuwn.me/skills"
+ # - "KEEP_GEMINI_DOMAIN=fuwn.me"
+ - "PROXY_BY_DEFAULT=true"
+ image: "fuwn/september:latest"
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 0000000..099ab4b
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly-2022-03-08"
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..1dd4c90
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,30 @@
+condense_wildcard_suffixes = true
+edition = "2021"
+enum_discrim_align_threshold = 20
+# error_on_line_overflow = true
+# error_on_unformatted = true
+fn_single_line = true
+force_multiline_blocks = true
+format_code_in_doc_comments = true
+format_macro_matchers = true
+format_strings = true
+imports_layout = "HorizontalVertical"
+license_template_path = ".license_template"
+max_width = 80
+match_arm_blocks = false
+imports_granularity = "Crate"
+newline_style = "Unix"
+normalize_comments = true
+normalize_doc_attributes = true
+reorder_impl_items = true
+group_imports = "StdExternalCrate"
+reorder_modules = true
+report_fixme = "Always"
+# report_todo = "Always"
+struct_field_align_threshold = 20
+struct_lit_single_line = false
+tab_spaces = 2
+use_field_init_shorthand = true
+use_try_shorthand = true
+where_single_line = true
+wrap_comments = true
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..4f71529
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,325 @@
+// This file is part of September <https://github.com/gemrest/september>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// 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
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+#![deny(
+ warnings,
+ nonstandard_style,
+ unused,
+ future_incompatible,
+ rust_2018_idioms,
+ unsafe_code
+)]
+#![deny(clippy::all, clippy::nursery, clippy::pedantic)]
+#![recursion_limit = "128"]
+#![allow(clippy::cast_precision_loss)]
+
+#[macro_use]
+extern crate log;
+
+use std::{env::var, time::Instant};
+
+use actix_web::{web, Error, HttpResponse};
+use gmi::{protocol::Response, url::Url};
+
+#[allow(clippy::too_many_lines)]
+fn gemini_to_html(response: &Response, url: &Url) -> (String, String) {
+ let mut response_string = String::new();
+ let mut in_block = false;
+ let mut in_list = false;
+ let mut title = String::new();
+
+ for line in String::from_utf8_lossy(&response.data).to_string().lines() {
+ match line.get(0..1).unwrap_or("") {
+ // Convert links
+ "=" => {
+ let line = line.replace("=>", "").trim_start().to_owned();
+ let mut split = line.split(' ').collect::<Vec<_>>();
+ let mut href = split.remove(0).to_string();
+ let text = split.join(" ");
+
+ if var("PROXY_BY_DEFAULT").unwrap_or_else(|_| "true".to_string())
+ == "true"
+ && href.contains("gemini://")
+ {
+ href = format!("/proxy/{}", href.trim_start_matches("gemini://"));
+ }
+
+ if let Ok(keeps) = var("KEEP_GEMINI_EXACT") {
+ let mut keeps = keeps.split(',');
+
+ if href.starts_with('/') {
+ let temporary_href =
+ format!("gemini://{}{}", url.authority.host, href);
+
+ if keeps.any(|k| k == &*temporary_href) {
+ href = temporary_href;
+ }
+ }
+ }
+
+ if let Ok(keeps) = var("KEEP_GEMINI_DOMAIN") {
+ if href.starts_with('/')
+ && keeps.split(',').any(|k| k == &*url.authority.host)
+ {
+ href = format!("gemini://{}{}", url.authority.host, href);
+ }
+ }
+
+ response_string
+ .push_str(&format!("<p><a href=\"{}\">{}</a></p>\n", href, text));
+ }
+ // Add whitespace
+ "" => {
+ if in_list {
+ in_list = false;
+ response_string.push_str("</ul>\n");
+ }
+
+ response_string.push('\n');
+ }
+ // Convert lists
+ "*" =>
+ if in_list {
+ response_string.push_str(&format!(
+ "<li>{}</li>\n",
+ line.replace('*', "").trim_start()
+ ));
+ } else {
+ in_list = true;
+ response_string.push_str("<ul>\n");
+ },
+ // Convert headings
+ "#" => {
+ if in_list {
+ in_list = false;
+ response_string.push_str("</ul>\n");
+ }
+
+ match line.get(0..2) {
+ Some(heading) =>
+ match heading {
+ "###" => {
+ response_string.push_str(&format!(
+ "<h3>{}</h3>",
+ line.replace("###", "").trim_start()
+ ));
+ }
+ _ =>
+ if heading.starts_with("##") {
+ response_string.push_str(&format!(
+ "<h2>{}</h2>",
+ line.replace("##", "").trim_start()
+ ));
+ } else {
+ let fixed_line =
+ line.replace('#', "").trim_start().to_owned();
+
+ response_string.push_str(&format!("<h1>{}</h1>", fixed_line));
+
+ if title.is_empty() {
+ title = fixed_line;
+ }
+ },
+ },
+ None => {}
+ }
+ }
+ // Convert blockquotes
+ ">" => {
+ if in_list {
+ in_list = false;
+ response_string.push_str("</ul>\n");
+ }
+
+ response_string.push_str(&format!(
+ "<blockquote>{}</blockquote>\n",
+ line.replace('>', "").trim_start()
+ ));
+ }
+ // Convert preformatted blocks
+ "`" => {
+ if in_list {
+ in_list = false;
+ response_string.push_str("</ul>\n");
+ }
+
+ in_block = !in_block;
+ if in_block {
+ response_string.push_str("<pre>\n");
+ } else {
+ response_string.push_str("</pre>\n");
+ }
+ }
+ // Add text lines
+ _ => {
+ if in_list {
+ in_list = false;
+ response_string.push_str("</ul>\n");
+ }
+
+ if in_block {
+ response_string.push_str(&format!("{}\n", line));
+ } else {
+ response_string.push_str(&format!("<p>{}</p>", line));
+ }
+ }
+ }
+ }
+
+ (title, response_string)
+}
+
+#[allow(clippy::unused_async, clippy::future_not_send)]
+async fn default(req: actix_web::HttpRequest) -> Result<HttpResponse, Error> {
+ // Try to construct a Gemini URL
+ let url = match Url::try_from(&*if req.path().starts_with("/proxy") {
+ format!("gemini://{}", req.path().replace("/proxy/", ""))
+ } else if req.path().starts_with("/x") {
+ format!("gemini://{}", req.path().replace("/x/", ""))
+ } else {
+ // Try to set `ROOT` as `ROOT` environment variable, or use
+ // `"gemini://fuwn.me"` as default.
+ format!(
+ "{}{}",
+ {
+ if let Ok(root) = var("ROOT") {
+ root
+ } else {
+ warn!(
+ "could not use ROOT from environment variables, proceeding with \
+ default root: gemini://fuwn.me"
+ );
+
+ "gemini://fuwn.me".to_string()
+ }
+ },
+ req.path()
+ )
+ }) {
+ Ok(url) => url,
+ Err(e) => {
+ return Ok(HttpResponse::Ok().body(e.to_string()));
+ }
+ };
+ // Make a request to get Gemini content and time it.
+ let mut timer = Instant::now();
+ let response = match gmi::request::make_request(&url) {
+ Ok(response) => response,
+ Err(e) => {
+ return Ok(HttpResponse::Ok().body(e.to_string()));
+ }
+ };
+ let response_time_taken = timer.elapsed();
+
+ // Reset timer for below
+ timer = Instant::now();
+
+ // Convert Gemini Response to HTML and time it.
+ let mut html_context = gemini_to_html(&response, &url);
+ let convert_time_taken = timer.elapsed();
+
+ // Try to add an external stylesheet from the `CSS_EXTERNAL` environment
+ // variable.
+ if let Ok(css) = var("CSS_EXTERNAL") {
+ html_context.1.push_str(&format!(
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"{}\">",
+ css
+ ));
+ }
+
+ // Add a title to HTML response
+ html_context
+ .1
+ .push_str(&format!("<title>{}</title>", html_context.0));
+
+ // Add proxy information to footer of HTML response
+ html_context.1.push_str(&format!(
+ "<details>\n<summary>Proxy information</summary>
+<dl>
+<dt>Original URL</dt>
+<dd><a href=\"{}\">{0}</a></dd>
+<dt>Status code</dt>
+<dd>{:?}</dd>
+<dt>Meta</dt>
+<dd>{}</dd>
+<dt>Capsule response time</dt>
+<dd>{} milliseconds</dd>
+<dt>Gemini-to-HTML time</dt>
+<dd>{} milliseconds</dd>
+</dl>
+<p>This content has been proxied by \
+<a href=\"https://github.com/gemrest/september{}\">September ({})</a>.</p>
+</details>",
+ url,
+ response.status,
+ response.meta,
+ response_time_taken.as_nanos() as f64 / 1_000_000.0,
+ convert_time_taken.as_nanos() as f64 / 1_000_000.0,
+ format_args!("/tree/{}", env!("VERGEN_GIT_SHA")),
+ env!("VERGEN_GIT_SHA").get(0..5).unwrap_or("UNKNOWN"),
+ ));
+
+ // Return HTML response
+ Ok(
+ HttpResponse::Ok()
+ .content_type("text/html; charset=utf-8")
+ .body(html_context.1),
+ )
+}
+
+#[actix_web::main]
+async fn main() -> std::io::Result<()> {
+ // Set the `RUST_LOG` environment variable so Actix can provide logs.
+ //
+ // This can be overridden using the `RUST_LOG` environment variable in
+ // configuration.
+ std::env::set_var("RUST_LOG", "actix_web=info");
+
+ // Initialise `dotenv` so we can access `.env` files.
+ dotenv::dotenv().ok();
+ // Initialise logger so we can see logs
+ pretty_env_logger::init();
+
+ // Setup Actix web-server
+ actix_web::HttpServer::new(move || {
+ actix_web::App::new()
+ .default_service(web::get().to(default))
+ .wrap(actix_web::middleware::Logger::default())
+ })
+ .bind((
+ // Bind Actix web-server to localhost
+ "0.0.0.0",
+ // If the `PORT` environment variable is present, try to use it, otherwise;
+ // use port `80`.
+ if let Ok(port) = var("PORT") {
+ match port.parse::<_>() {
+ Ok(port) => port,
+ Err(e) => {
+ warn!("could not use PORT from environment variables: {}", e);
+ warn!("proceeding with default port: 80");
+
+ 80
+ }
+ }
+ } else {
+ 80
+ },
+ ))?
+ .run()
+ .await
+}