Compare commits

...

75 Commits

Author SHA1 Message Date
Cadey Ratio e9bcdf9f52 Merge pull request 'Add support for alt-text in preformatted blocks' (#15) from alch_emii/maj-prs:alt-text into main
Reviewed-on: cadey/maj#15
2021-09-08 13:30:32 +00:00
Emi Tatsuo c49c76b5ad
Merge remote-tracking branch 'upstream/main' into alt-text
continuous-integration/drone/pr Build is passing Details
2020-12-15 09:09:22 -05:00
Cadey Ratio f6424dca1a Merge pull request 'Update tokio & rustls, remove async-std & async-tls' (#19) from alch_emii/maj-prs:remove-async-std into main
continuous-integration/drone/push Build encountered an error Details
Reviewed-on: cadey/maj#19
2020-12-15 01:24:37 +00:00
Cadey Ratio 6524ee688a Merge pull request 'Enable CI tests for pull requests' (#17) from alch_emii/maj-prs:pr-test into main
continuous-integration/drone/push Build encountered an error Details
Reviewed-on: cadey/maj#17
2020-12-15 01:21:10 +00:00
Emi Tatsuo aaac9d0c93
Updated other workspace members
continuous-integration/drone/pr Build is passing Details
2020-12-11 10:07:01 -05:00
Emi Tatsuo 2566d930bf
Remove dependency on async-std & async-tls in maj, upgrade Tokio
Still gotta patch out the other crates tho
2020-12-10 14:51:40 -05:00
Emi Tatsuo 3b4e9c77ec
Use triggers instead of step conditions in .drone.yml where relevant
continuous-integration/drone/pr Build is failing Details
2020-12-10 13:12:30 -05:00
Emi Tatsuo accd372320
Enable CI tests for pull requests
continuous-integration/drone/pr Build is failing Details
2020-12-10 12:10:34 -05:00
Emi Tatsuo 5a6208dedf
Fix dependant workspace members
continuous-integration/drone/pr Build encountered an error Details
2020-12-10 11:53:57 -05:00
Emi Tatsuo 3dadcc05ba
Merge remote-tracking branch 'upstream/main' into alt-text
continuous-integration/drone/pr Build is passing Details
2020-12-10 11:04:22 -05:00
Cadey Ratio d08994c0cb Merge pull request 'Add a doctest for `blank_line()`' (#16) from alch_emii/maj-prs:blank-line-doctest into main
continuous-integration/drone/push Build is failing Details
Reviewed-on: cadey/maj#16
2020-12-10 12:47:31 +00:00
Emi Tatsuo e44decab07
Merge remote-tracking branch 'upstream/main' into blank-line-doctest
continuous-integration/drone/pr Build is passing Details
2020-12-07 18:27:26 -05:00
Cadey Ratio cf73a3bb1e Merge pull request 'Add a `blank_line()` method to `Builder`' (#13) from alch_emii/maj-prs:blank-line into main
continuous-integration/drone/push Build is failing Details
Reviewed-on: cadey/maj#13
2020-12-06 01:14:19 +00:00
Cadey Ratio 87a96151b5 Merge pull request 'Add conversion traits to Builder' (#12) from alch_emii/maj-prs:to-string into main
continuous-integration/drone/push Build is failing Details
Reviewed-on: cadey/maj#12
2020-12-06 01:13:21 +00:00
Emii Tatsuo 9bc2ea1159
Fix bug with type inferring for the `preformatted()` method
continuous-integration/drone/pr Build encountered an error Details
2020-11-30 14:17:13 -05:00
Emii Tatsuo 4c86fbba1e
Add support for alt-text in preformatted blocks ⚠️
continuous-integration/drone/pr Build is failing Details
⚠️  Breaking Change ⚠️

This adds support for parsing and rendering alt-text in preformatted blocks.  This changes the public API both around the `Node` enum and the `preformatted()` method of the builder.  I cannot think of a way to avoid this that is not needlessly overcomplicated, except maybe merging the preformatted & body into a single newline delimited string but that's hella gross and i don't think anyone wants that.  Lemme know if you can think of a better way of doing this

Preformatted blocks without alt text can still be created by passing in an empty string.  Because alt text isn't separated by a space, this does not add any unnecessary padding.  I chose not to accept Option<String> here because an empty string serves the same function but encourages users to use alt-text in their documents, which is very important for increasing accessibility.
2020-11-30 14:05:12 -05:00
Emii Tatsuo a7fabdc909
Allow Builder as Into<Vec<Node>>
continuous-integration/drone/pr Build encountered an error Details
2020-11-30 01:40:23 -05:00
Emii Tatsuo adf82e9d9b
Add a doctest for the `blank_line()` method
continuous-integration/drone/pr Build is passing Details
This adds a simple doctest for `blank_line()` but will not be included in the original PR because it is contingent on the merge of a pending PR for adding the `to_string()` method.
2020-11-30 01:17:41 -05:00
Emii Tatsuo c69ec3b7df
Merge branch 'to-string' into blank-line 2020-11-30 01:16:37 -05:00
Emii Tatsuo ac88fb60ee
Add a `blank_line()` method to `Builder`
continuous-integration/drone/pr Build is failing Details
Many times users may want to seperate lines of text using a blank line.  Currently, this can be accomplished by either adding '\n' to the previous `text()` call, which requires that the user has the ability to access this, and can also look a little messy, or by calling `text()` with an empty string, which definately works, but having an explicit method might be a nice sugar for a lot of users, and barely adds any weight to the codebase.

This is definately a small thing, and almost closer to a personal preferance than anything, but I definately think it would make the library a tiny bit nicer to use, and there's barely any tradeoff.  It's still up to you though if you'd rather keep your codebase small.
2020-11-30 01:11:12 -05:00
Emii Tatsuo 2f3dd72d90
Add AsRef and AsMut<[Node]> to builder
continuous-integration/drone/pr Build encountered an error Details
2020-11-30 00:52:08 -05:00
Emii Tatsuo 34dca8d92d
Impl ToString for Builder, accept AsRef<[Node]> in `render()`
continuous-integration/drone/pr Build is failing Details
This adds a to_string method to the `Builder` allowing for the easy conversion of a Vec<Node> into a String, for any usecases where a library might not be directly writing to an io::Write, or may want to do String-y things with your document first.  Without this, users would have to write to a Vec<u8> and convert to a String, which is kinda unintuitive, takes a lot of steps, and doesn't produce very readable code.  This simplifies it to one method call.

* Implementation of the std::str::ToString method for Builder
* Accepting any AsRef<[Node]> in render (including accepting the old Vec<Node>, so not breaking)
* Addition of estimate_len() to Node, used to pre-allocate the correct size of the String buffer

* `estimate_len` has some quick doctests and examples.  I know most of the rest of the project uses test methods, but I hope this is alright given that the tests may add some more clarity to the purpose and function of the method.
* `to_string` has a single line of unsafe code.  As the associated comment explains, this is provably safe, and exists just to avoid having to choose between having a bunch of duplicate code or inefficiently performing a UTF-8 check on a whole bunch bytes that we already know are safe.  That said, I totally get it if you're just generally against unsafe code and will change it to be an alternative if you so wish
* ToString is implemented instead of Display.  This is to discourage users from directly using this in a println!() or write!() macro, which would not be a thing you would normally expect to do with this.  It also gives us the advantage of being able to pre-allocate a buffer size, meaning less expensive String resizing.
* I couldn't think of a clever way to get `render()` to work with both `io::Write`s or `fmt::Write`s without duplicating the code, but I'm dumb and might be missing something, so if there's a way to do that instead of doing my funky unsafe hack that's cool and I can do that instead.
2020-11-29 23:17:15 -05:00
Cadey Ratio c743056263 Remove kindlegen from shell.nix
continuous-integration/drone/push Build is passing Details
Closes #11
2020-11-01 22:47:28 +00:00
Cadey Ratio bebfa4d7b1 release gemtext 0.2.1 with clone fix from @boringcactus
continuous-integration/drone/push Build is passing Details
2020-10-06 17:43:42 -04:00
Cadey Ratio 725957bf8c Merge pull request 'make gemtext::Node `Clone`' (#10) from boringcactus/maj:make-gemtext-node-clone into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: cadey/maj#10
2020-10-06 21:42:28 +00:00
Melody Horn c07d81077a make gemtext::Node `Clone`
continuous-integration/drone/pr Build is passing Details
2020-10-05 04:38:19 -06:00
Cadey Ratio d437ac6e8f version bump for gemtext
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-09-26 19:21:03 -04:00
Cadey Ratio f2a251e829 version bump
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-09-26 19:12:29 -04:00
Cadey Ratio e58a01d14d Merge pull request 'escape special prefixes in plaintext nodes' (#9) from boringcactus/maj:preserve-texthood-of-text-nodes into main
continuous-integration/drone/push Build is failing Details
Reviewed-on: cadey/maj#9
2020-09-26 23:09:23 +00:00
Melody Horn 3cd71ce302 escape special prefixes in plaintext nodes
continuous-integration/drone/pr Build is passing Details
2020-09-26 15:22:39 -06:00
Cadey Ratio 85a3cfda4a double oops
continuous-integration/drone/push Build is failing Details
2020-08-08 16:17:35 -04:00
Cadey Ratio 740ac00628 use gemtext crate :D 2020-08-08 16:14:07 -04:00
Cadey Ratio 6058af8b44 update gemtext metadata for publication
continuous-integration/drone/push Build is failing Details
2020-08-08 16:11:02 -04:00
Cadey Ratio bae0ccb136 refactor gemtext tools into its own crate
continuous-integration/drone/push Build is failing Details
2020-08-08 16:10:20 -04:00
Cadey Ratio 6b9070e200 majc: fix redirects
continuous-integration/drone/push Build is failing Details
2020-08-08 16:05:13 -04:00
Cadey Ratio 1429602370 oops
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-08-08 12:14:13 -04:00
Cadey Ratio f5d9e09e40 version 0.6.0
continuous-integration/drone/push Build is failing Details
2020-08-08 12:11:31 -04:00
Cadey Ratio a533ebbeaf fix CGI support 2020-08-08 12:08:03 -04:00
Cadey Ratio 1da65dcfeb add CGI support
continuous-integration/drone/push Build is passing Details
2020-08-08 11:23:44 -04:00
Cadey Ratio ccb142d8b3 Update 'VERSION'
continuous-integration/drone/push Build is passing Details
2020-08-06 19:08:57 +00:00
Cadey Ratio ddcb5afbc4 fix?
continuous-integration/drone/push Build was killed Details
2020-08-06 14:45:48 -04:00
Cadey Ratio 17a69980e0 tix tarot
continuous-integration/drone/push Build is passing Details
2020-08-05 16:16:41 -04:00
Cadey Ratio 9f19054993 add RPG character backstory generator
continuous-integration/drone/push Build is passing Details
2020-08-05 16:03:42 -04:00
Cadey Ratio f80bdd45e7 fix fix
continuous-integration/drone/push Build is passing Details
2020-08-02 03:16:29 +00:00
Cadey Ratio 0a3c6fb23f cleanup http rendering
continuous-integration/drone/push Build is passing Details
2020-08-02 03:13:01 +00:00
Cadey Ratio c6f15577bb fix 2020-08-02 02:45:36 +00:00
Cadey Ratio 00f8fdc9e1 shitpost: serve HTTP
continuous-integration/drone/push Build is passing Details
2020-08-01 22:42:44 -04:00
Cadey Ratio ff88b28688 oops
continuous-integration/drone/push Build is passing Details
2020-08-01 22:51:14 +00:00
Cadey Ratio ad7947c5fa serve cetacean.club
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-08-01 21:55:08 +00:00
Cadey Ratio 4b7374b39f adapt majsite to serve cetacean.club
continuous-integration/drone/push Build is passing Details
2020-08-01 16:47:34 -04:00
Cadey Ratio 0ffd86c9f6 more words
continuous-integration/drone/push Build is passing Details
2020-08-01 12:02:12 -04:00
Cadey Ratio 2c2fc3ee09 better stuff :D 2020-08-01 12:02:12 -04:00
Cadey Ratio 28ae14ffa7 input test and static file serving with majsite 2020-08-01 12:02:12 -04:00
Cadey Ratio d2af2c5f08 file serving 2020-08-01 12:02:12 -04:00
Cadey Ratio 91328c4188 fix gitignore 2020-08-01 12:02:12 -04:00
Cadey Ratio c6567cc99d karnycukta experiment 2020-08-01 12:02:12 -04:00
Cadey Ratio 3c9fb6eeb3 why is this not working
continuous-integration/drone/push Build is passing Details
2020-07-31 16:43:09 +00:00
Cadey Ratio 93a5dd445a majc: fix rendering of gemlog.blue
continuous-integration/drone/push Build is passing Details
2020-07-28 16:02:48 -04:00
Cadey Ratio 70074e0075 fix
continuous-integration/drone/push Build is passing Details
2020-07-27 21:43:31 -04:00
Cadey Ratio 8984355c4e Update 'Cargo.toml'
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
2020-07-28 01:39:57 +00:00
Cadey Ratio cd3cd13cd2 Merge pull request 'maj-async-std' (#4) from maj-async-std into main
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build was killed Details
Reviewed-on: cadey/maj#4
2020-07-28 01:36:01 +00:00
Cadey Ratio cd6ef8a516 cert info
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2020-07-27 21:21:24 -04:00
Cadey Ratio 04a0c8e988 more helpers
continuous-integration/drone/push Build is passing Details
2020-07-27 21:03:50 -04:00
Cadey Ratio b47c1a69a2 response building helpers
continuous-integration/drone/push Build is passing Details
2020-07-27 20:50:42 -04:00
Cadey Ratio dd6f7f4e7d update changelog
continuous-integration/drone/push Build is passing Details
2020-07-27 20:31:54 -04:00
Cadey Ratio 4216f6b709 gemini: add document rendering function
continuous-integration/drone/push Build is passing Details
2020-07-27 20:31:05 -04:00
Cadey Ratio 851d7925c5 gemini: add document builder 2020-07-27 20:25:00 -04:00
Cadey Ratio 8495c5ab0d pass gemini torture tests
continuous-integration/drone/push Build is passing Details
2020-07-27 20:11:57 -04:00
Cadey Ratio 2bc1aa315f use async-std and tokio i guess
continuous-integration/drone/push Build is passing Details
2020-07-27 20:01:09 -04:00
Cadey Ratio 564eb3990c bump changelog
continuous-integration/drone/push Build is failing Details
2020-07-27 19:51:45 -04:00
Cadey Ratio aa4715abd8 even smaller binaries 2020-07-27 19:50:41 -04:00
Cadey Ratio f5a99679af purge tokio 2020-07-27 19:49:39 -04:00
Cadey Ratio 128ff27bb5 Update '.drone.yml'
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2020-07-27 22:24:30 +00:00
Cadey Ratio d7dcce67ca remove nix for the moment
continuous-integration/drone/push Build is passing Details
2020-07-27 18:17:33 -04:00
Cadey Ratio 0217fa2792 Merge pull request 'majc: support history' (#3) from majc-history into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: cadey/maj#3
2020-07-27 22:16:58 +00:00
45 changed files with 2583 additions and 5092 deletions

View File

@ -7,23 +7,25 @@ steps:
pull: always pull: always
commands: commands:
- cargo test --all --all-features - cargo test --all --all-features
when:
event:
- push
- name: auto-release - name: auto-release
image: xena/gitea-release image: xena/gitea-release
pull: always pull: always
settings: environment:
auth_username: cadey RUST_LOG: debug
gitea_server: https://tulpa.dev PLUGIN_AUTH_USERNAME: cadey
gitea_token: PLUGIN_GITEA_SERVER: https://tulpa.dev
PLUGIN_GITEA_TOKEN:
from_secret: GITEA_TOKEN from_secret: GITEA_TOKEN
when: when:
event: event:
- push - push
branch: branch:
- main - main
trigger:
event:
- push
- pull_request
--- ---
@ -38,6 +40,6 @@ steps:
environment: environment:
CARGO_TOKEN: CARGO_TOKEN:
from_secret: CARGO_TOKEN from_secret: CARGO_TOKEN
when: trigger:
event: event:
- tag - tag

View File

@ -1,5 +1,38 @@
# Changelog # Changelog
## 0.6.2
Bump gemtext to 0.2.0
## 0.6.1
### FIXED
- [#9] Fixes from @boringcactus for gemtext rendering
## 0.6.0
### ADDED
- `maj::server` now has a CGI handler and majsite now has CGI enabled by default.
## 0.5.0
### ADDED
- A few more helper methods for making text/gemini nodes and responses.
`majsite` now serves static files and tests input from the user.
## 0.4.1
Oops
## 0.4.0
Tokio has been somewhat purged.
The gemini module now includes a document builder and rendering tool.
## 0.3.0 ## 0.3.0
### maj ### maj

4492
Cargo.nix

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "maj" name = "maj"
version = "0.3.0" version = "0.6.2"
authors = ["Christine Dodrill <me@christine.website>"] authors = ["Christine Dodrill <me@christine.website>"]
edition = "2018" edition = "2018"
license = "0BSD" license = "0BSD"
@ -11,44 +11,46 @@ repository = "https://tulpa.dev/cadey/maj"
[dependencies] [dependencies]
async-trait = { version = "0", optional = true } async-trait = { version = "0", optional = true }
log = "0.4"
mime_guess = "2.0"
num = "0.2" num = "0.2"
num-derive = "0.3" num-derive = "0.3"
num-traits = "0.2" num-traits = "0.2"
rustls = { version = "0.18", optional = true, features = ["dangerous_configuration"] } once_cell = "1.4"
webpki = { version = "0.21.0", optional = true } rustls = { version = "0.19", optional = true, features = ["dangerous_configuration"] }
webpki-roots = { version = "0.20", optional = true }
tokio-rustls = { version = "0.14", features = ["dangerous_configuration"], optional = true }
tokio-io-timeout = "0.4"
log = "0.4"
url = "2"
thiserror = "1"
structopt = "0.3" structopt = "0.3"
thiserror = "1"
tokio-rustls = { version = "0.21", features = ["dangerous_configuration"] }
tokio = { version = "0.3", features = ["full"] }
url = "2"
webpki-roots = { version = "0.20", optional = true }
webpki = { version = "0.21.0", optional = true }
gemtext = { path = "./gemtext" }
[dev-dependencies] [dev-dependencies]
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
[dependencies.tokio]
version = "0.2"
features = [
"macros",
"net",
"tcp",
"io-util",
"rt-threaded",
"time",
"stream"
]
optional = true
[features] [features]
default = ["client", "server"] default = ["client", "server"]
client = ["rustls", "webpki", "webpki-roots", "tokio", "tokio-rustls"] client = [
server = ["rustls", "webpki", "webpki-roots", "tokio", "async-trait", "tokio-rustls"] "webpki",
"webpki-roots",
]
server = [
"rustls",
"webpki",
"webpki-roots",
"async-trait",
]
[workspace] [workspace]
members = [ members = [
"./gemtext",
"./majc", "./majc",
"./majd", "./majd",
"./site" "./site",
"./pilno/karnycukta"
] ]

View File

@ -1 +1 @@
0.3.0 0.6.2

View File

@ -1,13 +0,0 @@
{ pkgs ? import <nixpkgs> { } }:
let
cargo_nix = pkgs.callPackage ./Cargo.nix {
defaultCrateOverrides = pkgs.defaultCrateOverrides // {
ncurses = attrs: { buildInputs = with pkgs; [ ncurses pkg-config ]; };
};
};
in {
majc = cargo_nix.workspaceMembers."majc".build;
majsite = cargo_nix.workspaceMembers."majsite".build;
}

View File

@ -1,24 +0,0 @@
{ system ? builtins.currentSystem }:
let
pkgs = import <nixpkgs> { };
callPackage = pkgs.lib.callPackageWith pkgs;
crates = callPackage ./default.nix { };
dockerImage = pkg:
pkgs.dockerTools.buildLayeredImage {
name = "xena/${pkg.name}";
tag = "latest";
contents = [ pkg ];
config = {
Cmd = [ "/bin/${pkg.name}" ];
WorkingDir = "/";
};
};
in {
majc = dockerImage crates.majc;
majsite = dockerImage crates.majsite;
}

15
gemtext/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "gemtext"
version = "0.2.1"
authors = ["Christine Dodrill <me@christine.website>"]
edition = "2018"
license = "0BSD"
description = "A gemini client and server for Rust"
repository = "https://tulpa.dev/cadey/maj"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[dev-dependencies]
pretty_env_logger = "0.4"

486
gemtext/src/lib.rs Normal file
View File

@ -0,0 +1,486 @@
/// This module implements a simple text/gemini parser based on the description
/// here: https://gemini.circumlunar.space/docs/specification.html
use std::io::{self, Write};
/// Build a gemini document up from a series of nodes.
#[derive(Default)]
pub struct Builder {
nodes: Vec<Node>,
}
impl Builder {
pub fn new() -> Builder {
Builder::default()
}
pub fn text<T: Into<String>>(mut self, data: T) -> Builder {
self.nodes.push(Node::Text(data.into()));
self
}
/// Append a single blank line to the document
///
/// This is equivilent to calling [`text()`] with an empty string, or pushing a blank
/// [`Node`]
///
/// ```
/// # use gemtext::Builder;
/// let greeting = Builder::new()
/// .text("Hello")
/// .blank_line()
/// .text("universe")
/// .to_string();
///
/// assert_eq!(greeting.trim(), "Hello\n\nuniverse");
/// ```
///
/// [`text()`]: Self::text()
pub fn blank_line(mut self) -> Self {
self.nodes.push(Node::blank());
self
}
pub fn link<T: Into<String>>(mut self, to: T, name: Option<String>) -> Builder {
self.nodes.push(Node::Link {
to: to.into(),
name: name,
});
self
}
pub fn preformatted<A, T>(mut self, alt_text: A, data: T) -> Builder
where
A: Into<String>,
T: Into<String>,
{
self.nodes.push(Node::Preformatted { alt: alt_text.into(), body: data.into() });
self
}
pub fn heading<T: Into<String>>(mut self, level: u8, body: T) -> Builder {
self.nodes.push(Node::Heading {
level: level,
body: body.into(),
});
self
}
pub fn list_item<T: Into<String>>(mut self, item: T) -> Builder {
self.nodes.push(Node::ListItem(item.into()));
self
}
pub fn quote<T: Into<String>>(mut self, body: T) -> Builder {
self.nodes.push(Node::Quote(body.into()));
self
}
pub fn build(self) -> Vec<Node> {
self.nodes
}
}
impl ToString for Builder {
/// Render a document to a string
///
/// This produces a text/gemini compliant text document, represented as a string
fn to_string(&self) -> String {
let len: usize = self.nodes.iter().map(Node::estimate_len).sum(); // sum up node lengths
let mut bytes = Vec::with_capacity(len + self.nodes.len()); // add in inter-node newlines
render(self, &mut bytes).unwrap(); // Writing to a string shouldn't produce errors
unsafe {
// This is safe because bytes is composed of Strings. We could have this as
// pure safe code by replicating the `render()` method and switching it to use
// a fmt::Write (or even `String::push()`)instead of a io::Write, but this has
// the same effect, with much DRYer code.
String::from_utf8_unchecked(bytes)
}
}
}
impl AsRef<[Node]> for Builder {
/// Get a reference to the internal node list of this builder
fn as_ref(&self) -> &[Node] {
self.nodes.as_ref()
}
}
impl AsMut<[Node]> for Builder {
/// Get a mutable reference to the internal node list of this builder
fn as_mut(&mut self) -> &mut [Node] {
self.nodes.as_mut()
}
}
impl From<Builder> for Vec<Node> {
/// Convert into a collection of [`Node`]s.
///
/// Equivilent to calling [`Builder::build()`]
fn from(builder: Builder) -> Self {
builder.build()
}
}
/// Render a set of nodes as a document to a writer.
pub fn render(nodes: impl AsRef<[Node]>, out: &mut impl Write) -> io::Result<()> {
use Node::*;
for node in nodes.as_ref() {
match node {
Text(body) => {
let special_prefixes = ["=>", "```", "#", "*", ">"];
if special_prefixes.iter().any(|prefix| body.starts_with(prefix)) {
write!(out, " ")?;
}
write!(out, "{}\n", body)?
},
Link { to, name } => match name {
Some(name) => write!(out, "=> {} {}\n", to, name)?,
None => write!(out, "=> {}\n", to)?,
},
Preformatted { alt, body } => write!(out, "```{}\n{}\n```\n", alt, body)?,
Heading { level, body } => write!(out, "{} {}\n", "#".repeat(*level as usize), body)?,
ListItem(body) => write!(out, "* {}\n", body)?,
Quote(body) => write!(out, "> {}\n", body)?,
};
}
Ok(())
}
/// Individual nodes of the document. Each node correlates to a line in the file.
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Node {
/// Text lines are the most fundamental line type - any line which does not
/// match the definition of another line type defined below defaults to
/// being a text line. The majority of lines in a typical text/gemini document will be text lines.
Text(String),
/// Lines beginning with the two characters "=>" are link lines, which have the following syntax:
///
/// ```gemini
/// =>[<whitespace>]<URL>[<whitespace><USER-FRIENDLY LINK NAME>]
/// ```
///
/// where:
///
/// * `<whitespace>` is any non-zero number of consecutive spaces or tabs
/// * Square brackets indicate that the enclosed content is optional.
/// * `<URL>` is a URL, which may be absolute or relative. If the URL
/// does not include a scheme, a scheme of `gemini://` is implied.
Link { to: String, name: Option<String> },
/// Any line whose first three characters are "```" (i.e. three consecutive
/// back ticks with no leading whitespace) are preformatted toggle lines.
/// These lines should NOT be included in the rendered output shown to the
/// user. Instead, these lines toggle the parser between preformatted mode
/// being "on" or "off". Preformatted mode should be "off" at the beginning
/// of a document. The current status of preformatted mode is the only
/// internal state a parser is required to maintain. When preformatted mode
/// is "on", the usual rules for identifying line types are suspended, and
/// all lines should be identified as preformatted text lines (see 5.4.4).
///
/// Preformatted text lines should be presented to the user in a "neutral",
/// monowidth font without any alteration to whitespace or stylistic
/// enhancements. Graphical clients should use scrolling mechanisms to present
/// preformatted text lines which are longer than the client viewport, in
/// preference to wrapping. In displaying preformatted text lines, clients
/// should keep in mind applications like ASCII art and computer source
/// code: in particular, source code in languages with significant whitespace
/// (e.g. Python) should be able to be copied and pasted from the client into
/// a file and interpreted/compiled without any problems arising from the
/// client's manner of displaying them.
///
/// The first preformatted toggle of a document is often followed by a short
/// string, which acts as alt-text for the preformatted block. This is also
/// often used to denote the language of code in a block of text. For example,
/// a block starting with the text `\`\`\`rust` may be interpreted as rust
/// code, and a block starting with `\`\`\` An ascii art owl` would be
/// described aptly to visually impaired users using a screen reader. The alt
/// text may be separated from the toggle by whitespace. `gemtext` currently
/// renders alt text without this separation.
///
/// To create a preformatted block with no alt text, simply pass a zero-length
/// string as alt text.
Preformatted { alt: String, body: String },
/// Lines beginning with "#" are heading lines. Heading lines consist of one,
/// two or three consecutive "#" characters, followed by optional whitespace,
/// followed by heading text. The number of # characters indicates the "level"
/// of header; #, ## and ### can be thought of as analogous to `<h1>`, `<h2>`
/// and `<h3>` in HTML.
///
/// Heading text should be presented to the user, and clients MAY use special
/// formatting, e.g. a larger or bold font, to indicate its status as a header
/// (simple clients may simply print the line, including its leading #s,
/// without any styling at all). However, the main motivation for the
/// definition of heading lines is not stylistic but to provide a
/// machine-readable representation of the internal structure of the document.
/// Advanced clients can use this information to, e.g. display an automatically
/// generated and hierarchically formatted "table of contents" for a long
/// document in a side-pane, allowing users to easily jump to specific sections
/// without excessive scrolling. CMS-style tools automatically generating menus
/// or Atom/RSS feeds for a directory of text/gemini files can use first
/// heading in the file as a human-friendly title.
Heading { level: u8, body: String },
/// Lines beginning with "* " are unordered list items. This line type exists
/// purely for stylistic reasons. The * may be replaced in advanced clients by
/// a bullet symbol. Any text after the "* " should be presented to the user as
/// if it were a text line, i.e. wrapped to fit the viewport and formatted
/// "nicely". Advanced clients can take the space of the bullet symbol into
/// account when wrapping long list items to ensure that all lines of text
/// corresponding to the item are offset an equal distance from the left of the screen.
ListItem(String),
/// Lines beginning with ">" are quote lines. This line type exists so that
/// advanced clients may use distinct styling to convey to readers the important
/// semantic information that certain text is being quoted from an external
/// source. For example, when wrapping long lines to the the viewport, each
/// resultant line may have a ">" symbol placed at the front.
Quote(String),
}
impl Node {
pub fn blank() -> Node {
Node::Text("".to_string())
}
/// Cheaply estimate the length of this node
///
/// This measures length in bytes, *not characters*. So if the user includes
/// non-ascii characters, a single one of these characters may add several bytes to
/// the length, despite only displaying as one character.
///
/// This does include any newlines, but not any trailing newlines. For example, a
/// preformatted text block containing a single line reading "trans rights! 🏳️‍⚧️"
/// would have a length of 30: 3 backticks, a newline, the text (including 16 bytes
/// for the trans flag), another newline, and another 3 backticks.
///
/// ```
/// # use gemtext::Node;
/// let simple_text = Node::Text(String::from("Henlo worl"));
/// let linky_link = Node::Link { to: "gemini://cetacean.club/maj/".to_string(), name: Some("Maj".to_string()) };
/// let human_rights = Node::Preformatted {
/// alt: "".to_string(),
/// body: "trans rights! 🏳️‍⚧️".to_string(),
/// };
///
/// assert_eq!(
/// simple_text.estimate_len(),
/// "Henlo worl".as_bytes().len()
/// );
/// assert_eq!(
/// linky_link.estimate_len(),
/// "=> gemini://cetacean.club/maj/ Maj".as_bytes().len()
/// );
/// assert_eq!(
/// human_rights.estimate_len(),
/// "```\ntrans rights! 🏳️‍⚧️\n```".as_bytes().len()
/// );
/// ```
pub fn estimate_len(&self) -> usize {
match self {
Self::Text(text) => text.len(),
Self::Link { to, name } => 3 + to.as_bytes().len() +
name.as_ref().map(|n| n.as_bytes().len() + 1).unwrap_or(0),
Self::Preformatted { alt, body } => alt.as_bytes().len()
+ body.as_bytes().len() + 8,
Self::Heading { level, body } => *level as usize + 1 + body.as_bytes().len(),
Self::ListItem(item) | Self::Quote(item)=> 2 + item.as_bytes().len(),
}
}
}
pub fn parse(doc: &str) -> Vec<Node> {
let mut result: Vec<Node> = vec![];
let mut collect_preformatted: bool = false;
let mut preformatted_buffer: Vec<u8> = vec![];
let mut alt = "";
for line in doc.lines() {
if let Some(trailing) = line.strip_prefix("```") {
collect_preformatted = !collect_preformatted;
if !collect_preformatted {
result.push(Node::Preformatted {
alt: alt.to_string(),
body: String::from_utf8(preformatted_buffer)
.unwrap()
.trim_end()
.to_string(),
});
preformatted_buffer = vec![];
} else {
alt = trailing.trim();
}
continue;
}
if collect_preformatted && line != "```" {
write!(preformatted_buffer, "{}\n", line).unwrap();
continue;
}
// Quotes
if line.starts_with(">") {
result.push(Node::Quote(line[1..].trim().to_string()));
continue;
}
// List items
if line.starts_with("*") {
result.push(Node::ListItem(line[1..].trim().to_string()));
continue;
}
// Headings
if line.starts_with("###") {
result.push(Node::Heading {
level: 3,
body: line[3..].trim().to_string(),
});
continue;
}
if line.starts_with("##") {
result.push(Node::Heading {
level: 2,
body: line[2..].trim().to_string(),
});
continue;
}
if line.starts_with("#") {
result.push(Node::Heading {
level: 1,
body: line[1..].trim().to_string(),
});
continue;
}
// Links
if line.starts_with("=>") {
let sp = line[2..].split_ascii_whitespace().collect::<Vec<&str>>();
match sp.len() {
1 => result.push(Node::Link {
to: sp[0].trim().to_string(),
name: None,
}),
_ => result.push(Node::Link {
to: sp[0].trim().to_string(),
name: Some(sp[1..].join(" ").trim().to_string()),
}),
}
continue;
}
result.push(Node::Text(line.to_string()));
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic() {
let _ = pretty_env_logger::try_init();
let msg = include_str!("../../majc/src/help.gmi");
let doc = super::parse(msg);
assert_ne!(doc.len(), 0);
}
#[test]
fn quote() {
let _ = pretty_env_logger::try_init();
let msg = ">hi there";
let expected: Vec<Node> = vec![Node::Quote("hi there".to_string())];
assert_eq!(expected, parse(msg));
}
#[test]
fn list() {
let _ = pretty_env_logger::try_init();
let msg = "*hi there";
let expected: Vec<Node> = vec![Node::ListItem("hi there".to_string())];
assert_eq!(expected, parse(msg));
}
#[test]
fn preformatted() {
let _ = pretty_env_logger::try_init();
let msg = "```hi there\n\
obi-wan kenobi\n\
```\n\
\n\
Test\n";
let expected: Vec<Node> = vec![
Node::Preformatted{ alt: "hi there".to_string(), body: "obi-wan kenobi".to_string() },
Node::Text(String::new()),
Node::Text("Test".to_string()),
];
assert_eq!(expected, parse(msg));
}
#[test]
fn header() {
let _ = pretty_env_logger::try_init();
let msg = "#hi\n##there\n### my friends";
let expected: Vec<Node> = vec![
Node::Heading {
level: 1,
body: "hi".to_string(),
},
Node::Heading {
level: 2,
body: "there".to_string(),
},
Node::Heading {
level: 3,
body: "my friends".to_string(),
},
];
assert_eq!(expected, parse(msg));
}
#[test]
fn link() {
let _ = pretty_env_logger::try_init();
let msg = "=>/\n=> / Go home";
let expected: Vec<Node> = vec![
Node::Link {
to: "/".to_string(),
name: None,
},
Node::Link {
to: "/".to_string(),
name: Some("Go home".to_string()),
},
];
assert_eq!(expected, parse(msg));
}
#[test]
fn ambiguous_preformatted() {
let _ = pretty_env_logger::try_init();
let msg = include_str!("../../testdata/ambig_preformatted.gmi");
let expected: Vec<Node> = vec![
Node::Preformatted { alt: "foo".to_string(), body: "FOO".to_string() },
Node::Text("Foo bar".to_string()),
];
assert_eq!(expected, parse(msg));
}
#[test]
fn ambiguous_text() {
let _ = pretty_env_logger::try_init();
let original = Node::Text("#1 World's Best Coder".to_string());
let expected = " #1 World's Best Coder\n";
let mut rendered: Vec<u8> = vec![];
render(vec![original], &mut rendered).unwrap();
let rendered = String::from_utf8(rendered).unwrap();
assert_eq!(expected, rendered)
}
}

View File

@ -1,6 +1,6 @@
[package] [package]
name = "majc" name = "majc"
version = "0.2.0" version = "0.2.1"
authors = ["Christine Dodrill <me@christine.website>"] authors = ["Christine Dodrill <me@christine.website>"]
edition = "2018" edition = "2018"
@ -11,8 +11,7 @@ cursive = "0.15"
log = "0.4" log = "0.4"
url = "2" url = "2"
webpki = "0.21.0" webpki = "0.21.0"
tokio-rustls = { version = "0.14", features = ["dangerous_configuration"] } rustls = { version = "0.19", features = ["dangerous_configuration"] }
rustls = { version = "0.18", features = ["dangerous_configuration"] }
smol = { version = "0.3", features = ["tokio02"] } smol = { version = "0.3", features = ["tokio02"] }
maj = { path = ".." } maj = { path = ".." }

View File

@ -4,6 +4,16 @@ use cursive::{
Cursive, Cursive,
}; };
pub fn quit(siv: &mut Cursive) {
siv.add_layer(
Dialog::text("Are you sure you want to quit?")
.button("No", |s| {
s.pop_layer();
})
.button("Yes", Cursive::quit),
);
}
pub fn help(siv: &mut Cursive) { pub fn help(siv: &mut Cursive) {
let content = include_str!("./help.gmi"); let content = include_str!("./help.gmi");

View File

@ -1,13 +1,14 @@
use cursive::{ use cursive::{
theme::{Effect, Style}, theme::{BaseColor, Color, Effect, Style},
traits::*, traits::*,
utils::markup::StyledString, utils::markup::StyledString,
views::{Dialog, EditView, ResizedView, SelectView, TextView}, views::{Dialog, EditView, ResizedView, SelectView, TextView},
Cursive, Cursive,
}; };
use maj::{self, Response}; use maj::{self, Response};
use rustls::ClientConfig;
use std::str; use std::str;
use tokio_rustls::rustls::ClientConfig; use url::Url;
/// The state of the browser. /// The state of the browser.
#[derive(Clone)] #[derive(Clone)]
@ -153,13 +154,17 @@ pub fn show(siv: &mut Cursive, url: &str, resp: Response) {
TemporaryRedirect => { TemporaryRedirect => {
let st = siv.user_data::<State>().unwrap(); let st = siv.user_data::<State>().unwrap();
st.history.pop(); st.history.pop();
open(siv, resp.meta.as_str()); let u = Url::parse(url).unwrap();
let u = u.join(resp.meta.as_str()).unwrap();
open(siv, u.as_str());
} }
PermanentRedirect => { PermanentRedirect => {
let st = siv.user_data::<State>().unwrap(); let st = siv.user_data::<State>().unwrap();
st.history.pop(); st.history.pop();
open(siv, resp.meta.as_str()); let u = Url::parse(url).unwrap();
let u = u.join(resp.meta.as_str()).unwrap();
open(siv, u.as_str());
} }
Input => { Input => {
@ -219,13 +224,16 @@ pub fn render(body: &str) -> StyledString {
styled.append(StyledString::styled(name, Style::from(Effect::Underline))) styled.append(StyledString::styled(name, Style::from(Effect::Underline)))
} }
}, },
Preformatted(data) => styled.append(StyledString::plain(data)), Preformatted { body, .. } => styled.append(StyledString::plain(body)),
Heading { level, body } => styled.append(StyledString::styled( Heading { level, body } => styled.append(StyledString::styled(
format!("{} {}", "#".repeat(level as usize), body), format!("{} {}", "#".repeat(level as usize), body),
Style::from(Effect::Bold), Style::from(Effect::Bold),
)), )),
ListItem(item) => styled.append(StyledString::plain(format!("* {}", item))), ListItem(item) => styled.append(StyledString::plain(format!("* {}", item))),
Quote(quote) => styled.append(StyledString::plain(format!("> {}", quote))), Quote(quote) => styled.append(StyledString::styled(
format!("> {}", quote),
Style::from(Color::Dark(BaseColor::Green)),
)),
} }
styled.append(StyledString::plain("\n")); styled.append(StyledString::plain("\n"));
} }

View File

@ -1,4 +1,4 @@
use cursive::{event::Key, menu::MenuTree}; use cursive::{event::Key, menu::MenuTree, Cursive};
pub(crate) mod commands; pub(crate) mod commands;
pub(crate) mod gemini; pub(crate) mod gemini;
@ -17,8 +17,8 @@ fn main() {
siv.add_global_callback('c', |s| { siv.add_global_callback('c', |s| {
s.pop_layer(); s.pop_layer();
}); });
siv.add_global_callback('q', cursive::Cursive::quit); siv.add_global_callback('q', commands::quit);
siv.add_global_callback('~', cursive::Cursive::toggle_debug_console); siv.add_global_callback('~', Cursive::toggle_debug_console);
siv.add_global_callback('h', gemini::history); siv.add_global_callback('h', gemini::history);
siv.add_global_callback('l', gemini::links); siv.add_global_callback('l', gemini::links);
siv.add_global_callback('o', gemini::open_prompt); siv.add_global_callback('o', gemini::open_prompt);

View File

@ -1,4 +1,3 @@
use tokio_rustls::rustls;
use std::sync::Arc; use std::sync::Arc;
pub fn config() -> rustls::ClientConfig { pub fn config() -> rustls::ClientConfig {

2
pilno/karnycukta/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.epub
*.mobi

View File

@ -0,0 +1,21 @@
[package]
name = "karnycukta"
version = "0.1.0"
authors = ["Christine Dodrill <me@christine.website>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1"
async-trait = "0"
atom_syndication = "0.9"
chrono = "*"
log = "0"
pretty_env_logger = "0.4"
webpki = "0.21.0"
rustls = { version = "0.19", features = ["dangerous_configuration"] }
structopt = "0.3"
tokio = { version = "0.3", features = ["full"] }
maj = { path = "../..", features = ["server", "client"], default-features = false }

View File

@ -0,0 +1,38 @@
use anyhow::Result;
use structopt::StructOpt;
mod selfu;
mod zbasu;
#[derive(StructOpt, Debug)]
#[structopt(about = "la .karnycukta. cu finti lo samcukta fo lo zo .gemlogs.")]
enum Cmd {
/// selfu la samse'u
Selfu {
#[structopt(flatten)]
opts: selfu::Options,
},
/// zbasu lo cukta
Zbasu {
#[structopt(long, short = "n")]
nuzyurli: Vec<String>,
/// How many days to look back
#[structopt(long, short = "d")]
seldei: usize,
},
}
#[tokio::main]
async fn main() -> Result<()> {
pretty_env_logger::init();
let cmd = Cmd::from_args();
log::debug!("{:?}", cmd);
match cmd {
Cmd::Selfu { opts } => selfu::run(opts).await?,
Cmd::Zbasu { nuzyurli, seldei } => zbasu::run(nuzyurli, seldei).await?,
}
Ok(())
}

View File

@ -0,0 +1,66 @@
use anyhow::Result;
use rustls::internal::pemfile::{certs, rsa_private_keys};
use rustls::{
AllowAnyAnonymousOrAuthenticatedClient, Certificate, PrivateKey, RootCertStore, ServerConfig,
};
use std::fs::File;
use std::io::{self, BufReader};
use std::path::{Path, PathBuf};
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
pub struct Options {
/// host to listen on
#[structopt(short = "H", long, env = "HOST", default_value = "10.77.2.8")]
host: String,
/// port to listen on
#[structopt(short = "p", long, env = "PORT", default_value = "1965")]
port: u16,
/// cert file
#[structopt(short = "c", long = "cert", env = "CERT_FILE")]
cert: PathBuf,
/// key file
#[structopt(short = "k", long = "key", env = "KEY_FILE")]
key: PathBuf,
/// server hostname
#[structopt(
long = "hostname",
env = "SERVER_HOSTNAME",
default_value = "shachi.wg.akua"
)]
hostname: String,
}
fn load_certs(path: &Path) -> io::Result<Vec<Certificate>> {
certs(&mut BufReader::new(File::open(path)?))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert"))
}
fn load_keys(path: &Path) -> io::Result<Vec<PrivateKey>> {
rsa_private_keys(&mut BufReader::new(File::open(path)?))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))
}
pub async fn run(opts: Options) -> Result<()> {
let certs = load_certs(&opts.cert)?;
let mut keys = load_keys(&opts.key)?;
log::info!(
"serving gemini://{} on {}:{}",
opts.hostname,
opts.host,
opts.port
);
let mut config = ServerConfig::new(AllowAnyAnonymousOrAuthenticatedClient::new(
RootCertStore::empty(),
));
config
.set_single_cert(certs, keys.remove(0))
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
Ok(())
}

View File

@ -0,0 +1,89 @@
use anyhow::{anyhow, Result};
use atom_syndication as atom;
use chrono::{prelude::*, Duration};
use maj::gemini::Node;
use rustls::ClientConfig;
use std::io::{self, BufReader, Cursor, Write};
use std::ops::Sub;
use std::str;
mod tls;
fn gem_to_md(tcana: Vec<Node>, out: &mut impl Write) -> io::Result<()> {
use Node::*;
for tcan in tcana {
match tcan {
Text(body) => {
if body == "---" {
break;
}
write!(out, "{}\n", body)?;
}
Link { to, name } => match name {
Some(name) => write!(out, "[{}]({})\n\n", name, to)?,
None => write!(out, "[{0}]({0})", to)?,
},
Preformatted { alt, body } => write!(out, "```{}\n{}\n```\n\n", alt, body)?,
Heading { level, body } => {
write!(out, "##{} {}\n\n", "#".repeat(level as usize), body)?
}
ListItem(body) => write!(out, "* {}\n", body)?,
Quote(body) => write!(out, "> {}\n\n", body)?,
}
}
Ok(())
}
async fn read_feed(gurl: String, cfg: ClientConfig) -> Result<atom::Feed> {
let resp = maj::get(gurl, cfg).await?;
if resp.status != maj::StatusCode::Success {
Err(anyhow!(
"expected success, got: {} {}",
resp.status as u8,
resp.meta
))?;
}
let body = Cursor::new(resp.body);
let feed = atom::Feed::read_from(BufReader::new(body))?;
Ok(feed)
}
pub async fn run(nuzyurli: Vec<String>, seldei: usize) -> Result<()> {
let cfg = tls::config();
let ca: atom::FixedDateTime = Utc::now().into();
for urli in nuzyurli {
let feed = read_feed(urli.clone(), cfg.clone()).await?;
log::info!("reading entries for {}: {}", urli, feed.title);
println!("## {}\nBy {}\n\n", feed.title, feed.authors[0].name);
println!("[Site link]({})\n\n", feed.id);
for entry in feed.entries {
if ca.sub(entry.updated) > Duration::days(seldei as i64) {
continue;
}
let href: String = entry.links()[0].href.clone();
let resp = maj::get(href, cfg.clone()).await?;
if resp.status != maj::StatusCode::Success {
return Err(anyhow!(
"expected success, got: {} {}",
resp.status as u8,
resp.meta
));
}
let body = str::from_utf8(&resp.body)?;
let body = maj::gemini::parse(body);
let mut buf: Vec<u8> = vec![];
gem_to_md(body, &mut buf)?;
println!("{}", str::from_utf8(&buf)?);
}
}
Ok(())
}

View File

@ -0,0 +1,24 @@
use std::sync::Arc;
pub fn config() -> rustls::ClientConfig {
let mut config = rustls::ClientConfig::new();
config
.dangerous()
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
config
}
struct NoCertificateVerification {}
impl rustls::ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert(
&self,
_roots: &rustls::RootCertStore,
_presented_certs: &[rustls::Certificate],
_dns_name: webpki::DNSNameRef<'_>,
_ocsp: &[u8],
) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
Ok(rustls::ServerCertVerified::assertion())
}
}

View File

@ -1,10 +1,19 @@
{ pkgs ? import <nixpkgs> {} }: let
moz_overlay = import (builtins.fetchTarball
pkgs.mkShell { "https://github.com/mozilla/nixpkgs-mozilla/archive/master.tar.gz");
pkgs = import <nixpkgs> { overlays = [ moz_overlay ]; };
nur = import (builtins.fetchTarball
"https://github.com/nix-community/NUR/archive/master.tar.gz") {
inherit pkgs;
};
in pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
rustc cargo rls rustfmt cargo-watch pkgs.latest.rustChannels.stable.rust
cargo-watch
pkg-config pkg-config
ncurses ncurses
]; ];
RUST_LOG="info,majsite=debug,majsite::server=debug";
} }

View File

@ -3,16 +3,30 @@ name = "majsite"
version = "0.2.0" version = "0.2.0"
authors = ["Christine Dodrill <me@christine.website>"] authors = ["Christine Dodrill <me@christine.website>"]
edition = "2018" edition = "2018"
build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
structopt = "0.3"
tokio = { version = "0.2", features = ["rt-threaded", "macros"] }
tokio-rustls = { version = "0.14", features = ["dangerous_configuration"] }
async-trait = "0"
pretty_env_logger = "0.4"
log = "0"
anyhow = "1" anyhow = "1"
async-trait = "0"
dnd_dice_roller = "0.3"
env_logger = "0"
log = "0"
mime = "0.3.0"
percent-encoding = "2"
rand = "0"
rustls = { version = "0.19", features = ["dangerous_configuration"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
smol = { version = "0.3", features = ["tokio02"] }
structopt = "0.3"
url = "2"
warp = "0.2"
tokio = { version = "0.3", features = ["rt"] }
maj = { path = ".." } maj = { path = "..", features = ["server"], default-features = false }
[build-dependencies]
anyhow = "1"
ructe = { version = "0.11", features = ["warp02"] }

15
site/build.rs Normal file
View File

@ -0,0 +1,15 @@
use anyhow::Result;
use ructe::Ructe;
use std::process::Command;
fn main() -> Result<()> {
let mut ructe = Ructe::from_env()?;
let mut statics = ructe.statics()?;
statics.add_files("static")?;
ructe.compile_templates("templates")?;
let output = Command::new("git").args(&["rev-parse", "HEAD"]).output()?;
let git_hash = String::from_utf8(output.stdout)?;
println!("cargo:rustc-env=GITHUB_SHA={}", git_hash);
Ok(())
}

6
site/cgi-bin/env.sh Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
echo "20 text/plain"
echo "The following is the CGI environment of this program:"
echo
env

123
site/src/http.rs Normal file
View File

@ -0,0 +1,123 @@
use crate::templates::{self, statics::StaticFile, Html, RenderRucte, ToHtml};
use maj::server::Handler;
use maj::{gemini, server::Request as GemRequest, StatusCode};
use std::io::Write;
use std::sync::Arc;
use url::Url;
use warp::{filters::path::FullPath, http::Response, path, Filter, Rejection, Reply};
const HOST: &'static str = "cetacean.club"; // XXX(cadey): HACK
const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
async fn route(args: (FullPath, Arc<crate::server::Handler>)) -> Result<impl Reply, Rejection> {
let (path, h) = args;
let u = Url::parse(&format!("gemini://{}{}", HOST, path.as_str())).unwrap();
let req = GemRequest {
url: u.clone(),
addr: "127.0.0.1:8080".parse().unwrap(),
certs: None,
};
let resp = h.clone().handle(req).await.unwrap();
match resp.status {
StatusCode::Success => {
if resp.meta.starts_with("text/gemini") {
let (title, body) = gemtext_to_html(resp.body);
Response::builder().html(|o| templates::page_html(o, title, body))
} else {
Response::builder()
.status(warp::http::StatusCode::INTERNAL_SERVER_ERROR)
.html(|o| {
templates::error_html(o, u.to_string(), "cannot proxy this yet".to_string())
})
}
}
StatusCode::PermanentRedirect => {
let uu = Url::parse(&resp.meta).expect("url parsing to work");
log::info!("uu: {}", uu.to_string());
Response::builder()
.status(warp::http::StatusCode::PERMANENT_REDIRECT)
.header("Location", uu.path())
.html(|o| {
templates::error_html(
o,
u.to_string(),
format!("forwarding you to {}", uu.path()),
)
})
}
_ => Response::builder()
.status(warp::http::StatusCode::INTERNAL_SERVER_ERROR)
.html(|o| templates::error_html(o, u.to_string(), resp.meta)),
}
}
fn gemtext_to_html(inp: Vec<u8>) -> (String, impl ToHtml) {
use gemini::Node::*;
let mut title: String = "Unknown Title".to_string();
let inp = std::str::from_utf8(&inp).unwrap();
let nodes = gemini::parse(inp);
let mut buf: Vec<u8> = Vec::new();
for node in &nodes {
match node {
Heading { level, body } => {
if *level == 1 {
title = body.to_string();
}
write!(buf, "<h{0}>{1}</h{0}>", level, body).unwrap();
}
Text(body) => write!(buf, "{}\n<br />", body).unwrap(),
Link { to, name } => write!(
buf,
r#"<a href="{}">{}</a><br />"#,
to,
name.as_ref().or(Some(&to.to_string())).unwrap()
)
.unwrap(),
Preformatted { alt, body } => write!(
buf,
"<code><pre title = \"{}\">{}</pre></code>",
alt.replace("\"", "\\\"").replace("\\", "\\\\"),
body
).unwrap(),
ListItem(body) => write!(buf, "<li>{}</li>", body).unwrap(),
Quote(body) => write!(buf, "<blockquote>{}</blockquote>", body).unwrap(),
}
}
(title, Html(String::from_utf8(buf).unwrap()))
}
pub fn run(h: Arc<crate::server::Handler>, port: u16) {
smol::run(async {
let h = h.clone();
let handler = warp::path::full()
.map(move |path: FullPath| (path, h.clone()))
.and_then(route);
let statics = path("static").and(path::param()).and_then(static_file);
let site = statics.or(handler).with(warp::log(APPLICATION_NAME));
warp::serve(site).run(([0, 0, 0, 0], port)).await;
});
}
/// Handler for static files.
/// Create a response from the file data with a correct content type
/// and a far expires header (or a 404 if the file does not exist).
async fn static_file(name: String) -> Result<impl Reply, Rejection> {
if let Some(data) = StaticFile::get(&name) {
Ok(Response::builder()
.status(warp::http::StatusCode::OK)
.header("content-type", data.mime.as_ref())
// TODO .header("expires", _far_expires)
.body(data.content))
} else {
println!("Static file {} not found", name);
Err(warp::reject::not_found())
}
}

View File

@ -1,21 +0,0 @@
# maj
```
__
_____ _____ |__|
/ \ \__ \ | |
| Y Y \ / __ \_ | |
|__|_| /(____ //\__| |
\/ \/ \______|
```
A gemini ecosystem for Rust
=> gemini://gemini.circumlunar.space/ Gemini homepage
## Homepage
The main homepage for maj is on tulpa.dev:
=> https://tulpa.dev/cadey/maj
## Projects
=> /majc majc, an interactive curses client
=> /majd majd, a high-power gemini server with Lua capabilities

View File

@ -1,9 +1,19 @@
use std::fs::File; use rustls::{
use std::io::{self, BufReader}; internal::pemfile::{certs, pkcs8_private_keys},
use std::path::{Path, PathBuf}; AllowAnyAnonymousOrAuthenticatedClient, Certificate, PrivateKey, RootCertStore, ServerConfig,
};
use std::{
fs::File,
io::{self, BufReader},
path::{Path, PathBuf},
sync::Arc,
thread,
};
use structopt::StructOpt; use structopt::StructOpt;
use tokio_rustls::rustls::internal::pemfile::{certs, rsa_private_keys};
use tokio_rustls::rustls::{Certificate, NoClientAuth, PrivateKey, ServerConfig}; mod http;
mod server;
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
struct Options { struct Options {
@ -23,13 +33,25 @@ struct Options {
#[structopt(short = "k", long = "key", env = "KEY_FILE")] #[structopt(short = "k", long = "key", env = "KEY_FILE")]
key: PathBuf, key: PathBuf,
/// static path
#[structopt(short = "s", long, env = "STATIC_PATH", default_value = "./static")]
static_path: PathBuf,
/// CGI path
#[structopt(short = "C", long, env = "CGI_PATH", default_value = "./cgi-bin")]
cgi_path: PathBuf,
/// server hostname /// server hostname
#[structopt( #[structopt(
long = "hostname", long = "hostname",
env = "SERVER_HOSTNAME", env = "SERVER_HOSTNAME",
default_value = "maj.kahless.cetacean.club" default_value = "cetacean.club"
)] )]
hostname: String, hostname: String,
/// HTTP port
#[structopt(long, default_value = "34587")]
http_port: u16,
} }
fn load_certs(path: &Path) -> io::Result<Vec<Certificate>> { fn load_certs(path: &Path) -> io::Result<Vec<Certificate>> {
@ -38,85 +60,46 @@ fn load_certs(path: &Path) -> io::Result<Vec<Certificate>> {
} }
fn load_keys(path: &Path) -> io::Result<Vec<PrivateKey>> { fn load_keys(path: &Path) -> io::Result<Vec<PrivateKey>> {
rsa_private_keys(&mut BufReader::new(File::open(path)?)) pkcs8_private_keys(&mut BufReader::new(File::open(path)?))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key")) .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))
} }
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), maj::server::Error> { async fn main() -> Result<(), maj::server::Error> {
pretty_env_logger::init(); env_logger::init();
let opts = Options::from_args(); let opts = Options::from_args();
let certs = load_certs(&opts.cert)?; let certs = load_certs(&opts.cert).unwrap();
let mut keys = load_keys(&opts.key)?; let mut keys = load_keys(&opts.key).unwrap();
log::info!("{:?}", opts); log::info!(
"serving gemini://{} on {}:{}",
opts.hostname,
opts.host,
opts.port
);
let mut config = ServerConfig::new(NoClientAuth::new()); let mut config = ServerConfig::new(AllowAnyAnonymousOrAuthenticatedClient::new(
RootCertStore::empty(),
));
config config
.set_single_cert(certs, keys.remove(0)) .set_single_cert(certs, keys.remove(0))
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
maj::server::serve( let h = Arc::new(server::Handler {
&Handler { hostname: opts.hostname,
hostname: opts.hostname, files: maj::server::files::Handler::new(opts.static_path),
}, cgi: maj::server::cgi::Handler::new(opts.cgi_path),
config, });
opts.host,
opts.port, {
) let port = opts.http_port.clone();
.await?; let h = h.clone();
thread::spawn(move || http::run(h.clone(), port));
}
maj::server::serve(h.clone(), config, opts.host, opts.port).await?;
Ok(()) Ok(())
} }
struct Handler { include!(concat!(env!("OUT_DIR"), "/templates.rs"));
hostname: String,
}
fn index() -> Result<maj::Response, maj::server::Error> {
let msg = include_bytes!("index.gmi");
Ok(maj::Response {
status: maj::StatusCode::Success,
meta: "text/gemini".to_string(),
body: msg.to_vec(),
})
}
fn majc() -> Result<maj::Response, maj::server::Error> {
let msg = include_bytes!("majc.gmi");
Ok(maj::Response {
status: maj::StatusCode::Success,
meta: "text/gemini".to_string(),
body: msg.to_vec(),
})
}
#[async_trait::async_trait]
impl maj::server::Handler for Handler {
async fn handle(&self, r: maj::server::Request) -> Result<maj::Response, maj::server::Error> {
if r.url.has_host() && r.url.host_str().unwrap().to_string() != self.hostname {
return Ok(maj::Response {
status: maj::StatusCode::ProxyRequestRefused,
meta: "Wrong host".to_string(),
body: vec![],
});
}
match r.url.path() {
"" => Ok(maj::Response {
status: maj::StatusCode::PermanentRedirect,
meta: format!("gemini://{}/", self.hostname),
body: vec![],
}),
"/" => index(),
"/majc" => majc(),
_ => Ok(maj::Response {
status: maj::StatusCode::NotFound,
meta: "Not found".to_string(),
body: vec![],
}),
}
}
}

View File

@ -1,39 +0,0 @@
# majc
```
__
_____ _____ |__| ____
/ \ \__ \ | |_/ ___\
| Y Y \ / __ \_ | |\ \___
|__|_| /(____ //\__| | \___ >
\/ \/ \______| \/
```
A curses client for Gemini!
## Homepage
The main homepage for majc is on tulpa.dev:
=> https://tulpa.dev/cadey/maj
## Installation
majc can be installed using Nix:
```
$ nix-env -if https://tulpa.dev/cadey/maj/archive/master.tar.gz -A majc
```
Then you can run it with `majc`:
```
$ majc
```
## Important Keys
<esc>: opens the menubar
c: closes the active window
o: prompts to open a URL
q: quits majc
?: shows this screen
~: toggles the debug logging pane
---
=> / Go back

90
site/src/server.rs Normal file
View File

@ -0,0 +1,90 @@
use dnd_dice_roller::{dice::Dice, error::DiceError};
use maj::{
gemini::Builder,
route, seg,
server::{Error, Handler as MajHandler, Request},
split, Response,
};
use percent_encoding::percent_decode_str;
use std::str::FromStr;
mod tarot;
pub struct Handler {
pub hostname: String,
pub files: maj::server::files::Handler,
pub cgi: maj::server::cgi::Handler,
}
async fn dice(req: Request) -> Result<Response, Error> {
fn dice_roll<T: Into<String>>(roll: T) -> Result<String, DiceError> {
let mut dice = Dice::from_str(&roll.into())?;
if dice.number_of_dice_to_roll > 100 {
dice.number_of_dice_to_roll = 100;
}
if dice.sides > 100 {
dice.sides = 100
}
if dice.sides == 0 {
dice.sides = 6;
}
let res = dice.roll_dice();
let reply = format!(
"{}{} = {}\n",
res.dice_results,
match dice.modifier {
Some(amt) => format!(" + {}", amt),
None => "".into(),
},
res.final_result[0]
);
Ok(reply)
}
match req.url.query() {
None => Ok(Response::input(
"What do you want to roll? [n]dn[+n] [adv|dadv]",
)),
Some(q) => Ok({
let dice = percent_decode_str(q).decode_utf8()?;
let b = Builder::new()
.heading(1, "Dice Results")
.text("")
.text(format!("You rolled {} and you got:", dice))
.text("")
.preformatted("", format!("{}", dice_roll(dice)?))
.text("")
.link("/dice", Some("Do another roll".to_string()));
Response::render(b.build())
}),
}
}
#[async_trait::async_trait]
impl MajHandler for Handler {
async fn handle(&self, req: Request) -> Result<Response, Error> {
if req.url.has_host() && req.url.host_str().unwrap().to_string() != self.hostname {
return Ok(Response::no_proxy());
}
if req.url.path() == "" {
return Ok(Response::perm_redirect(format!(
"gemini://{}/",
self.hostname
)));
}
route!(req.url.path(), {
(/"dice") => dice(req).await;
(/"tools"/"character_gen") => tarot::character().await;
(/"cgi-bin"[/rest..]) => self.cgi.handle(req).await;
});
self.files.handle(req).await
}
}

819
site/src/server/tarot.json Normal file
View File

@ -0,0 +1,819 @@
{
"count": 78,
"cards": [
{
"name": "The Magician",
"name_short": "ar01",
"value": "1",
"value_int": 1,
"meaning_up": "Skill, diplomacy, address, subtlety; sickness, pain, loss, disaster, snares of enemies; self-confidence, will; the Querent, if male. ",
"meaning_rev": "Physician, Magus, mental disease, disgrace, disquiet.",
"type": "major"
},
{
"name": "The High Priestess",
"name_short": "ar02",
"value": "2",
"value_int": 2,
"meaning_up": "Secrets, mystery, the future as yet unrevealed; the woman who interests the Querent, if male; the Querent herself, if female; silence, tenacity; mystery, wisdom, science. ",
"meaning_rev": "Passion, moral or physical ardour, conceit, surface knowledge.",
"type": "major"
},
{
"name": "The Empress",
"name_short": "ar03",
"value": "3",
"value_int": 3,
"meaning_up": "Fruitfulness, action, initiative, length of days; the unknown, clandestine; also difficulty, doubt, ignorance. ",
"meaning_rev": "Light, truth, the unravelling of involved matters, public rejoicings; according to another reading, vacillation.",
"type": "major"
},
{
"name": "The Emperor",
"name_short": "ar04",
"value": "4",
"value_int": 4,
"meaning_up": "Stability, power, protection, realization; a great person; aid, reason, conviction; also authority and will. ",
"meaning_rev": "Benevolence, compassion, credit; also confusion to enemies, obstruction, immaturity.",
"type": "major"
},
{
"name": "The Hierophant",
"name_short": "ar05",
"value": "5",
"value_int": 5,
"meaning_up": "Marriage, alliance, captivity, servitude; by another account, mercy and goodness; inspiration; the man to whom the Querent has recourse. ",
"meaning_rev": "Society, good understanding, concord, overkindness, weakness.",
"type": "major"
},
{
"name": "The Lovers",
"name_short": "ar06",
"value": "6",
"value_int": 6,
"meaning_up": "Attraction, love, beauty, trials overcome. ",
"meaning_rev": "Failure, foolish designs. Another account speaks of marriage frustrated and contrarieties of all kinds.",
"type": "major"
},
{
"name": "The Chariot",
"name_short": "ar07",
"value": "7",
"value_int": 7,
"meaning_up": "Succour, providence also war, triumph, presumption, vengeance, trouble. ",
"meaning_rev": "Riot, quarrel, dispute, litigation, defeat.",
"type": "major"
},
{
"name": "Fortitude",
"name_short": "ar08",
"value": "8",
"value_int": 8,
"meaning_up": "Power, energy, action, courage, magnanimity; also complete success and honours. ",
"meaning_rev": "Despotism, abuse if power, weakness, discord, sometimes even disgrace.",
"type": "major"
},
{
"name": "The Hermit",
"name_short": "ar09",
"value": "9",
"value_int": 9,
"meaning_up": "Prudence, circumspection; also and especially treason, dissimulation, roguery, corruption. ",
"meaning_rev": "Concealment, disguise, policy, fear, unreasoned caution.",
"type": "major"
},
{
"name": "Wheel Of Fortune",
"name_short": "ar10",
"value": "10",
"value_int": 10,
"meaning_up": "Destiny, fortune, success, elevation, luck, felicity. ",
"meaning_rev": "Increase, abundance, superfluity.",
"type": "major"
},
{
"name": "Justice",
"name_short": "ar11",
"value": "11",
"value_int": 11,
"meaning_up": "Equity, rightness, probity, executive; triumph of the deserving side in law. ",
"meaning_rev": "Law in all its departments, legal complications, bigotry, bias, excessive severity.",
"type": "major"
},
{
"name": "The Hanged Man",
"name_short": "ar12",
"value": "12",
"value_int": 12,
"meaning_up": "Wisdom, circumspection, discernment, trials, sacrifice, intuition, divination, prophecy. ",
"meaning_rev": "Selfishness, the crowd, body politic.",
"type": "major"
},
{
"name": "Death",
"name_short": "ar13",
"value": "13",
"value_int": 13,
"meaning_up": "End, mortality, destruction, corruption also, for a man, the loss of a benefactor for a woman, many contrarieties; for a maid, failure of marriage projects. ",
"meaning_rev": "Inertia, sleep, lethargy, petrifaction, somnambulism; hope destroyed.",
"type": "major"
},
{
"name": "Temperance",
"name_short": "ar14",
"value": "14",
"value_int": 14,
"meaning_up": "Economy, moderation, frugality, management, accommodation. ",
"meaning_rev": "Things connected with churches, religions, sects, the priesthood, sometimes even the priest who will marry the Querent; also disunion, unfortunate combinations, competing interests.",
"type": "major"
},
{
"name": "The Devil",
"name_short": "ar15",
"value": "15",
"value_int": 15,
"meaning_up": "Ravage, violence, vehemence, extraordinary efforts, force, fatality; that which is predestined but is not for this reason evil. ",
"meaning_rev": "Evil fatality, weakness, pettiness, blindness.",
"type": "major"
},
{
"name": "The Tower",
"name_short": "ar16",
"value": "16",
"value_int": 16,
"meaning_up": "Misery, distress, indigence, adversity, calamity, disgrace, deception, ruin. It is a card in particular of unforeseen catastrophe. ",
"meaning_rev": "According to one account, the same in a lesser degree also oppression, imprisonment, tyranny.",
"type": "major"
},
{
"name": "The Star",
"name_short": "ar17",
"value": "17",
"value_int": 17,
"meaning_up": "Loss, theft, privation, abandonment; another reading says-hope and bright prospects.",
"meaning_rev": "Arrogance, haughtiness, impotence.",
"type": "major"
},
{
"name": "The Moon",
"name_short": "ar18",
"value": "18",
"value_int": 18,
"meaning_up": "Hidden enemies, danger, calumny, darkness, terror, deception, occult forces, error. ",
"meaning_rev": "Instability, inconstancy, silence, lesser degrees of deception and error.",
"type": "major"
},
{
"name": "The Sun",
"name_short": "ar19",
"value": "19",
"value_int": 19,
"meaning_up": "Material happiness, fortunate marriage, contentment. ",
"meaning_rev": "The same in a lesser sense.",
"type": "major"
},
{
"name": "The Last Judgment",
"name_short": "ar20",
"value": "20",
"value_int": 20,
"meaning_up": "Change of position, renewal, outcome. Another account specifies total loss though lawsuit. ",
"meaning_rev": "Weakness, pusillanimity, simplicity; also deliberation, decision, sentence.",
"type": "major"
},
{
"name": "The Fool",
"name_short": "ar00",
"value": "zero",
"value_int": 0,
"meaning_up": "Folly, mania, extravagance, intoxication, delirium, frenzy, bewrayment. ",
"meaning_rev": "Negligence, absence, distribution, carelessness, apathy, nullity, vanity.",
"type": "major"
},
{
"name": "The World",
"name_short": "ar21",
"value": "21",
"value_int": 21,
"meaning_up": "Assured success, recompense, voyage, route, emigration, flight, change of place. ",
"meaning_rev": "Inertia, fixity, stagnation, permanence.",
"type": "major"
},
{
"value": "page",
"value_int": 11,
"name": "Page of Wands",
"name_short": "wapa",
"suit": "wands",
"meaning_up": "Dark young man, faithful, a lover, an envoy, a postman. Beside a man, he will bear favourable testimony concerning him. A dangerous rival, if followed by the Page of Cups. Has the chief qualities of his suit. He may signify family intelligence. ",
"meaning_rev": "Anecdotes, announcements, evil news. Also indecision and the instability which accompanies it.",
"type": "minor",
"desc": "In a scene similar to the former, a young man stands in the act of proclamation. He is unknown but faithful, and his tidings are strange. "
},
{
"value": "knight",
"value_int": 12,
"name": "Knight of Wands",
"name_short": "wakn",
"suit": "wands",
"meaning_up": "Departure, absence, flight, emigration. A dark young man, friendly. Change of residence. ",
"meaning_rev": "Rupture, division, interruption, discord.",
"type": "minor",
"desc": "He is shewn as if upon a journey, armed with a short wand, and although mailed is not on a warlike errand. He is passing mounds or pyramids. The motion of the horse is a key to the character of its rider, and suggests the precipitate mood, or things connected therewith. "
},
{
"value": "queen",
"value_int": 13,
"name": "Queen of Wands",
"name_short": "waqu",
"suit": "wands",
"meaning_up": "A dark woman, countrywoman, friendly, chaste, loving, honourable. If the card beside her signifies a man, she is well disposed towards him; if a woman, she is interested in the Querent. Also, love of money, or a certain success in business. ",
"meaning_rev": "Good, economical, obliging, serviceable. Signifies also--but in certain positions and in the neighbourhood of other cards tending in such directions--opposition, jealousy, even deceit and infidelity.",
"type": "minor",
"desc": "The Wands throughout this suit are always in leaf, as it is a suit of life and animation. Emotionally and otherwise, the Queen's personality corresponds to that of the King, but is more magnetic. "
},
{
"value": "king",
"value_int": 14,
"name": "King of Wands",
"name_short": "waki",
"suit": "wands",
"meaning_up": "Dark man, friendly, countryman, generally married, honest and conscientious. The card always signifies honesty, and may mean news concerning an unexpected heritage to fall in before very long. ",
"meaning_rev": "Good, but severe; austere, yet tolerant.",
"type": "minor",
"desc": "The physical and emotional nature to which this card is attributed is dark, ardent, lithe, animated, impassioned, noble. The King uplifts a flowering wand, and wears, like his three correspondences in the remaining suits, what is called a cap of maintenance beneath his crown. He connects with the symbol of the lion, which is emblazoned on the back of his throne. "
},
{
"value": "ace",
"value_int": 1,
"name": "Ace of Wands",
"name_short": "waac",
"suit": "wands",
"meaning_up": "Creation, invention, enterprise, the powers which result in these; principle, beginning, source; birth, family, origin, and in a sense the virility which is behind them; the starting point of enterprises; according to another account, money, fortune, inheritance. ",
"meaning_rev": "Fall, decadence, ruin, perdition, to perish also a certain clouded joy.",
"type": "minor",
"desc": "A hand issuing from a cloud grasps a stout wand or club. "
},
{
"value": "two",
"value_int": 2,
"name": "Two of Wands",
"name_short": "wa02",
"suit": "wands",
"meaning_up": "Between the alternative readings there is no marriage possible; on the one hand, riches, fortune, magnificence; on the other, physical suffering, disease, chagrin, sadness, mortification. The design gives one suggestion; here is a lord overlooking his dominion and alternately contemplating a globe; it looks like the malady, the mortification, the sadness of Alexander amidst the grandeur of this world's wealth. ",
"meaning_rev": "Surprise, wonder, enchantment, emotion, trouble, fear.",
"type": "minor",
"desc": "A tall man looks from a battlemented roof over sea and shore; he holds a globe in his right hand, while a staff in his left rests on the battlement; another is fixed in a ring. The Rose and Cross and Lily should be noticed on the left side. "
},
{
"value": "three",
"value_int": 3,
"name": "Three of Wands",
"name_short": "wa03",
"suit": "wands",
"meaning_up": "He symbolizes established strength, enterprise, effort, trade, commerce, discovery; those are his ships, bearing his merchandise, which are sailing over the sea. The card also signifies able co-operation in business, as if the successful merchant prince were looking from his side towards yours with a view to help you. ",
"meaning_rev": "The end of troubles, suspension or cessation of adversity, toil and disappointment.",
"type": "minor",
"desc": "A calm, stately personage, with his back turned, looking from a cliff's edge at ships passing over the sea. Three staves are planted in the ground, and he leans slightly on one of them. "
},
{
"value": "four",
"value_int": 4,
"name": "Four of Wands",
"name_short": "wa04",
"suit": "wands",
"meaning_up": "They are for once almost on the surface--country life, haven of refuge, a species of domestic harvest-home, repose, concord, harmony, prosperity, peace, and the perfected work of these. ",
"meaning_rev": "The meaning remains unaltered; it is prosperity, increase, felicity, beauty, embellishment.",
"type": "minor",
"desc": "From the four great staves planted in the foreground there is a great garland suspended; two female figures uplift nosegays; at their side is a bridge over a moat, leading to an old manorial house. "
},
{
"value": "five",
"value_int": 5,
"name": "Five of Wands",
"name_short": "wa05",
"suit": "wands",
"meaning_up": "Imitation, as, for example, sham fight, but also the strenuous competition and struggle of the search after riches and fortune. In this sense it connects with the battle of life. Hence some attributions say that it is a card of gold, gain, opulence. ",
"meaning_rev": "Litigation, disputes, trickery, contradiction.",
"type": "minor",
"desc": "A posse of youths, who are brandishing staves, as if in sport or strife. It is mimic warfare, and hereto correspond the "
},
{
"value": "six",
"value_int": 6,
"name": "Six of Wands",
"name_short": "wa06",
"suit": "wands",
"meaning_up": "The card has been so designed that it can cover several significations; on the surface, it is a victor triumphing, but it is also great news, such as might be carried in state by the King's courier; it is expectation crowned with its own desire, the crown of hope, and so forth. ",
"meaning_rev": "Apprehension, fear, as of a victorious enemy at the gate; treachery, disloyalty, as of gates being opened to the enemy; also indefinite delay.",
"type": "minor",
"desc": "A laurelled horseman bears one staff adorned with a laurel crown; footmen with staves are at his side. "
},
{
"value": "seven",
"value_int": 7,
"name": "Seven of Wands",
"name_short": "wa07",
"suit": "wands",
"meaning_up": "It is a card of valour, for, on the surface, six are attacking one, who has, however, the vantage position. On the intellectual plane, it signifies discussion, wordy strife; in business--negotiations, war of trade, barter, competition. It is further a card of success, for the combatant is on the top and his enemies may be unable to reach him. ",
"meaning_rev": "Perplexity, embarrassments, anxiety. It is also a caution against indecision.",
"type": "minor",
"desc": "A young man on a craggy eminence brandishing a staff; six other staves are raised towards him from below. "
},
{
"value": "eight",
"value_int": 8,
"name": "Eight of Wands",
"name_short": "wa08",
"suit": "wands",
"meaning_up": "Activity in undertakings, the path of such activity, swiftness, as that of an express messenger; great haste, great hope, speed towards an end which promises assured felicity; generally, that which is on the move; also the arrows of love. ",
"meaning_rev": "Arrows of jealousy, internal dispute, stingings of conscience, quarrels; and domestic disputes for persons who are married.",
"type": "minor",
"desc": "The card represents motion through the immovable-a flight of wands through an open country; but they draw to the term of their course. That which they signify is at hand; it may be even on the threshold. "
},
{
"value": "nine",
"value_int": 9,
"name": "Nine of Wands",
"name_short": "wa09",
"suit": "wands",
"meaning_up": "The card signifies strength in opposition. If attacked, the person will meet an onslaught boldly; and his build shews, that he may prove a formidable antagonist. With this main significance there are all its possible adjuncts--delay, suspension, adjournment. ",
"meaning_rev": "Obstacles, adversity, calamity.",
"type": "minor",
"desc": "The figure leans upon his staff and has an expectant look, as if awaiting an enemy. Behind are eight other staves--erect, in orderly disposition, like a palisade. "
},
{
"value": "ten",
"value_int": 10,
"name": "Ten of Wands",
"name_short": "wa10",
"suit": "wands",
"meaning_up": "A card of many significances, and some of the readings cannot be harmonized. I set aside that which connects it with honour and good faith. The chief meaning is oppression simply, but it is also fortune, gain, any kind of success, and then it is the oppression of these things. It is also a card of false-seeming, disguise, perfidy. The place which the figure is approaching may suffer from the rods that he carries. Success is stultified if the Nine of Swords follows, and if it is a question of a lawsuit, there will be certain loss. ",
"meaning_rev": "Contrarieties, difficulties, intrigues, and their analogies.",
"type": "minor",
"desc": "A man oppressed by the weight of the ten staves which he is carrying. "
},
{
"value": "page",
"value_int": 11,
"name": "Page of Cups",
"name_short": "cupa",
"suit": "cups",
"meaning_up": "Fair young man, one impelled to render service and with whom the Querent will be connected; a studious youth; news, message; application, reflection, meditation; also these things directed to business. ",
"meaning_rev": "Taste, inclination, attachment, seduction, deception, artifice.",
"type": "minor",
"desc": "A fair, pleasing, somewhat effeminate page, of studious and intent aspect, contemplates a fish rising from a cup to look at him. It is the pictures of the mind taking form. "
},
{
"value": "knight",
"value_int": 12,
"name": "Knight of Cups",
"name_short": "cukn",
"suit": "cups",
"meaning_up": "Arrival, approach--sometimes that of a messenger; advances, proposition, demeanour, invitation, incitement. ",
"meaning_rev": "Trickery, artifice, subtlety, swindling, duplicity, fraud.",
"type": "minor",
"desc": "Graceful, but not warlike; riding quietly, wearing a winged helmet, referring to those higher graces of the imagination which sometimes characterize this card. He too is a dreamer, but the images of the side of sense haunt him in his vision. "
},
{
"value": "queen",
"value_int": 13,
"name": "Queen of Cups",
"name_short": "cuqu",
"suit": "cups",
"meaning_up": "Good, fair woman; honest, devoted woman, who will do service to the Querent; loving intelligence, and hence the gift of vision; success, happiness, pleasure; also wisdom, virtue; a perfect spouse and a good mother. ",
"meaning_rev": "The accounts vary; good woman; otherwise, distinguished woman but one not to be trusted; perverse woman; vice, dishonour, depravity.",
"type": "minor",
"desc": "Beautiful, fair, dreamy--as one who sees visions in a cup. This is, however, only one of her aspects; she sees, but she also acts, and her activity feeds her dream. "
},
{
"value": "king",
"value_int": 14,
"name": "King of Cups",
"name_short": "cuki",
"suit": "cups",
"meaning_up": "Fair man, man of business, law, or divinity; responsible, disposed to oblige the Querent; also equity, art and science, including those who profess science, law and art; creative intelligence. ",
"meaning_rev": "Dishonest, double-dealing man; roguery, exaction, injustice, vice, scandal, pillage, considerable loss.",
"type": "minor",
"desc": "He holds a short sceptre in his left hand and a great cup in his right; his throne is set upon the sea; on one side a ship is riding and on the other a dolphin is leaping. The implicit is that the Sign of the Cup naturally refers to water, which appears in all the court cards. "
},
{
"value": "ace",
"value_int": 1,
"name": "Ace of Cups",
"name_short": "cuac",
"suit": "cups",
"meaning_up": "House of the true heart, joy, content, abode, nourishment, abundance, fertility; Holy Table, felicity hereof. ",
"meaning_rev": "House of the false heart, mutation, instability, revolution.",
"type": "minor",
"desc": "The waters are beneath, and thereon are water-lilies; the hand issues from the cloud, holding in its palm the cup, from which four streams are pouring; a dove, bearing in its bill a cross-marked Host, descends to place the Wafer in the Cup; the dew of water is falling on all sides. It is an intimation of that which may lie behind the Lesser Arcana. "
},
{
"value": "two",
"value_int": 2,
"name": "Two of Cups",
"name_short": "cu02",
"suit": "cups",
"meaning_up": "Love, passion, friendship, affinity, union, concord, sympathy, the interrelation of the sexes, and--as a suggestion apart from all offices of divination--that desire which is not in Nature, but by which Nature is sanctified",
"meaning_rev": " and maiden are pledging one another, and above their cups rises the Caduceus of Hermes, between the great wings of which there appears a lion's head. It is a variant of a sign which is found in a few old examples of this card. Some curious emblematical meanings are attached to it, but they do not concern us in this place. Divinatory Meanings: Love, passion, friendship, affinity, union, concord, sympathy, the interrelation of the sexes, and--as a suggestion apart from all offices of divination--that desire which is not in Nature, but by which Nature is sanctified.",
"type": "minor",
"desc": "A youth and maiden are pledging one another, and above their cups rises the Caduceus of Hermes, between the great wings of which there appears a lion's head. It is a variant of a sign which is found in a few old examples of this card. Some curious emblematical meanings are attached to it, but they do not concern us in this place. "
},
{
"value": "three",
"value_int": 3,
"name": "Three of Cups",
"name_short": "cu03",
"suit": "cups",
"meaning_up": "The conclusion of any matter in plenty, perfection and merriment; happy issue, victory, fulfilment, solace, healing, ",
"meaning_rev": "Expedition, dispatch, achievement, end. It signifies also the side of excess in physical enjoyment, and the pleasures of the senses.",
"type": "minor",
"desc": "Maidens in a garden-ground with cups uplifted, as if pledging one another. "
},
{
"value": "four",
"value_int": 4,
"name": "Four of Cups",
"name_short": "cu04",
"suit": "cups",
"meaning_up": "Weariness, disgust, aversion, imaginary vexations, as if the wine of this world had caused satiety only; another wine, as if a fairy gift, is now offered the wastrel, but he sees no consolation therein. This is also a card of blended pleasure. ",
"meaning_rev": "Novelty, presage, new instruction, new relations.",
"type": "minor",
"desc": "A young man is seated under a tree and contemplates three cups set on the grass before him; an arm issuing from a cloud offers him another cup. His expression notwithstanding is one of discontent with his environment. "
},
{
"value": "five",
"value_int": 5,
"name": "Five of Cups",
"name_short": "cu05",
"suit": "cups",
"meaning_up": "gure, looking sideways at three prone cups two others stand upright behind him; a bridge is in the background, leading to a small keep or holding. Divanatory Meanings: It is a card of loss, but something remains over; three have been taken, but two are left; it is a card of inheritance, patrimony, transmission, but not corresponding to expectations; with some interpreters it is a card of marriage, but not without bitterness or frustration. ",
"meaning_rev": "News, alliances, affinity, consanguinity, ancestry, return, false projects.",
"type": "minor",
"desc": "A dark, cloaked figure, looking sideways at three prone cups two others stand upright behind him; a bridge is in the background, leading to a small keep or holding. Divanatory Meanings: It is a card of loss, but something remains over; three have been taken, but two are left; it is a card of inheritance, patrimony, transmission, but not corresponding to expectations; with some interpreters it is a card of marriage, but not without bitterness or frustration. Reversed: News, alliances, affinity, consanguinity, ancestry, return, false projects"
},
{
"value": "six",
"value_int": 6,
"name": "Six of Cups",
"name_short": "cu06",
"suit": "cups",
"meaning_up": "A card of the past and of memories, looking back, as--for example--on childhood; happiness, enjoyment, but coming rather from the past; things that have vanished. Another reading reverses this, giving new relations, new knowledge, new environment, and then the children are disporting in an unfamiliar precinct. ",
"meaning_rev": "The future, renewal, that which will come to pass presently.",
"type": "minor",
"desc": "Children in an old garden, their cups filled with flowers. "
},
{
"value": "seven",
"value_int": 7,
"name": "Seven of Cups",
"name_short": "cu07",
"suit": "cups",
"meaning_up": "Fairy favours, images of reflection, sentiment, imagination, things seen in the glass of contemplation; some attainment in these degrees, but nothing permanent or substantial is suggested. ",
"meaning_rev": "Desire, will, determination, project.",
"type": "minor",
"desc": "Strange chalices of vision, but the images are more especially those of the fantastic spirit. "
},
{
"value": "eight",
"value_int": 8,
"name": "Eight of Cups",
"name_short": "cu08",
"suit": "cups",
"meaning_up": "The card speaks for itself on the surface, but other readings are entirely antithetical--giving joy, mildness, timidity, honour, modesty. In practice, it is usually found that the card shews the decline of a matter, or that a matter which has been thought to be important is really of slight consequence--either for good or evil. ",
"meaning_rev": "Great joy, happiness, feasting.",
"type": "minor",
"desc": "A man of dejected aspect is deserting the cups of his felicity, enterprise, undertaking or previous concern. "
},
{
"value": "nine",
"value_int": 9,
"name": "Nine of Cups",
"name_short": "cu09",
"suit": "cups",
"meaning_up": "Concord, contentment, physical bien-ĂŞtre; also victory, success, advantage; satisfaction for the Querent or person for whom the consultation is made. ",
"meaning_rev": "Truth, loyalty, liberty; but the readings vary and include mistakes, imperfections, etc.",
"type": "minor",
"desc": "A goodly personage has feasted to his heart's content, and abundant refreshment of wine is on the arched counter behind him, seeming to indicate that the future is also assured. The picture offers the material side only, but there are other aspects. "
},
{
"value": "ten",
"value_int": 10,
"name": "Ten of Cups",
"name_short": "cu10",
"suit": "cups",
"meaning_up": "Contentment, repose of the entire heart; the perfection of that state; also perfection of human love and friendship; if with several picture-cards, a person who is taking charge of the Querent's interests; also the town, village or country inhabited by the Querent. ",
"meaning_rev": "Repose of the false heart, indignation, violence.",
"type": "minor",
"desc": "Appearance of Cups in a rainbow; it is contemplated in wonder and ecstacy by a man and woman below, evidently husband and wife. His right arm is about her; his left is raised upward; she raises her right arm. The two children dancing near them have not observed the prodigy but are happy after their own manner. There is a home-scene beyond. "
},
{
"value": "page",
"value_int": 11,
"name": "Page of Pentacles",
"name_short": "pepa",
"suit": "pentacles",
"meaning_up": "Application, study, scholarship, reflection another reading says news, messages and the bringer thereof; also rule, management. ",
"meaning_rev": "Prodigality, dissipation, liberality, luxury; unfavourable news.",
"type": "minor",
"desc": "A youthful figure, looking intently at the pentacle which hovers over his raised hands. He moves slowly, insensible of that which is about him. "
},
{
"value": "knight",
"value_int": 12,
"name": "Knight of Pentacles",
"name_short": "pekn",
"suit": "pentacles",
"meaning_up": "Utility, serviceableness, interest, responsibility, rectitude-all on the normal and external plane. ",
"meaning_rev": "inertia, idleness, repose of that kind, stagnation; also placidity, discouragement, carelessness.",
"type": "minor",
"desc": "He rides a slow, enduring, heavy horse, to which his own aspect corresponds. He exhibits his symbol, but does not look therein. "
},
{
"value": "queen",
"value_int": 13,
"name": "Queen of Pentacles",
"name_short": "pequ",
"suit": "pentacles",
"meaning_up": "Opulence, generosity, magnificence, security, liberty. ",
"meaning_rev": "Evil, suspicion, suspense, fear, mistrust.",
"type": "minor",
"desc": "The face suggests that of a dark woman, whose qualities might be summed up in the idea of greatness of soul; she has also the serious cast of intelligence; she contemplates her symbol and may see worlds therein. "
},
{
"value": "king",
"value_int": 14,
"name": "King of Pentacles",
"name_short": "peki",
"suit": "pentacles",
"meaning_up": "Valour, realizing intelligence, business and normal intellectual aptitude, sometimes mathematical gifts and attainments of this kind; success in these paths. ",
"meaning_rev": "Vice, weakness, ugliness, perversity, corruption, peril.",
"type": "minor",
"desc": "The figure calls for no special description the face is rather dark, suggesting also courage, but somewhat lethargic in tendency. The bull's head should be noted as a recurrent symbol on the throne. The sign of this suit is represented throughout as engraved or blazoned with the pentagram, typifying the correspondence of the four elements in human nature and that by which they may be governed. In many old Tarot packs this suit stood for current coin, money, deniers. I have not invented the substitution of pentacles and I have no special cause to sustain in respect of the alternative. But the consensus of divinatory meanings is on the side of some change, because the cards do not happen to deal especially with questions of money. "
},
{
"value": "ace",
"value_int": 1,
"name": "Ace of Pentacles",
"name_short": "peac",
"suit": "pentacles",
"meaning_up": "Perfect contentment, felicity, ecstasy; also speedy intelligence; gold. ",
"meaning_rev": "The evil side of wealth, bad intelligence; also great riches. In any case it shews prosperity, comfortable material conditions, but whether these are of advantage to the possessor will depend on whether the card is reversed or not.",
"type": "minor",
"desc": "A hand--issuing, as usual, from a cloud--holds up a pentacle. "
},
{
"value": "two",
"value_int": 2,
"name": "Two of Pentacles",
"name_short": "pe02",
"suit": "pentacles",
"meaning_up": "On the one hand it is represented as a card of gaiety, recreation and its connexions, which is the subject of the design; but it is read also as news and messages in writing, as obstacles, agitation, trouble, embroilment. ",
"meaning_rev": "Enforced gaiety, simulated enjoyment, literal sense, handwriting, composition, letters of exchange.",
"type": "minor",
"desc": "A young man, in the act of dancing, has a pentacle in either hand, and they are joined by that endless cord which is like the number 8 reversed. "
},
{
"value": "three",
"value_int": 3,
"name": "Three of Pentacles",
"name_short": "pe03",
"suit": "pentacles",
"meaning_up": "MĂ©tier, trade, skilled labour; usually, however, regarded as a card of nobility, aristocracy, renown, glory. ",
"meaning_rev": "Mediocrity, in work and otherwise, puerility, pettiness, weakness.",
"type": "minor",
"desc": "A sculptor at his work in a monastery. Compare the design which illustrates the Eight of Pentacles. The apprentice or amateur therein has received his reward and is now at work in earnest. "
},
{
"value": "four",
"value_int": 4,
"name": "Four of Pentacles",
"name_short": "pe04",
"suit": "pentacles",
"meaning_up": "The surety of possessions, cleaving to that which one has, gift, legacy, inheritance. ",
"meaning_rev": "Suspense, delay, opposition.",
"type": "minor",
"desc": "A crowned figure, having a pentacle over his crown, clasps another with hands and arms; two pentacles are under his feet. He holds to that which he has. "
},
{
"value": "five",
"value_int": 5,
"name": "Five of Pentacles",
"name_short": "pe05",
"suit": "pentacles",
"meaning_up": "The card foretells material trouble above all, whether in the form illustrated--that is, destitution--or otherwise. For some cartomancists, it is a card of love and lovers-wife, husband, friend, mistress; also concordance, affinities. These alternatives cannot be harmonized. ",
"meaning_rev": "Disorder, chaos, ruin, discord, profligacy.",
"type": "minor",
"desc": "Two mendicants in a snow-storm pass a lighted casement. "
},
{
"value": "six",
"value_int": 6,
"name": "Six of Pentacles",
"name_short": "pe06",
"suit": "pentacles",
"meaning_up": "Presents, gifts, gratification another account says attention, vigilance now is the accepted time, present prosperity, etc. ",
"meaning_rev": "Desire, cupidity, envy, jealousy, illusion.",
"type": "minor",
"desc": "A person in the guise of a merchant weighs money in a pair of scales and distributes it to the needy and distressed. It is a testimony to his own success in life, as well as to his goodness of heart. "
},
{
"value": "seven",
"value_int": 7,
"name": "Seven of Pentacles",
"name_short": "pe07",
"suit": "pentacles",
"meaning_up": "These are exceedingly contradictory; in the main, it is a card of money, business, barter; but one reading gives altercation, quarrels--and another innocence, ingenuity, purgation. ",
"meaning_rev": "Cause for anxiety regarding money which it may be proposed to lend.",
"type": "minor",
"desc": "A young man, leaning on his staff, looks intently at seven pentacles attached to a clump of greenery on his right; one would say that these were his treasures and that his heart was there. "
},
{
"value": "eight",
"value_int": 8,
"name": "Eight of Pentacles",
"name_short": "pe08",
"suit": "pentacles",
"meaning_up": "Work, employment, commission, craftsmanship, skill in craft and business, perhaps in the preparatory stage. ",
"meaning_rev": "Voided ambition, vanity, cupidity, exaction, usury. It may also signify the possession of skill, in the sense of the ingenious mind turned to cunning and intrigue.",
"type": "minor",
"desc": "An artist in stone at his work, which he exhibits in the form of trophies. "
},
{
"value": "nine",
"value_int": 9,
"name": "Nine of Pentacles",
"name_short": "pe09",
"suit": "pentacles",
"meaning_up": "Prudence, safety, success, accomplishment, certitude, discernment. ",
"meaning_rev": "Roguery, deception, voided project, bad faith.",
"type": "minor",
"desc": "A woman, with a bird upon her wrist, stands amidst a great abundance of grapevines in the garden of a manorial house. It is a wide domain, suggesting plenty in all things. Possibly it is her own possession and testifies to material well-being. "
},
{
"value": "ten",
"value_int": 10,
"name": "Ten of Pentacles",
"name_short": "pe10",
"suit": "pentacles",
"meaning_up": "Gain, riches; family matters, archives, extraction, the abode of a family. ",
"meaning_rev": "Chance, fatality, loss, robbery, games of hazard; sometimes gift, dowry, pension.",
"type": "minor",
"desc": "A man and woman beneath an archway which gives entrance to a house and domain. They are accompanied by a child, who looks curiously at two dogs accosting an ancient personage seated in the foreground. The child's hand is on one of them. "
},
{
"value": "page",
"value_int": 11,
"name": "Page of Swords",
"name_short": "swpa",
"suit": "swords",
"meaning_up": "Authority, overseeing, secret service, vigilance, spying, examination, and the qualities thereto belonging. ",
"meaning_rev": "More evil side of these qualities; what is unforeseen, unprepared state; sickness is also intimated.",
"type": "minor",
"desc": "A lithe, active figure holds a sword upright in both hands, while in the act of swift walking. He is passing over rugged land, and about his way the clouds are collocated wildly. He is alert and lithe, looking this way and that, as if an expected enemy might appear at any moment. "
},
{
"value": "knight",
"value_int": 12,
"name": "Knight of Swords",
"name_short": "swkn",
"suit": "swords",
"meaning_up": "Skill, bravery, capacity, defence, address, enmity, wrath, war, destruction, opposition, resistance, ruin. There is therefore a sense in which the card signifies death, but it carries this meaning only in its proximity to other cards of fatality. ",
"meaning_rev": "Imprudence, incapacity, extravagance.",
"type": "minor",
"desc": "He is riding in full course, as if scattering his enemies. In the design he is really a prototypical hero of romantic chivalry. He might almost be Galahad, whose sword is swift and sure because he is clean of heart. "
},
{
"value": "queen",
"value_int": 13,
"name": "Queen of Swords",
"name_short": "swqu",
"suit": "swords",
"meaning_up": "Widowhood, female sadness and embarrassment, absence, sterility, mourning, privation, separation. ",
"meaning_rev": "Malice, bigotry, artifice, prudery, bale, deceit.",
"type": "minor",
"desc": "Her right hand raises the weapon vertically and the hilt rests on an arm of her royal chair the left hand is extended, the arm raised her countenance is severe but chastened; it suggests familiarity with sorrow. It does not represent mercy, and, her sword notwithstanding, she is scarcely a symbol of power. "
},
{
"value": "king",
"value_int": 14,
"name": "King of Swords",
"name_short": "swki",
"suit": "swords",
"meaning_up": "Whatsoever arises out of the idea of judgment and all its connexions-power, command, authority, militant intelligence, law, offices of the crown, and so forth. ",
"meaning_rev": "Cruelty, perversity, barbarity, perfidy, evil intention.",
"type": "minor",
"desc": "He sits in judgment, holding the unsheathed sign of his suit. He recalls, of course, the conventional Symbol of justice in the Trumps Major, and he may represent this virtue, but he is rather the power of life and death, in virtue of his office. "
},
{
"value": "ace",
"value_int": 1,
"name": "Ace of Swords",
"name_short": "swac",
"suit": "swords",
"meaning_up": "Triumph, the excessive degree in everything, conquest, triumph of force. It is a card of great force, in love as well as in hatred. The crown may carry a much higher significance than comes usually within the sphere of fortune-telling. ",
"meaning_rev": "The same, but the results are disastrous; another account says--conception, childbirth, augmentation, multiplicity.",
"type": "minor",
"desc": "A hand issues from a cloud, grasping as word, the point of which is encircled by a crown. "
},
{
"value": "two",
"value_int": 2,
"name": "Two of Swords",
"name_short": "sw02",
"suit": "swords",
"meaning_up": "Conformity and the equipoise which it suggests, courage, friendship, concord in a state of arms; another reading gives tenderness, affection, intimacy. The suggestion of harmony and other favourable readings must be considered in a qualified manner, as Swords generally are not symbolical of beneficent forces in human affairs. ",
"meaning_rev": "Imposture, falsehood, duplicity, disloyalty.",
"type": "minor",
"desc": "A hoodwinked female figure balances two swords upon her shoulders. "
},
{
"value": "three",
"value_int": 3,
"name": "Three of Swords",
"name_short": "sw03",
"suit": "swords",
"meaning_up": "Removal, absence, delay, division, rupture, dispersion, and all that the design signifies naturally, being too simple and obvious to call for specific enumeration. ",
"meaning_rev": "Mental alienation, error, loss, distraction, disorder, confusion.",
"type": "minor",
"desc": "Three swords piercing a heart; cloud and rain behind. "
},
{
"value": "four",
"value_int": 4,
"name": "Four of Swords",
"name_short": "sw04",
"suit": "swords",
"meaning_up": "Vigilance, retreat, solitude, hermit's repose, exile, tomb and coffin. It is these last that have suggested the design. ",
"meaning_rev": "Wise administration, circumspection, economy, avarice, precaution, testament.",
"type": "minor",
"desc": "The effigy of a knight in the attitude of prayer, at full length upon his tomb. "
},
{
"value": "five",
"value_int": 5,
"name": "Five of Swords",
"name_short": "sw05",
"suit": "swords",
"meaning_up": "Degradation, destruction, revocation, infamy, dishonour, loss, with the variants and analogues of these. ",
"meaning_rev": "The same; burial and obsequies.",
"type": "minor",
"desc": "A disdainful man looks after two retreating and dejected figures. Their swords lie upon the ground. He carries two others on his left shoulder, and a third sword is in his right hand, point to earth. He is the master in possession of the field. "
},
{
"value": "six",
"value_int": 6,
"name": "Six of Swords",
"name_short": "sw06",
"suit": "swords",
"meaning_up": "journey by water, route, way, envoy, commissionary, expedient. ",
"meaning_rev": "Declaration, confession, publicity; one account says that it is a proposal of love.",
"type": "minor",
"desc": "A ferryman carrying passengers in his punt to the further shore. The course is smooth, and seeing that the freight is light, it may be noted that the work is not beyond his strength. "
},
{
"value": "seven",
"value_int": 7,
"name": "Seven of Swords",
"name_short": "sw07",
"suit": "swords",
"meaning_up": "Design, attempt, wish, hope, confidence; also quarrelling, a plan that may fail, annoyance. The design is uncertain in its import, because the significations are widely at variance with each other. ",
"meaning_rev": "Good advice, counsel, instruction, slander, babbling.",
"type": "minor",
"desc": "A man in the act of carrying away five swords rapidly; the two others of the card remain stuck in the ground. A camp is close at hand. "
},
{
"value": "eight",
"value_int": 8,
"name": "Eight of Swords",
"name_short": "sw08",
"suit": "swords",
"meaning_up": "Bad news, violent chagrin, crisis, censure, power in trammels, conflict, calumny; also sickness. ",
"meaning_rev": "Disquiet, difficulty, opposition, accident, treachery; what is unforeseen; fatality.",
"type": "minor",
"desc": "A woman, bound and hoodwinked, with the swords of the card about her. Yet it is rather a card of temporary durance than of irretrievable bondage. "
},
{
"value": "nine",
"value_int": 9,
"name": "Nine of Swords",
"name_short": "sw09",
"suit": "swords",
"meaning_up": "Death, failure, miscarriage, delay, deception, disappointment, despair. ",
"meaning_rev": "Imprisonment, suspicion, doubt, reasonable fear, shame.",
"type": "minor",
"desc": "One seated on her couch in lamentation, with the swords over her. She is as one who knows no sorrow which is like unto hers. It is a card of utter desolation. "
},
{
"value": "ten",
"value_int": 10,
"name": "Ten of Swords",
"name_short": "sw10",
"suit": "swords",
"meaning_up": "Whatsoever is intimated by the design; also pain, affliction, tears, sadness, desolation. It is not especially a card of violent death. ",
"meaning_rev": "Advantage, profit, success, favour, but none of these are permanent; also power and authority.",
"type": "minor",
"desc": "A prostrate figure, pierced by all the swords belonging to the card. "
}
]
}

82
site/src/server/tarot.rs Normal file
View File

@ -0,0 +1,82 @@
use maj::{gemini, server::Error, Response};
use rand::seq::SliceRandom;
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
struct Card {
name: String,
name_short: String,
#[serde(rename = "value_int")]
value: u16,
meaning_up: String,
meaning_rev: String,
meaning: Option<String>,
#[serde(rename = "type")]
kind: String,
#[serde(default)]
upright: bool,
}
struct Deck {
cards: Vec<Card>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct Container {
count: u32,
cards: Vec<Card>,
}
impl Deck {
fn new() -> Result<Self, serde_json::Error> {
let ctr: Container = serde_json::from_str(include_str!("./tarot.json"))?;
let mut deck = ctr.cards;
deck.shuffle(&mut thread_rng());
Ok(Deck { cards: deck })
}
fn draw(&mut self) -> Card {
let face = thread_rng().gen::<u8>() % 2;
let mut card = self.cards.pop().unwrap();
card.upright = face == 0;
if !card.upright {
card.name = format!("{} (reversed)", card.name);
card.meaning = Some(card.meaning_rev.clone());
} else {
card.meaning = Some(card.meaning_up.clone());
}
card
}
}
pub async fn character() -> Result<Response, Error> {
let mut d = Deck::new()?;
let history = d.draw();
let recent = d.draw();
let current = d.draw();
let b = gemini::Builder::new()
.heading(1, "RPG Character Backstory Generator")
.text("Stuck coming up with a plausible backstory for a character? Try this generator out. Each of these categories lists a series of descriptors describing various stages of the character's life: their life history, some recent major event in their life and their current state/mood. This should be all you need to flesh out a believeable NPC's dialogue.")
.text("")
.heading(2, "Background / History")
.text(format!("{}: {}", history.name, history.meaning.unwrap()))
.text("")
.heading(2, "Recent Events")
.text(format!("{}: {}", recent.name, recent.meaning.unwrap()))
.text("")
.heading(2, "Current Situation")
.text(format!("{}: {}", current.name, current.meaning.unwrap()))
.text("")
.link(
"/tools/character_gen",
Some("Make a new character".to_string()),
);
Ok(Response::render(b.build()))
}

3
site/static/foo.gmi Normal file
View File

@ -0,0 +1,3 @@
# foo
Hahaha static serving works

74
site/static/gruvbox.css Normal file
View File

@ -0,0 +1,74 @@
main {
font-family: monospace, monospace;
max-width: 38rem;
padding: 2rem;
margin: auto;
}
@media only screen and (max-device-width: 736px) {
main {
padding: 0rem;
}
}
::selection {
background: #d3869b;
}
body {
background: #282828;
color: #ebdbb2;
}
pre {
background-color: #3c3836;
padding: 1em;
border: 0;
}
a, a:active, a:visited {
color: #b16286;
background-color: #1d2021;
}
h1, h2, h3, h4, h5 {
margin-bottom: .1rem;
}
blockquote {
border-left: 1px solid #bdae93;
margin: 0.5em 10px;
padding: 0.5em 10px;
}
footer {
align: center;
}
@media (prefers-color-scheme: light) {
body {
background: #fbf1c7;
color: #3c3836;
}
pre {
background-color: #ebdbb2;
padding: 1em;
border: 0;
}
a, a:active, a:visited {
color: #b16286;
background-color: #f9f5d7;
}
h1, h2, h3, h4, h5 {
margin-bottom: .1rem;
}
blockquote {
border-left: 1px solid #655c54;
margin: 0.5em 10px;
padding: 0.5em 10px;
}
}

3
site/static/index.gmi Normal file
View File

@ -0,0 +1,3 @@
# test
Hi there

View File

@ -0,0 +1,15 @@
@use super::{header_html, footer_html};
@(url: String, why: String)
@:header_html("Error")
<h1>Error</h1>
<p>
There was an error proxying <code>@url</code>: @why.
</p>
<hr />
@:footer_html()

View File

@ -0,0 +1,7 @@
@()
<footer>
<p><a href="https://tulpa.dev/cadey/maj">Source code here</a> - From <a href="gemini://cetacean.club">Within</a></p>
</footer>
</main>
</body>
</html>

View File

@ -0,0 +1,13 @@
@use super::statics::*;
@(title: &str)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
<link rel="stylesheet" href="/static/@gruvbox_css.name">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body id="top">
<main>

View File

@ -0,0 +1,13 @@
@use super::{header_html, footer_html};
@(title: String, body: impl ToHtml)
@:header_html(&title)
<p>
@body
</p>
<hr />
@:footer_html()

View File

@ -4,10 +4,7 @@ use tokio::{
io::{AsyncReadExt, AsyncWriteExt}, io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream, net::TcpStream,
}; };
use tokio_rustls::{ use tokio_rustls::{rustls::TLSError, TlsConnector};
rustls::{TLSError},
TlsConnector,
};
use url::Url; use url::Url;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]

View File

@ -1,250 +1 @@
/// This module implements a simple text/gemini parser based on the description pub use gemtext::*;
/// here: https://gemini.circumlunar.space/docs/specification.html
use std::io::Write;
/// Individual nodes of the document. Each node correlates to a line in the file.
#[derive(Debug, PartialEq, Eq)]
pub enum Node {
/// Text lines are the most fundamental line type - any line which does not
/// match the definition of another line type defined below defaults to
/// being a text line. The majority of lines in a typical text/gemini document will be text lines.
Text(String),
/// Lines beginning with the two characters "=>" are link lines, which have the following syntax:
///
/// ```gemini
/// =>[<whitespace>]<URL>[<whitespace><USER-FRIENDLY LINK NAME>]
/// ```
///
/// where:
///
/// * `<whitespace>` is any non-zero number of consecutive spaces or tabs
/// * Square brackets indicate that the enclosed content is optional.
/// * `<URL>` is a URL, which may be absolute or relative. If the URL
/// does not include a scheme, a scheme of `gemini://` is implied.
Link { to: String, name: Option<String> },
/// Any line whose first three characters are "```" (i.e. three consecutive
/// back ticks with no leading whitespace) are preformatted toggle lines.
/// These lines should NOT be included in the rendered output shown to the
/// user. Instead, these lines toggle the parser between preformatted mode
/// being "on" or "off". Preformatted mode should be "off" at the beginning
/// of a document. The current status of preformatted mode is the only
/// internal state a parser is required to maintain. When preformatted mode
/// is "on", the usual rules for identifying line types are suspended, and
/// all lines should be identified as preformatted text lines (see 5.4.4).
///
/// Preformatted text lines should be presented to the user in a "neutral",
/// monowidth font without any alteration to whitespace or stylistic
/// enhancements. Graphical clients should use scrolling mechanisms to present
/// preformatted text lines which are longer than the client viewport, in
/// preference to wrapping. In displaying preformatted text lines, clients
/// should keep in mind applications like ASCII art and computer source
/// code: in particular, source code in languages with significant whitespace
/// (e.g. Python) should be able to be copied and pasted from the client into
/// a file and interpreted/compiled without any problems arising from the
/// client's manner of displaying them.
Preformatted(String),
/// Lines beginning with "#" are heading lines. Heading lines consist of one,
/// two or three consecutive "#" characters, followed by optional whitespace,
/// followed by heading text. The number of # characters indicates the "level"
/// of header; #, ## and ### can be thought of as analogous to `<h1>`, `<h2>`
/// and `<h3>` in HTML.
///
/// Heading text should be presented to the user, and clients MAY use special
/// formatting, e.g. a larger or bold font, to indicate its status as a header
/// (simple clients may simply print the line, including its leading #s,
/// without any styling at all). However, the main motivation for the
/// definition of heading lines is not stylistic but to provide a
/// machine-readable representation of the internal structure of the document.
/// Advanced clients can use this information to, e.g. display an automatically
/// generated and hierarchically formatted "table of contents" for a long
/// document in a side-pane, allowing users to easily jump to specific sections
/// without excessive scrolling. CMS-style tools automatically generating menus
/// or Atom/RSS feeds for a directory of text/gemini files can use first
/// heading in the file as a human-friendly title.
Heading { level: u8, body: String },
/// Lines beginning with "* " are unordered list items. This line type exists
/// purely for stylistic reasons. The * may be replaced in advanced clients by
/// a bullet symbol. Any text after the "* " should be presented to the user as
/// if it were a text line, i.e. wrapped to fit the viewport and formatted
/// "nicely". Advanced clients can take the space of the bullet symbol into
/// account when wrapping long list items to ensure that all lines of text
/// corresponding to the item are offset an equal distance from the left of the screen.
ListItem(String),
/// Lines beginning with ">" are quote lines. This line type exists so that
/// advanced clients may use distinct styling to convey to readers the important
/// semantic information that certain text is being quoted from an external
/// source. For example, when wrapping long lines to the the viewport, each
/// resultant line may have a ">" symbol placed at the front.
Quote(String),
}
pub fn parse(doc: &str) -> Vec<Node> {
let mut result: Vec<Node> = vec![];
let mut collect_preformatted: bool = false;
let mut preformatted_buffer: Vec<u8> = vec![];
for line in doc.lines() {
if line == "```" {
collect_preformatted = !collect_preformatted;
if !collect_preformatted {
result.push(Node::Preformatted(
String::from_utf8(preformatted_buffer)
.unwrap()
.trim_end()
.to_string(),
));
preformatted_buffer = vec![];
}
continue;
}
if collect_preformatted && line != "```" {
write!(preformatted_buffer, "{}\n", line).unwrap();
continue;
}
// Quotes
if line.starts_with(">") {
result.push(Node::Quote(line[1..].trim().to_string()));
continue;
}
// List items
if line.starts_with("*") {
result.push(Node::ListItem(line[1..].trim().to_string()));
continue;
}
// Headings
if line.starts_with("###") {
result.push(Node::Heading {
level: 3,
body: line[3..].trim().to_string(),
});
continue;
}
if line.starts_with("##") {
result.push(Node::Heading {
level: 2,
body: line[2..].trim().to_string(),
});
continue;
}
if line.starts_with("#") {
result.push(Node::Heading {
level: 1,
body: line[1..].trim().to_string(),
});
continue;
}
// Links
if line.starts_with("=>") {
let sp = line[2..].split_ascii_whitespace().collect::<Vec<&str>>();
match sp.len() {
1 => result.push(Node::Link {
to: sp[0].trim().to_string(),
name: None,
}),
_ => result.push(Node::Link {
to: sp[0].trim().to_string(),
name: Some(sp[1..].join(" ").trim().to_string()),
}),
}
continue;
}
result.push(Node::Text(line.to_string()));
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic() {
let _ = pretty_env_logger::try_init();
let msg = include_str!("../majc/src/help.gmi");
let doc = super::parse(msg);
assert_ne!(doc.len(), 0);
}
#[test]
fn quote() {
let _ = pretty_env_logger::try_init();
let msg = ">hi there";
let expected: Vec<Node> = vec![Node::Quote("hi there".to_string())];
assert_eq!(expected, parse(msg));
}
#[test]
fn list() {
let _ = pretty_env_logger::try_init();
let msg = "*hi there";
let expected: Vec<Node> = vec![Node::ListItem("hi there".to_string())];
assert_eq!(expected, parse(msg));
}
#[test]
fn preformatted() {
let _ = pretty_env_logger::try_init();
let msg = "```\n\
hi there\n\
```\n\
\n\
Test\n";
let expected: Vec<Node> = vec![
Node::Preformatted("hi there".to_string()),
Node::Text(String::new()),
Node::Text("Test".to_string()),
];
assert_eq!(expected, parse(msg));
}
#[test]
fn header() {
let _ = pretty_env_logger::try_init();
let msg = "#hi\n##there\n### my friends";
let expected: Vec<Node> = vec![
Node::Heading {
level: 1,
body: "hi".to_string(),
},
Node::Heading {
level: 2,
body: "there".to_string(),
},
Node::Heading {
level: 3,
body: "my friends".to_string(),
},
];
assert_eq!(expected, parse(msg));
}
#[test]
fn link() {
let _ = pretty_env_logger::try_init();
let msg = "=>/\n=> / Go home";
let expected: Vec<Node> = vec![
Node::Link {
to: "/".to_string(),
name: None,
},
Node::Link {
to: "/".to_string(),
name: Some("Go home".to_string()),
},
];
assert_eq!(expected, parse(msg));
}
}

View File

@ -1,6 +1,7 @@
use crate::StatusCode; use crate::{gemini, StatusCode};
use num::FromPrimitive; use num::FromPrimitive;
use std::io::{prelude::*, ErrorKind, self}; use std::fmt;
use std::io::{self, prelude::*, ErrorKind};
/// A Gemini response as specified in [the spec](https://gemini.circumlunar.space/docs/specification.html). /// A Gemini response as specified in [the spec](https://gemini.circumlunar.space/docs/specification.html).
#[derive(Default)] #[derive(Default)]
@ -10,6 +11,91 @@ pub struct Response {
pub body: Vec<u8>, pub body: Vec<u8>,
} }
#[derive(thiserror::Error, Debug)]
pub struct ResponseStatusError {
status: StatusCode,
meta: String,
}
impl fmt::Display for ResponseStatusError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{:?} ({}): {}",
self.status, self.status as u8, self.meta
)
}
}
impl Response {
pub fn with_body(meta: String, body: Vec<u8>) -> Response {
Response {
status: StatusCode::Success,
meta: meta,
body: body,
}
}
pub fn gemini(body: Vec<u8>) -> Response {
Response {
status: StatusCode::Success,
meta: "text/gemini".to_string(),
body: body,
}
}
pub fn render(body: Vec<gemini::Node>) -> Response {
let mut buf: Vec<u8> = vec![];
gemini::render(body, &mut buf).unwrap();
Response {
status: StatusCode::Success,
meta: "text/gemini".to_string(),
body: buf,
}
}
pub fn perm_redirect(to: String) -> Response {
Response {
status: StatusCode::PermanentRedirect,
meta: to,
body: vec![],
}
}
pub fn no_proxy() -> Response {
Response {
status: StatusCode::ProxyRequestRefused,
meta: "Wrong host".to_string(),
body: vec![],
}
}
pub fn not_found() -> Response {
Response {
status: StatusCode::NotFound,
meta: "Not found".to_string(),
body: vec![],
}
}
pub fn input<T: Into<String>>(msg: T) -> Response {
Response {
status: StatusCode::Input,
meta: msg.into(),
body: vec![],
}
}
pub fn need_cert<T: Into<String>>(msg: T) -> Response {
Response {
status: StatusCode::ClientCertificateRequired,
meta: msg.into(),
body: vec![],
}
}
}
/// The parser state. /// The parser state.
#[derive(Debug)] #[derive(Debug)]
enum State { enum State {
@ -103,7 +189,7 @@ impl Response {
} }
_ => { _ => {
if data.len() == 1024 { if data.len() == 1024 {
return Err(Error::ResponseMetaTooLong) return Err(Error::ResponseMetaTooLong);
} }
data.push(buf[0]); data.push(buf[0]);
} }
@ -168,11 +254,13 @@ mod tests {
match Response::parse(&mut fin) { match Response::parse(&mut fin) {
Ok(_) => panic!("wanted error but didn't get one"), Ok(_) => panic!("wanted error but didn't get one"),
Err(why) => if let ResponseError::ResponseMetaTooLong = why { Err(why) => {
println!("ok"); if let ResponseError::ResponseMetaTooLong = why {
} else { println!("ok");
panic!("wanted ResponseError::ResponseMetaTooLong") } else {
}, panic!("wanted ResponseError::ResponseMetaTooLong")
}
}
} }
} }
} }

