Skip to content

corv89/ObiWAN

Repository files navigation

ObiWAN

A lightweight Gemini protocol client and server library in Nim.

What is ObiWAN?

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.

Features

  • 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

Installation

Prerequisites

  • Nim 2.2.2 or later
  • mbedTLS 3.6.2 (vendored as a git submodule)

MbedTLS

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.git

Or if you already cloned the repository:

git submodule update --init

The build system will automatically build mbedTLS when needed, or you can build it manually with:

nimble buildmbedtls

Installing Nim

On macOS:

brew install nim

On Linux:

# Debian/Ubuntu
sudo apt install nim

Installing ObiWAN

Install the ObiWAN package to compile or call it from Nim.

nimble install

Or, in your project add to your .nimble file:

requires "obiwan >= 0.6.0"

Generating Certificates

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 -nodes

Quick Start

Running ObiWAN

First, build all the programs:

# Build both client and server
nimble buildall

Then 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.pem

Command Line Options

ObiWAN provides a comprehensive command-line interface for both clients and servers:

Unified Client Options

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

Unified Server Options

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.

Configuration Files

ObiWAN supports TOML configuration files. By default, it looks for a file named obiwan.toml in:

  1. The current directory
  2. ~/.config/obiwan/config.toml
  3. /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 = true

Client Usage

Synchronous Client

import 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()

Asynchronous Client

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()

Server Usage

Synchronous Server

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)

Asynchronous Server

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()

Advanced Usage

File Serving

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

Client Certificates

# 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.

Server Certificate Verification

# 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.verification

API Documentation

To generate the full API documentation, run:

nimble docs

This will create HTML documentation in the docs/ directory. Open docs/index.html in your web browser to view it.

Main Types

  • ObiwanClient / AsyncObiwanClient: Client interfaces
  • ObiwanServer / AsyncObiwanServer: Server interfaces
  • Request / AsyncRequest: Incoming client requests
  • Response / AsyncResponse: Server responses
  • Status: Gemini status codes (e.g., Success, NotFound, etc.)

Certificate Functions

  • commonName: Get the subject common name from a certificate
  • fingerprint: Get the SHA-256 fingerprint of a certificate
  • hasCertificate: Check if a certificate is present
  • isVerified: Check if a certificate is verified
  • isSelfSigned: Check if a certificate is self-signed

Project Structure

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

Running Tests

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 tests

Docker Support

ObiWAN 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 obiwan

ARM64 Support (Apple Silicon M1/M2)

For 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 obiwan

Building on Alpine Linux (musl libc)

The 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

Roadmap

  • 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

License

All Rights Reserved.

Acknowledgements

About

A lightweight Gemini protocol client and server library in Nim

Topics

Resources

Stars

Watchers

Forks