A lightweight Gemini protocol client and server library in Nim.
ObiWAN is a comprehensive library for building clients and servers that speak the Gemini protocol, a lightweight alternative to HTTP designed for simplicity and privacy. The library provides both synchronous and asynchronous APIs with a clean, type-safe interface.
- Complete Gemini Protocol Support: Full implementation of the Gemini protocol specification
- Dual API: Both synchronous and asynchronous interfaces
- TLS Security: Modern TLS 1.3 implementation using mbedTLS
- Certificate Handling: Support for client and server certificates with self-signed cert verification
- IPv4 and IPv6 Support: Handles both IP protocol versions with dual-stack support (when supported by OS)
- Resource Efficient: Minimal memory footprint and CPU usage
- Type Safety: Leverages Nim's strong typing and generics for safe, expressive code
- Nim 2.2.2 or later
- mbedTLS 3.6.2 (vendored as a git submodule)
ObiWAN uses mbedTLS 3.6.2 that is included as a git submodule. When you clone the repository, make sure to include submodules:
git clone --recurse-submodules https://github.com/corv89/ObiWAN.gitOr if you already cloned the repository:
git submodule update --initThe build system will automatically build mbedTLS when needed, or you can build it manually with:
nimble buildmbedtlsOn macOS:
brew install nimOn Linux:
# Debian/Ubuntu
sudo apt install nimInstall the ObiWAN package to compile or call it from Nim.
nimble installOr, in your project add to your .nimble file:
requires "obiwan >= 0.6.0"
Generate a self-signed certificate to run the server
mkdir -p certs
openssl req -x509 -newkey rsa:4096 -keyout certs/privkey.pem -out certs/cert.pem -days 365 -nodesFirst, build all the programs:
# Build both client and server
nimble buildallThen you can run with the command-line interface:
# Show unified client help
./build/obiwan-client --help
# Run client in synchronous mode (default)
./build/obiwan-client gemini://geminiprotocol.net/
# Run client in asynchronous mode
./build/obiwan-client --async gemini://geminiprotocol.com/
# Run client with verbose output and custom redirects
./build/obiwan-client --verbose --redirects=10 gemini://example.com/
# Show unified server help
./build/obiwan-server --help
# Run server in asynchronous mode (default)
./build/obiwan-server --port=1965
# Run server in synchronous mode
./build/obiwan-server --sync
# Run server with IPv6 support
./build/obiwan-server --ipv6
# Run server with custom certificate files
./build/obiwan-server --cert=mycert.pem --key=mykey.pemObiWAN provides a comprehensive command-line interface for both clients and servers:
Options:
-h --help Show this help screen
-v --verbose Increase verbosity level
-c --config=<file> Use specific config file
-r --redirects=<num> Maximum number of redirects [default: 5]
--cert=<file> Client certificate file for authentication
--key=<file> Client key file for authentication
-a --async Use asynchronous (non-blocking) mode
--version Show version information
Options:
-h --help Show this help screen
-v --verbose Increase verbosity level
-c --config=<file> Use specific config file
-p --port=<port> Port to listen on [default: 1965]
-a --address=<addr> Address to bind to [default: 0.0.0.0]
-6 --ipv6 Use IPv6 instead of IPv4
--sync Use synchronous (blocking) mode
-r --reuse-addr Allow reuse of local addresses [default: true]
--reuse-port Allow multiple bindings to same port
--cert=<file> Server certificate file [default: cert.pem]
--key=<file> Server key file [default: privkey.pem]
--docroot=<dir> Document root directory [default: ./content]
--version Show version information
Note: The server uses asynchronous (non-blocking) mode by default.
ObiWAN supports TOML configuration files. By default, it looks for a file named obiwan.toml in:
- The current directory
~/.config/obiwan/config.toml/etc/obiwan/config.toml
You can also specify a config file using the --config option:
./build/server --config=myconfig.toml
./build/client --config=myconfig.toml gemini://example.com/Command-line options override values from the configuration file.
Example configuration:
# ObiWAN Gemini Server Configuration
[server]
address = "0.0.0.0"
port = 1965
cert_file = "cert.pem"
key_file = "privkey.pem"
reuse_addr = true
reuse_port = false
use_ipv6 = false
session_id = ""
doc_root = "./content"
log_requests = true
max_request_length = 1024
[client]
cert_file = ""
key_file = ""
max_redirects = 5
timeout = 30
user_agent = "ObiWAN/0.3.0"
[log]
level = 1
file = ""
timestamp = trueimport obiwan
# Create a synchronous client
let client = newObiwanClient()
# Make a request to a Gemini server
let response = client.request("gemini://geminiprotocol.net/")
# Display response information
echo "Status: ", response.status
echo "Meta: ", response.meta
# Check certificate information
if response.hasCertificate:
echo "Server certificate: ", response.certificate.commonName
echo "Certificate verified: ", response.isVerified
# Get response body
let body = response.body
# Always close when done
client.close()import obiwan
import asyncdispatch
# Create an asynchronous client
let client = newAsyncObiwanClient()
proc main() {.async.} =
# Make a request to a Gemini server
let response = await client.request("gemini://geminiprotocol.net/")
# Display response information
echo "Status: ", response.status
echo "Meta: ", response.meta
# Get response body
let body = await response.body
echo body
# Always close when done
client.close()
waitFor main()import obiwan
proc handleRequest(request: Request) =
# Display request info
echo "URL requested: ", request.url
# Check for client certificate
if request.hasCertificate:
echo "Client certificate: ", request.certificate.commonName
# Send a response
request.respond(Success, "text/gemini", """
# Hello from ObiWAN!
This is a sample Gemini page served using the ObiWAN synchronous API.
""")
# Create server with certificate and key
let server = newObiwanServer(
certFile = "certs/cert.pem",
keyFile = "certs/privkey.pem"
)
# Start serving on port 1965 (default Gemini port)
server.serve(1965, handleRequest)import obiwan
import asyncdispatch
proc handleRequest(request: AsyncRequest): Future[void] {.async.} =
# Process the request
await request.respond(Success, "text/gemini", """
# Hello from ObiWAN!
This is a sample Gemini page served using the ObiWAN asynchronous API.
""")
proc main() {.async.} =
# Create server with certificate and key
let server = newAsyncObiwanServer(
certFile = "certs/cert.pem",
keyFile = "certs/privkey.pem"
)
# Start serving on port 1965 (default Gemini port)
await server.serve(1965, handleRequest)
waitFor main()ObiWAN includes built-in file serving functionality for Gemini servers:
import obiwan
proc main() =
# Create a server with default configuration
let server = newObiwanServer(
certFile = "certs/cert.pem",
keyFile = "certs/privkey.pem",
docRoot = "./content" # Path to content directory
)
# Start the server - it will automatically serve files from docRoot
server.serve(1965)
main()Features of the file serving system:
- Automatic MIME type detection based on file extensions
- Directory listings when no index.gmi is present
- Security against path traversal attacks
- Support for relative and absolute paths
- Handles index.gmi files for directories automatically
From the command line:
# Run the server with a custom document root
./build/obiwan-server --docroot=/path/to/content# Create client with identity certificate
let client = newObiwanClient(
certFile = "client-cert.pem",
keyFile = "client-key.pem"
)
# OR (preferred approach) load certificates after client creation
let client = newObiwanClient()
if client.loadIdentityFile("client-cert.pem", "client-key.pem"):
echo "Certificate loaded successfully"
let response = client.request("gemini://example.com/auth")
# Check if authentication succeeded
if response.status == Success:
echo "Authentication successful!"The loadIdentityFile approach is recommended because it allows making different
requests with and without client authentication during the same session.
# Check certificate verification in responses
let response = client.request("gemini://geminiprotocol.net/")
if response.hasCertificate:
echo "Certificate fingerprint: ", response.certificate.fingerprint
if response.isVerified:
echo "Certificate is fully verified"
elif response.isSelfSigned:
echo "Certificate is self-signed"
else:
echo "Certificate verification failed: ", response.verificationTo generate the full API documentation, run:
nimble docsThis will create HTML documentation in the docs/ directory. Open docs/index.html in your web browser to view it.
ObiwanClient/AsyncObiwanClient: Client interfacesObiwanServer/AsyncObiwanServer: Server interfacesRequest/AsyncRequest: Incoming client requestsResponse/AsyncResponse: Server responsesStatus: Gemini status codes (e.g.,Success,NotFound, etc.)
commonName: Get the subject common name from a certificatefingerprint: Get the SHA-256 fingerprint of a certificatehasCertificate: Check if a certificate is presentisVerified: Check if a certificate is verifiedisSelfSigned: Check if a certificate is self-signed
src/
├── obiwan.nim # Main package entrypoint
├── obiwan/
│ ├── common.nim # Shared types and protocols
│ ├── debug.nim # Debug utilities
│ ├── config.nim # Configuration management
│ ├── client.nim # Unified client executable (sync/async)
│ ├── server.nim # Unified server executable (sync/async)
│ ├── fs.nim # File system operations and MIME handling
│ ├── url.nim # URL parsing and manipulation
│ ├── tls/ # TLS implementation
│ │ ├── mbedtls.nim # C bindings
│ │ ├── socket.nim # Base socket
│ │ └── async_socket.nim # Async socket
Use these Nimble tasks to run the test suite:
# Run all tests
nimble test
# Run all tests in parallel (but out of order)
nimble testparallel
# Run specific test suites
nimble testserver # Server tests
nimble testclient # Client tests
nimble testtls # TLS implementation tests
nimble testurl # URL parsing testsObiWAN can be run using Docker with the included Dockerfile:
# Build the Docker image
docker build -t obiwan .
# Run the ObiWAN server container (with self-signed cert)
docker run -p 1965:1965 obiwan
# Run with custom content and certificates
docker run -p 1965:1965 \
-v /path/to/content:/app/content \
-v /path/to/certs:/app/certs \
obiwan --docroot=/app/content --cert=/app/certs/cert.pem --key=/app/certs/key.pem
# Run as a background service
docker run -d -p 1965:1965 --name obiwan-server --restart unless-stopped obiwanFor ARM64 systems, use platform emulation:
# Build and run with platform emulation
docker build --platform linux/amd64 -t obiwan .
docker run --platform linux/amd64 -p 1965:1965 obiwanThe Dockerfile uses Alpine Linux and configures ObiWAN to use system-provided mbedTLS libraries. To build manually on Alpine:
# 1. Install dependencies
apk add mbedtls-dev build-base openssl openssl-dev pkgconfig nim
# 2. Configure to use system mbedTLS
./use_system_mbedtls.sh
# 3. Build
nimble buildall- Comprehensive test suite
- Improved documentation
- Vendored mbedTLS 3.6.2
- TOML configuration file support
- Command-line argument parsing with docopt
- Unified client and server executables with sync/async modes
- Complete server implementation with file serving
- MIME type detection
- CGI support
- Certificate management (autogeneration)
- Proxy implementation (client and server)
- Caching mechanisms for improved performance
- Metrics and statistics collection
- Rate limiting implementation (for Slowdown status)
- Virtual hosting support
- Access control and authorization framework
All Rights Reserved.
- Gemini Protocol - For creating the Gemini protocol
- mbedTLS - For the TLS implementation
- Nim - For the programming language