fulmin.client 0.1.3

dotnet add package fulmin.client --version 0.1.3
                    
NuGet\Install-Package fulmin.client -Version 0.1.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="fulmin.client" Version="0.1.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="fulmin.client" Version="0.1.3" />
                    
Directory.Packages.props
<PackageReference Include="fulmin.client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add fulmin.client --version 0.1.3
                    
#r "nuget: fulmin.client, 0.1.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package fulmin.client@0.1.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=fulmin.client&version=0.1.3
                    
Install as a Cake Addin
#tool nuget:?package=fulmin.client&version=0.1.3
                    
Install as a Cake Tool

Fulmin

Fulmin provides a server and client for the Gemini protocol.

Fulmin.Server

Nuget

Install the nuget in your project:

nuget add package fulmin.server

Usage

The Fulmin.Server.GeminiListener class is an object oriented listener to handle serverside gemini request.

open Fulmin.Server

let client = new GeminiListener(GeminiListenerConfiguration.Default)

client.Start()
task {
    use! ctx = client.GetContextAsync()
    let! writer = ctx.Success()
    do! writer.WriteHeading1Async("Hello world")
}
task {
    use! ctx = client.GetContextAsync()
    do! ctx.NotFound("Not found")
}

The context returned by GetContextAsync is disposable and should be closed to terminate the connection.

Once acquiered, it's possible to process a request in parallel with other tasks.

The GemWriter class proposes methods to write GemText.

Client certificate

The context ClientCertificate property contains the client certificate if available. The listener will return a CertificateNotValid response automatically if the certificate has expired.

Configuration

The configuration contains the following parameters:

  • Address: the listening ip address. Default is 0.0.0.0 any.
  • Port: the listening tcp port. Default is 1965, the default gemini port.
  • Certificate: the server certificate

it is possible to specify only specific configuration properties by using GeminiListenerConfiguration.Default as a base:

let config = { GeminiListenerConfiguration.Default with Port = 1966 }

ServerCertificate

By default, fulmin generate a new temporary server certificate for localhost with a duration of 24 hours.

For production use, it is possible to pass directly a X509Certificate2 instance, or to let gemini load it from PEM files:

let config1 = { GeminiListenerConfiguration.Default with 
                    Certificate = X509 myCertificate }
let config2 = { GeminiListenerConfiguration.Default with 
                    Certificate = 
                        File(certPath, 
                             privatekeyPath, 
                             None) }

it is also possible to let gemini generate and renew automatically by specifying a certificate host name and a duration:

let config = { GeminiListenerConfiguration.Default with 
                   Certificate =
                        File(certPath, 
                             privatekeyPath, 
                             Some { 
                                Host = "mysite.com"
                                Duration=TimeSpan.FromDays(365)}) }

It will store the certificate and the key in specified files, and renew the certificate once expired.

GemServer

For a more functional approach, Fulmin.Server.GemServer defines composable function to write complete gemini services.

let capsule =
    choose [
        path "/" >=>
            OK [
                h1 "Fulmin"
                lf
                text "Fulmin server is a gemini server in F#"
                link "hello" "Hello"
            ]
        path "/hello" >=> 
            input "What's your name?" (fun name ->
                OK [
                    h1 $"Hello {name}"
                    lf
                    link "/" "Home"
                    link "." "Again"
                ])
    ]
let stop = startServer GeminiListenerConfiguration.Default capsule

GemPart

The capsule is build from GemPart, functions that take a GeminiContext and return maybe later a GeminiContext.

They can be used as filters, or responses, and composed together.

for instance path "/" returns the context if the request local path is /. Otherwise it returns nothing.

OK [ ... ] always write a success response with specified lines.

When combined with the >=> kleisly operator, it makes a GemPart that write the lines to the response if the path is /.

The operator choose execute GemParts one after the other until one retuns a value.

OK

The OK GemPart ouput a 20 success response and write all formatted lines as content

input

input check whether there is a query string.

If not, it returns a 10 input response with specified prompt.

If present, it calls the function with the query to get a content to return.

Certificate.require

Certificate.require check whether there is a client certificate.

If not, it returns a 60 certificate required response with specified prompt.

If present, the function is called with the certificate to get a content to return.

path "/admin" >=>
    Certificate.require "Please authenticate" (fun cert ->
        OK [
            h1 $"Welcome {cert.Subject}"
            let h = cert.GetCertHash()
            text $"Hash: {System.Convert.ToHexStringLower(h)}"
            link "/" "Back"
        ]
    )
file

the file function output a gemtext file from disk.

path "/file" >=> file "file.gmi"
Redirects

Redirect.temporary and Redirect.permanent redirect to specified urls:

path "/redirect" >=> Redirect.temporary "test"
OKAsync

OkAsync work like OK except that it accepts a IAsyncEnumerable, and render it line by line.

This can be used for progressive loading or streaming.

Notice that some client load the page completely before starting the rendering. However gemini protocol is line oriented, and many client render lines as they are received.

        path "/fizzbuzz" >=> parseInput "Up to ?" parseInt (fun value -> 
                OKAsync (taskSeq {
                    h1 "Async FizzBuzz"

                    for i in 1 .. value  do
                        match i%3, i%5 with
                        | 0, 0 -> text "FizzBuzz"
                        | 0, _ -> text "Fizz"
                        | _, 0 -> text "Buzz"
                        | _ -> text (string i)
                        do! Task.Delay(1000)
                }))

This example returns the fizzbuzz results line by line with a 1 second interval between them.

regexPath

regexPath filter queries like path, but takes a regular expression. The matched group values are passed to the specified function:

        pathRegex @"/page/(?<id>\d+)/content" (fun values ->
            OK [
                h1 $"""Page {values["id"]}"""
            ])
GemText

All gemtext line types can be used with OK and OKAsync.

path "/test" >=>
    OK [
        h1 "Heading 1"
        h2 "Heading 2"
        h3 "Heading 3"
        text "Some text"
        pre' "fsharp" [
            "let add x y = x + y"
        ]
        lf // short form for empty line
        lst [ "Item1"; "Item2"; "Item3"]
        lf
        quote "Someone said that in on the internet"
        link "/" "home"
    ]

The pre and pre' function avoid the explicit use of preformatted text toggles. It still possible to use them (preToggle and preToggle') and emit text line between.

In the same way, lst emit multiple listItem at once.

Fulmin.Client

Fulmin client is a full featured client for the gemini protocol.

task {
    // create a client
    let client = GeminiClient("gemini://geminiprotocol.net")
    // query a specific path
    use! response = client.RequestAsync("/")

    match response with
    | Success r ->
        // success ! we can read all line, parsed
        for line in r.ReadLinesAsync(true, CancellationToken.None) do
            printfn "%A"  line
    | _ -> printfn "%A" response

}

Client certificate

It is possible to create an send certificate with the request:

task {
    let client = GeminiClient("localhost")
    let cert = createCertificate(createKey(), "Jérémie")

    use! response = client.RequestAsync("/", cert)
    //...
}

Certificate management must be currently done by the application.

Redirects

By default, the client auto follows redirects with a maximum of 5. It is possible to change the maximum count while constructing the client.

When the miximum count is reached, an exception occures.

However, by passing -1 as a maximum count, redirects do cause error anymore, but just return the redirect information directly.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.3 61 11/30/2025
0.1.2 171 11/24/2025
0.1.1 157 11/22/2025
0.1.0 228 11/22/2025