Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Ignore everything
**

!maple
!maple/
!maple/**
!tests/
!tests/*.py
!build.ninja

10 changes: 1 addition & 9 deletions .envrc
Original file line number Diff line number Diff line change
@@ -1,9 +1 @@
#!/usr/bin/env bash

if type -P lorri &>/dev/null; then
eval "$(lorri direnv)"
else
echo 'while direnv evaluated .envrc, could not find the command "lorri" [https://github.com/nix-community/lorri]'

use flake
fi
use flake
21 changes: 21 additions & 0 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Check

on:
push:
pull_request:

permissions:
contents: read

jobs:
test:
name: Test
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Install Build Dependencies
run: sudo apt-get update && sudo apt-get install -y clang ninja-build pkg-config libssl-dev
- name: Build and Test
run: ninja check
- name: Build and Test the Container Image
run: docker build --tag maple:test .
9 changes: 7 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
.idea
.vscode

# Tup
.tup
# macOS
.DS_Store

# Build Artifacts
build

# Ninja
.ninja_log
.ninja_deps

# Nix
.direnv/
result

# Python Tests
__pycache__/
35 changes: 15 additions & 20 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
FROM alpine:latest as environment
FROM alpine:3.23 AS builder

RUN apk update \
&& apk upgrade \
&& apk add --no-cache libstdc++
RUN apk add --no-cache clang g++ ninja pkgconf openssl-dev openssl python3

FROM environment as build_environment
WORKDIR /source

RUN apk add --no-cache \
clang \
ninja \
alpine-sdk \
openssl-dev \
linux-headers
COPY build.ninja ./
COPY maple ./maple
COPY tests ./tests

FROM build_environment as builder
RUN ninja check

WORKDIR /maple

COPY ./ ./
FROM alpine:3.23

RUN ninja

FROM environment
RUN apk add --no-cache libstdc++ libssl3 libcrypto3 \
&& addgroup -S -g 1000 maple \
&& adduser -S -u 1000 -G maple maple

WORKDIR /maple

COPY --from=builder /maple/build/maple ./
COPY --from=builder /source/build/maple /usr/local/bin/maple

USER maple

EXPOSE 1965

ENTRYPOINT ["./maple"]
ENTRYPOINT ["maple"]
136 changes: 55 additions & 81 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,108 +1,82 @@
# Maple

A very simple static Gemini server, now with Titan support!
A small static Gemini server with optional Titan uploads.

### Lines-of-code
## Build and Run

This codebase is now 351 lines of lines-of-code!
Maple requires a C++20 Clang compiler, Ninja, pkg-config, and OpenSSL development
libraries. Python 3 and the OpenSSL command-line tool are needed for tests.

The statement "... written within a single file and liberally
spanning 159 lines-of-code." has been removed.
For a new capsule:

If you would still like the minimal 159-LOC experience, checkout
[49ce0f8](https://github.com/gemrest/maple/tree/49ce0f83b8abd1af4760e56c1673c6997ef8a2c4)!
(Docker tag `0.1.4`) Do note, that version of this project is outdated and only
has Gemini support.

## Usage

### Docker Compose

A Docker Compose file is already setup [here](./docker-compose.yaml). The only
field which should be modified is the `ports`, if need be.

```shell
docker-compose up -d
```sh
ninja
ninja check
bin/genkey
printf '# Welcome\n' > .maple/gmi/index.gmi
build/maple
```

### Docker

```shell
docker run -p 1965:1965 -v ./your_capsules_directory:/maple/.maple fuwn/maple:latest
```

### Executable

1. Build: `tup` (requires [Tup](https://gittup.org/tup/index.html))
2. Run: `build/maple`, or
`TITAN=1 TITAN_TOKEN=secret TITAN_MAX_SIZE=2048 build/maple`

### Certificates

Before any possible method of usage; you must generate or use a preexisting
server certificate.

If you do not have an existing certificate, generate one using the provided
certificate generations script: [`bin/genkey`](bin/genkey) (requires
[OpenSSL](https://www.openssl.org/)).
Maple listens on TCP port 1965 by default. Set `MAPLE_PORT` to an unsigned decimal
integer from 1 to 65535 to use another port, for example `MAPLE_PORT=1966 build/maple`.

The server certificate must be placed in a directory -- directly adjacent to the
Docker Compose file or executable -- named [`.maple`](.maple). The public and
private keys must be named `public.pem` and `private.pem`, respectively.
With Nix:

### Gemini Content

Gemini content must be placed in a directory -- directly adjacent to the
Docker Compose file or executable -- named [`.maple/gmi`](.maple/gmi) and
ending with the file extension `.gmi`.

### Environment Variables

Maple can be configured with three optional environment variables:

#### `TITAN`

Enable Titan support.

Takes a value of either `true` or `1`, case-insensitive.

Defaults to off.
```sh
nix develop
nix flake check
nix build
```

#### `TITAN_TOKEN`
## Content and Certificates

Set a token to restrict Titan access.
All paths are relative to the server's **working directory**, not its executable:

Takes a string value, case-sensitive.
```text
.maple/
public.pem
private.pem
gmi/
index.gmi
```

Defaults to no token, **anyone can upload!**
`bin/genkey` creates a self-signed certificate and refuses to overwrite existing
keys. You can instead provide your own certificate and matching private key.
Keep the private key readable only by the account running Maple.

#### `TITAN_MAX_SIZE`
Maple serves regular `.gmi` files; directory requests use `index.gmi`. Changes
become visible without a restart. URL path components are percent-decoded once;
queries do not affect file selection. Symlinks, encoded separators, NUL bytes,
and `.` or `..` path components are rejected. Content directories must already
exist before uploading into them.

The maximum size in bytes which a Titan upload is permitted to perform.
## Titan

Takes an integer.
| Variable | Default | Meaning |
| --- | --- | --- |
| `TITAN` | Disabled | `1` or `true`, case-insensitive, enables uploads. |
| `TITAN_TOKEN` | Empty | Required upload token. An empty token permits unauthenticated uploads. |
| `TITAN_MAX_SIZE` | `1024` | Maximum upload bytes, written as an unsigned decimal integer. |

Default to `1024`.
Uploads require `mime=text/gemini`, a valid `size`, and a `.gmi` destination.
Completed uploads replace files atomically; incomplete uploads preserve them.
New files are private to the server account.

### Hacking
Maple serves one connection at a time. The TLS handshake and request header share
a five-second absolute deadline. Uploads and responses have a 15-second
inactivity timeout.

If you decide to tamper with Maple, you should give your commits a test. As
there isn't a test suite yet, feel free to use any of these examples:
## Docker

```bash
# Using OpenSSL directly to debug different requests:
$ openssl s_client -ign_eof -quiet -connect localhost:1965 <<< \
> "gemini://localhost:1965"
Prepare `.maple` as above, then run:

# Using Bollux to debug proper requests... or any client!
$ bollux localhost
```sh
MAPLE_UID=$(id -u) MAPLE_GID=$(id -g) docker compose up --build -d
```

## Capsules using Maple

[Add yours!](https://github.com/gemrest/maple/edit/main/README.md)

- <gemini://gem.rest/> (<https://gem.rest/>)
The user and group overrides let the container read your private key and write
uploads without widening file permissions. Compose passes through the three
Titan variables; uploads remain disabled by default.

## License

Expand Down
22 changes: 0 additions & 22 deletions Tupfile

This file was deleted.

28 changes: 14 additions & 14 deletions bin/genkey
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
#!/usr/bin/env bash

read \
-rp \
"Which common name (hostname) would you like to use for the generated key? " \
common_name
set -euo pipefail
umask 077

openssl req \
-new \
-subj "/CN=$common_name" \
-x509 \
-newkey ec \
if [[ -e .maple/private.pem || -L .maple/private.pem ||
-e .maple/public.pem || -L .maple/public.pem ]]; then
echo 'A certificate or key already exists in .maple; refusing to overwrite it.' >&2

exit 1
fi

mkdir -p .maple/gmi
read -r -p 'Hostname for the Certificate: ' common_name
openssl req -new -x509 -newkey ec \
-pkeyopt ec_paramgen_curve:prime256v1 \
-days 365 \
-nodes \
-out .maple/public.pem \
-keyout .maple/private.pem \
-inform pem
-subj "/CN=$common_name" -days 365 -nodes \
-out .maple/public.pem -keyout .maple/private.pem
44 changes: 31 additions & 13 deletions build.ninja
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
cc = clang++
cxxflags = -std=c++20 -Weverything -Wno-padded -Wno-c++98-compat -MMD -Wno-c++98-compat-pedantic
ldflags = -lssl -lcrypto
outdir = build
name = maple
srcdir = $name
compiler = clang++
compiler_flags = -std=c++20 -O2 -Weverything -Wno-padded -Wno-c++98-compat -Wno-c++98-compat-pedantic
build_directory = build

platform_warning_flags = $$(if test -f /etc/alpine-release; then $
printf '%s ' '-Wno-disabled-macro-expansion'; $
elif test "$$(uname -s)" = Darwin; then $
case "$$($compiler --version)" in $
("Apple clang"*) $
case "$$($compiler $compiler_flags -print-effective-triple)" in $
("$$(uname -m)"-apple-darwin*|"$$(uname -m)"-apple-macos*) $
printf '%s ' '-Wno-poison-system-directories';; $
esac;; $
esac; $
fi)

rule compile
command = $cc $cxxflags -c $in -o $out
command = $compiler $compiler_flags $platform_warning_flags $$(pkg-config --cflags openssl | sed -E 's/(^| )-I/\1-isystem /g') -MMD -MF $out.d -c $in -o $out
depfile = $out.d
deps = gcc

rule link
command = $cc $in -o $out $ldflags
command = $compiler $in -o $out $$(pkg-config --libs openssl)

build $outdir/$name.o: compile $srcdir/$name.cc
build $outdir/gemini.o: compile $srcdir/gemini.cc
build $outdir/titan.o: compile $srcdir/titan.cc
rule test
command = python3 -m unittest discover -s tests -v
pool = console

build $outdir/$name: link $outdir/$name.o $outdir/gemini.o $outdir/titan.o
build $build_directory/maple.o: compile maple/maple.cc
build $build_directory/gemini.o: compile maple/gemini.cc
build $build_directory/titan.o: compile maple/titan.cc
build $build_directory/content.o: compile maple/content.cc
build $build_directory/connection.o: compile maple/connection.cc
build $build_directory/uri.o: compile maple/uri.cc
build $build_directory/maple: link $build_directory/maple.o $build_directory/gemini.o $build_directory/titan.o $build_directory/content.o $build_directory/connection.o $build_directory/uri.o
build check: test $build_directory/maple

default $outdir/$name
default $build_directory/maple
Loading