https://github.com/benob/gemini
Building blocks for making async Gemini servers
https://github.com/benob/gemini
Last synced: 6 months ago
JSON representation
Building blocks for making async Gemini servers
- Host: GitHub
- URL: https://github.com/benob/gemini
- Owner: benob
- Created: 2021-02-02T21:03:51.000Z (almost 5 years ago)
- Default Branch: master
- Last Pushed: 2021-03-20T13:15:26.000Z (over 4 years ago)
- Last Synced: 2024-08-04T03:07:59.859Z (over 1 year ago)
- Language: Nim
- Size: 25.4 KB
- Stars: 2
- Watchers: 1
- Forks: 2
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
- awesome-nim - gemini - Building blocks for creating Gemini servers and clients. (Web / Protocols)
README
Gemini
=====
Building blocks for creating Gemini servers and clients.
Unlike [geminim](https://github.com/ardek66/geminim), this library does not handle serving files, cgi or configuration.
Example
-------
This library supports both Socket and AsyncSocket servers and clients. The async variants are prefixed with Async.
Since Gemini requires TLS, you have to provide a certificate and private key associated with your domain name.
Example server:
```
import gemini
proc handle(req: Request) =
await req.respond(Success, "text/gemini", "# Hello world")
var server = newGeminiServer(certFile = "fullchain.pem", keyFile = "privkey.pem")
server.serve(Port(1965), handle)
```
Example async server:
```
import asyncdispatch
import gemini
proc handle(req: AsyncRequest) {.async.} =
await req.respond(Success, "text/gemini", "# Hello world")
var server = newAsyncGeminiServer(certFile = "fullchain.pem", keyFile = "privkey.pem")
waitFor server.serve(Port(1965), handle)
```
Example client:
```
import gemini
try:
let client = newGeminiClient()
let response = await client.request("gemini://gemini.circumlunar.space")
echo "status: " & $response.status
echo "meta: " & response.meta
echo "body: " & response.body
except GeminiError:
echo getCurrentExceptionMsg()
```
Example async client:
```
import asyncdispatch
import gemini
proc main() {.async.} =
try:
let client = newAsyncGeminiClient()
let response = await client.request("gemini://gemini.circumlunar.space")
echo "status: " & $response.status
echo "meta: " & response.meta
echo "body: " & await response.body
except GeminiError:
echo getCurrentExceptionMsg()
waitFor main()
```
Documentation
-------------
See [https://gemini.circumlunar.space/docs/specification.html](https://gemini.circumlunar.space/docs/specification.html) for the protocol specification.
Create a new server:
```
proc newGeminiServer*(reuseAddr = true; reusePort = false, certFile = "", keyFile = ""): GeminiServer
proc newAsyncGeminiServer*(reuseAddr = true; reusePort = false, certFile = "", keyFile = ""): AsyncGeminiServer
```
Listen on a given port:
```
proc serve*(server: GeminiServer, port = Port(1965), callback: proc (request: Request) {.closure.}, address = "")
proc serve*(server: AsyncGeminiServer, port = Port(1965), callback: proc (request: AsyncRequest): Future[void] {.closure.}, address = "") {.async.}
```
The callback is given a request which contains the url requested and a certificate if the client provided one.
Note that when address contains a column, ":" the code assumes that you are specifying an IPv6 address (such as :: or ::1 which correspond to 0.0.0.0 and 127.0.0.1).
On Linux hosts, IPv4 requests are automatically mapped to IPv6 when listening to "::".
If the request url cannot be parsed, the server replies with status 50 INTERNAL ERROR.
Use respond() to send back a response:
```
proc respond*(req: Request, status: Status, meta: string, body: string = "")
proc respond*(req: AsyncRequest, status: Status, meta: string, body: string = "") {.async.}
```
Supported status codes:
```
type Status* = enum
Input = 10
SensitiveInput = 11
Success = 20
TempRedirect = 30
Redirect = 31
TempError = 40
ServerUnavailable = 41
CGIError = 42
ProxyError = 43
Slowdown = 44
Error = 50
NotFound = 51
Gone = 52
ProxyRefused = 53
MalformedRequest = 59
CertificateRequired = 60
CertificateUnauthorized = 61
CertificateNotValid = 62
```
To create a new client:
```
proc newGeminiClient*(maxRedirects = 5, certFile = "", keyFile = ""): GeminiClient
proc newAsyncGeminiClient*(maxRedirects = 5, certFile = "", keyFile = ""): AsyncGeminiClient
```
You can provide a certFile and keyFile to be sent along the query.
Then you can submit a request to a "gemini://" url
```
proc request*(client: GeminiClient, url: string): Response
proc request*(client: AsyncGeminiClient, url: string): Future[AsyncResponse] {.async.}
```
The response contains three fields: the returned status code and the associated meta, and the server certificate. Redirects are handled.
You cat get the response body with:
```
proc body*(response: Response): string
proc body*(response: Response): Future[string] {.async.}
```
If an exception occurs such as a protocol error, you will get a GeminiError exception.
Working with certificates
-------------------------
Both requests and responses have a few functions for handling certificates:
* `hasCertificate()` is true when there was a certificate
* `isVerified()` is true when the certificate chain was verfied up to a known root (as with a https client)
* `isSelfSigned()` is true when the certificate is self signed
You are supposed to handle self-signed certificates in a "Trust On First Use" fashion by building a whitelist like openssh.
You can inspect a certificate with `$req.certificate`, get the associated common name with `req.certificate.commonName()`, and get a sha256 fingerprint with `req.certificate.fingerprint()`.
Warning
-------
The TLS implementation in Nim is not well tested, it may contain vulnerabilities.
Note that the current code is too permissive and accepts SSL2/SSL3 handshakes.
Also, since net/asyncnet do not expose the underlying SSL sockets, we use a terrible hack which complexifies the code quite a bit.
See [https://github.com/nim-lang/Nim/issues/17021](https://github.com/nim-lang/Nim/issues/17021) for details.
Todo
----
- [x] Handle client certificates
- [x] Trust self-signed certificates
- [x] Non-async interface
- [ ] Parse text/gemini