79
src/server/cgi.rs Normal file
View File

@ -0,0 +1,79 @@
/// A simple handler for disk based files. Will optionally chop off a prefix.
use super::{Handler as MajHandler, Request, Result};
use crate::Response;
use crate::{route, seg, split};
use async_trait::async_trait;
use std::collections::HashMap;
use std::io::Cursor;
use std::path::PathBuf;
use std::process::Command;
pub struct Handler {
base_dir: PathBuf,
}
const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
#[async_trait]
impl MajHandler for Handler {
async fn handle(&self, r: Request) -> Result<Response> {
route!(r.url.path(), {
(/"cgi-bin"/[prog_name: String][/rest..]) => self.do_cgi(prog_name, rest.to_string(), r).await;
});
Ok(Response::not_found())
}
}
impl Handler {
pub fn new(base_dir: PathBuf) -> Self {
Handler { base_dir: base_dir }
}
async fn do_cgi(&self, prog_name: String, rest: String, r: Request) -> Result<Response> {
let mut path = PathBuf::from(&self.base_dir);
path.push(&prog_name);
log::debug!("path: {:?}", path);
let query = {
match r.url.query() {
Some(q) => q.clone(),
None => "",
}
};
if let Err(why) = std::fs::metadata(&path) {
log::error!("can't find {:?}: {}", path, why);
return Ok(Response::not_found());
}
let filtered_env: HashMap<String, String> = std::env::vars()
.filter(|&(ref k, _)| k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH")
.collect();
let remote_host = format!("{}", r.addr);
let output = Command::new(&path)
.env_clear()
.envs(filtered_env)
.env("GATEWAY_INTERFACE", "CGI/1.1")
.env("SERVER_PROTOCOL", "GEMINI")
.env("SERVER_SOFTWARE", APPLICATION_NAME)
.env("GEMINI_URL", format!("{}", r.url))
.env("SCRIPT_NAME", path)
.env("PATH_INFO", rest)
.env("QUERY_STRING", query)
.env("SERVER_NAME", r.url.host_str().unwrap())
.env("SERVER_HOSTNAME", r.url.host_str().unwrap())
.env("SERVER_PORT", format!("{}", r.url.port().unwrap_or(1965)))
.env("REMOTE_HOST", &remote_host)
.env("REMOTE_ADDR", remote_host)
.env("TLS_CIPHER", "Secure")
.env("TLS_VERSION", "TLSv1.3")
.output()?;
let resp = Response::parse(&mut Cursor::new(output.stdout))?;
Ok(resp)
}
}

60
src/server/files.rs Normal file
View File

@ -0,0 +1,60 @@
/// A simple handler for disk based files. Will optionally chop off a prefix.
use super::{Handler as MajHandler, Request, Result};
use crate::Response;
use async_trait::async_trait;
use std::ffi::OsStr;
use std::path::PathBuf;
pub struct Handler {
base_dir: PathBuf,
}
impl Handler {
/// Serves static files from an OS directory with a given prefix chopped off.
pub fn new(base_dir: PathBuf) -> Self {
Handler { base_dir: base_dir }
}
}
#[async_trait]
impl MajHandler for Handler {
async fn handle(&self, r: Request) -> Result<Response> {
let mut path = std::path::PathBuf::from(&self.base_dir);
if let Some(segments) = r.url.path_segments() {
path.extend(segments);
}
log::debug!("opening file {:?}", path);
match tokio::fs::metadata(&path).await {
Ok(stat) => {
if stat.is_dir() {
if r.url.as_str().ends_with('/') {
path.push("index.gmi");
} else {
// Send a redirect when the URL for a directory has no trailing slash.
return Ok(Response::perm_redirect(format!("{}/", r.url)));
}
}
}
Err(why) => {
log::error!("file {} not found: {}", path.to_str().unwrap(), why);
return Ok(Response::not_found());
}
}
let mut file = tokio::fs::File::open(&path).await?;
let mut buf: Vec<u8> = Vec::new();
tokio::io::copy(&mut file, &mut buf).await?;
// Send header.
if path.extension() == Some(OsStr::new("gmi"))
|| path.extension() == Some(OsStr::new("gemini"))
{
return Ok(Response::gemini(buf));
}
let mime = mime_guess::from_path(&path).first_or_octet_stream();
Ok(Response::with_body(mime.essence_str().to_string(), buf))
}
}

View File

@ -1,153 +1,191 @@
use crate::{Response, StatusCode}; use crate::{Response, StatusCode};
use async_trait::async_trait; use tokio::{
use rustls::{Certificate, Session}; prelude::*,
use std::{error::Error as StdError, net::SocketAddr, sync::Arc}; net::{TcpListener, TcpStream},
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; task,
use tokio::{net::TcpListener, stream::StreamExt}; };
use tokio_rustls::TlsAcceptor; use tokio_rustls::TlsAcceptor;
use async_trait::async_trait;
use rustls::Certificate;
use std::{error::Error as StdError, net::SocketAddr, sync::Arc};
use url::Url; use url::Url;
/// A Gemini request and its associated metadata. /// A Gemini request and its associated metadata.
#[allow(dead_code)] #[allow(dead_code)]
pub struct Request { pub struct Request {
pub url: Url, pub url: Url,
pub addr: SocketAddr,
pub certs: Option<Vec<Certificate>>, pub certs: Option<Vec<Certificate>>,
} }
pub type Error = Box<dyn StdError + Sync + Send>; pub type Error = Box<dyn StdError + Sync + Send>;
type Result<T = ()> = std::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
enum RequestParsingError {
#[error("invalid scheme {0}")]
InvalidScheme(String),
#[error("unexpected end of request")]
UnexpectedEnd,
}
#[allow(dead_code, unused_assignments, unused_mut, unused_variables)] #[allow(dead_code, unused_assignments, unused_mut, unused_variables)]
mod routes; mod routes;
pub use routes::*; pub use routes::*;
pub mod cgi;
pub mod files;
#[async_trait] #[async_trait]
pub trait Handler { pub trait Handler {
async fn handle(&self, r: Request) -> Result<Response, Error>; async fn handle(&self, r: Request) -> Result<Response>;
} }
pub async fn serve( pub async fn serve(
h: &(dyn Handler + Sync), h: Arc<dyn Handler + Send + Sync>,
cfg: rustls::ServerConfig, cfg: rustls::ServerConfig,
host: String, host: String,
port: u16, port: u16,
) -> Result<(), Error> ) -> Result
where where
{ {
let cfg = Arc::new(cfg); let cfg = Arc::new(cfg);
let mut listener = TcpListener::bind(&format!("{}:{}", host, port)).await?; let listener = TcpListener::bind(&format!("{}:{}", host, port)).await?;
let mut incoming = listener.incoming(); let acceptor = Arc::new(TlsAcceptor::from(cfg.clone()));
let acceptor = TlsAcceptor::from(cfg.clone()); while let Ok((stream, addr)) = listener.accept().await {
let h = h.clone();
let acceptor = acceptor.clone();
let port = port.clone();
while let Some(stream) = incoming.next().await { task::spawn(handle_request(h, stream, acceptor, addr, port));
let stream = stream?;
let addr = stream.peer_addr().unwrap();
let fut = async {
let acceptor = acceptor.clone();
let result = acceptor.accept(stream).await;
if result.is_err() {
return;
}
let mut stream = result.unwrap();
let mut rd = BufReader::new(&mut stream);
let mut u = String::new();
if let Err(why) = rd.read_line(&mut u).await {
log::error!("can't read request from {}: {:?}", addr, why);
let _ = stream
.write(format!("{} Invalid URL", StatusCode::BadRequest as u8).as_bytes())
.await;
return;
}
u = u.trim().to_string();
if u.len() >= 1025 {
let _ = stream
.write(format!("{} URL too long", StatusCode::BadRequest as u8).as_bytes())
.await;
return;
}
if u.starts_with("//") {
u = format!("gemini:{}", u);
}
match Url::parse(&u) {
Err(why) => {
let _ = stream
.write(
format!("{} bad URL: {:?}", StatusCode::BadRequest as u8, why)
.as_bytes(),
)
.await;
}
Ok(u) => {
if u.scheme() != "gemini" {
let _ = stream
.write(
format!(
"{} Cannot handle that kind of url",
StatusCode::ProxyRequestRefused as u8
)
.as_bytes(),
)
.await;
return;
}
if let Some(u_port) = u.port() {
if port != u_port {
let _ = stream
.write(
format!(
"{} Cannot handle that kind of url",
StatusCode::ProxyRequestRefused as u8
)
.as_bytes(),
)
.await;
return;
}
}
tokio::join!(handle(
h,
Request {
url: u.clone(),
certs: stream.get_ref().1.get_peer_certificates(),
},
&mut stream,
addr,
));
}
}
};
tokio::join!(fut);
} }
Ok(()) Ok(())
} }
async fn handle<T>(h: &(dyn Handler + Sync), req: Request, stream: &mut T, addr: SocketAddr) /// Handle a single client session (request + response).
where async fn handle_request(
T: AsyncWriteExt + Unpin, h: Arc<(dyn Handler + Send + Sync)>,
stream: TcpStream,
acceptor: Arc<TlsAcceptor>,
addr: SocketAddr,
port: u16,
) -> Result {
// Perform handshake.
let mut stream = acceptor.clone().accept(stream).await?;
match parse_request(&mut stream).await {
Ok(url) => {
if let Some(u_port) = url.port() {
if port != u_port {
let _ = write_header(
stream,
StatusCode::ProxyRequestRefused,
"Cannot proxy to that URL",
)
.await;
return Ok(());
}
}
if url.scheme() != "gemini" {
let _ = write_header(
&mut stream,
StatusCode::ProxyRequestRefused,
"Cannot proxy to that URL",
)
.await;
Err(RequestParsingError::InvalidScheme(url.scheme().to_string()))?
}
let req = Request {
url: url,
addr: addr,
certs: None,
};
handle(h, req, &mut stream, addr).await;
}
Err(e) => {
let _ = write_header(&mut stream, StatusCode::BadRequest, "Invalid request").await;
log::error!("error from {}: {}", addr, e);
}
}
Ok(())
}
pub async fn write_header<W: AsyncWrite + Unpin>(
mut stream: W,
status: StatusCode,
meta: &str,
) -> Result {
stream
.write(format!("{} {}\r\n", status as u8, meta).as_bytes())
.await?;
Ok(())
}
/// Return the URL requested by the client.
async fn parse_request<R: AsyncRead + Unpin>(mut stream: R) -> Result<Url> {
// Because requests are limited to 1024 bytes (plus 2 bytes for CRLF), we
// can use a fixed-sized buffer on the stack, avoiding allocations and
// copying, and stopping bad clients from making us use too much memory.
let mut request = [0; 1026];
let mut buf = &mut request[..];
let mut len = 0;
// Read until CRLF, end-of-stream, or there's no buffer space left.
loop {
let bytes_read = stream.read(buf).await?;
len += bytes_read;
if request[..len].ends_with(b"\r\n") {
break;
} else if bytes_read == 0 {
Err(RequestParsingError::UnexpectedEnd)?
}
buf = &mut request[len..];
}
let request = std::str::from_utf8(&request[..len - 2])?;
// Handle scheme-relative URLs.
let url = if request.starts_with("//") {
Url::parse(&format!("gemini:{}", request))?
} else {
Url::parse(request)?
};
// Validate the URL.
Ok(url)
}
async fn handle<T>(
h: Arc<(dyn Handler + Send + Sync)>,
req: Request,
stream: &mut T,
addr: SocketAddr,
) where
T: AsyncWrite + Unpin,
{ {
let u = req.url.clone(); let u = req.url.clone();
match h.handle(req).await { match h.handle(req).await {
Ok(resp) => { Ok(resp) => {
stream let _ = stream
.write(format!("{} {}\r\n", resp.status as u8, resp.meta).as_bytes()) .write(format!("{} {}\r\n", resp.status as u8, resp.meta).as_bytes())
.await .await;
.unwrap(); let _ = stream.write(&resp.body).await;
stream.write(&resp.body).await.unwrap(); log::info!("{}: {} {:?} {}", addr, u, resp.status, resp.meta);
log::info!("{}: {} {:?}", addr, u, resp.status);
} }
Err(why) => { Err(why) => {
stream let _ = stream
.write(format!("{} {:?}\r\n", StatusCode::PermanentFailure as u8, why).as_bytes()) .write(
.await format!(
.unwrap(); "{} {}\r\n",
StatusCode::PermanentFailure as u8,
why.to_string()
)
.as_bytes(),
)
.await;
log::error!("{}: {}: {:?}", addr, u, why); log::error!("{}: {}: {:?}", addr, u, why);
} }
}; };

4
testdata/ambig_preformatted.gmi vendored Normal file
View File

@ -0,0 +1,4 @@
``` foo
FOO
```
Foo bar