diff options
| author | Fuwn <[email protected]> | 2022-04-01 01:59:13 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2022-04-01 01:59:13 +0000 |
| commit | baa3b13e9dd4bfe381373b9251aef71a9d6670fe (patch) | |
| tree | 1df8260acc1feb9cbcbe9628873f45e7aa8c5ac7 /src | |
| parent | docs(license): add license (diff) | |
| download | september-baa3b13e9dd4bfe381373b9251aef71a9d6670fe.tar.xz september-baa3b13e9dd4bfe381373b9251aef71a9d6670fe.zip | |
feat: 0.0.0 :star:
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 325 |
1 files changed, 325 insertions, 0 deletions
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 +} |