Network.Framework and URLSession support for the Gemini protocol.
This module is intended as an implementation of the Gemini protocol specification: https://geminiprotocol.net/docs/protocol-specification.gmi
This codebase began as a clone/fork of the original project at frozendevil/GeminiProtocol.
Subsequent updates in this repository were developed by GPT-3.5 Codex supervised by Robin Barooah.
Calling URLProtocol.registerClass(GeminiProtocol.self) will cause your normal URLSession code to "Just Work" with gemini:// URLs. The URLResponse you receive will be a GeminiURLResponse with statusCode and meta properties.
Use GeminiClient directly for lower-level requests:
let request = URLRequest(url: URL(string: "gemini://gemini.circumlunar.space/")!)
let client = try GeminiClient(request: request)
let (header, body) = try await client.start(timeout: 20)GeminiClient uses system TLS trust by default. If you need insecure certificate acceptance for debugging only, use:
let client = try GeminiClient(request: request, tlsMode: .insecureNoVerification)Parse text/gemini documents into a typed AST with diagnostics:
let options = GemtextParserOptions(
mode: .permissive,
baseURL: URL(string: "gemini://example.org/")!
)
let document = try GemtextParser.parse(gemtextString, options: options)Renderer helpers are also available for normalization to plain text and Markdown:
let plain = GemtextRenderer.plainText(
from: document,
options: .init(normalizeWhitespace: true)
)
let markdown = GemtextRenderer.markdown(
from: document,
options: .init(normalizeWhitespace: true, includePreformatAltTextAsInfoString: true)
)Parse both currently published companion specifications from https://geminiprotocol.net/docs/companion/ (robots.txt and lightweight subscription feeds):
let robots = try GeminiRobotsParser.parse(robotsText)
let disallowedForIndexer = robots.disallowPrefixes(for: [.indexer])
let feedURL = URL(string: "gemini://example.org/gemlog/")!
let feed = try GeminiSubscriptionParser.parse(gemtextFeedPage, feedURL: feedURL)An executable example target is included that fetches gemini://warmedal.se/~antenna/, fetches all linked Gemini pages from that front page, converts responses to Markdown, and writes a local output folder.
swift run AntennaMirrorExampleOptional output directory:
swift run AntennaMirrorExample /tmp/AntennaMirrorOutputGeminiProtocol.swiftcontains the implementation of theURLSessionsupport.GeminiNetwork.swiftis aNetwork.frameworkimplementation of a Gemini client.GemtextParser.swiftandGemtextTypes.swiftimplement a line-oriented parser fortext/gemini.GemtextRenderer.swiftprovides plain-text and Markdown normalization helpers.CompanionParsers.swiftandCompanionTypes.swiftimplement companion-spec parsing helpers.
- Request URIs are normalized to match spec requirements:
- userinfo is rejected
- fragments are omitted
- empty paths are normalized to
/ - request line length is capped at 1024 bytes
- Response parsing enforces key protocol constraints:
- UTF-8 headers with mandatory CRLF delimiter
- UTF-8 BOM at header start is rejected
- status codes outside
10...69are rejected - undefined status codes in
10...69are handled by class fallback (1x,2x, etc.) - optional meta for
4x,5x,6xis supported - required meta for
1x,2x,3xis enforced
- TLS 1.2+ is required by default.
- MIME type grammar for
2xresponses is not fully validated; the header meta is exposed as-is. - Redirect-following policy (including 5-hop limit) is intentionally not implemented in the low-level client; callers decide redirect behavior.
- Automatic client-certificate flows for
6xresponses are not implemented by this library. - Robots parser intentionally ignores non-core robots extensions (e.g.
Allow, crawl-delay, wildcard matching); only comment,User-agent, andDisallowdirectives are interpreted. - In permissive mode, subscription feeds missing a level-1 heading use the feed URL as a fallback title and surface a diagnostic; strict mode rejects them.
- Subscription parser title sanitization after a date prefix uses conservative heuristics and may differ from client-specific presentation choices.
swift testruns local tests.GEMINI_LIVE_TESTS=1 swift testalso runs internet-backed tests against multiple public Gemini capsules.
Build the DocC archive locally from the package root:
swift package dump-symbol-graph
docc convert Sources/GeminiProtocol/GeminiProtocol.docc \
--fallback-display-name GeminiProtocol \
--fallback-bundle-identifier com.example.GeminiProtocol \
--fallback-bundle-version 1.0.0 \
--additional-symbol-graph-dir .build/arm64-apple-macosx/symbolgraph \
--output-path .build/docc/GeminiProtocol.doccarchive