Packages

A Gleam library for building Gemini servers

Current section

Files

Jump to
glemini src glemini.gleam
Raw

src/glemini.gleam

//// Main Glemini module. Contains the server and response functions.
//// # Example usage:
//// ```gleam
//// let config =
//// new_config()
//// |> add_ssl(certfile: "certs/cert.crt", keyfile: "certs/cert.key")
//// |> add_handler(fn(req) {
//// case req.path {
//// "/" -> gemtext_response([gemtext.heading1("Welcome to Glemini!")])
//// _ -> not_found_response()
//// }
//// })
//// let assert Ok(_) = start(config)
//// ```
import gleam/bit_array
import gleam/bytes_builder
import gleam/erlang/process.{type Subject}
import gleam/int
import gleam/io
import gleam/option.{type Option, None, Some}
import gleam/otp/actor
import gleam/otp/supervisor
import gleam/result
import gleam/string
import gleam/uri
import glisten.{type StartError, Packet}
import glemini/gemtext
/// Glemini server configuration.
pub type ServerConfig {
ServerConfig(
port: Int,
certfile: String,
keyfile: String,
request_handler: fn(Request) -> Response,
)
}
pub opaque type Request {
Request(host: String, path: String, query: String)
}
pub type Response {
SuccessResponse(status: Int, mimetype: String, body: String)
InputResponse(status: Int, prompt: String)
RedirectResponse(status: Int, uri: String)
ErrorResponse(status: Int, message: Option(String))
}
pub opaque type GleminiError {
RequestParseError
RequestSchemeError
}
@internal
pub fn main() {
io.println("Starting server!")
let config =
new_config()
|> add_ssl(certfile: "certs/cert.crt", keyfile: "certs/cert.key")
|> add_handler(fn(req) {
case req.path {
"/" ->
[gemtext.heading1("Welcome to Glemini!")]
|> gemtext_response()
_ -> not_found_response()
}
})
let assert Ok(_) = start(config)
io.println("Server running!")
process.sleep_forever()
}
/// Starts the gemini server.
/// Expects a `ServerConfig`
/// # Example
/// ```gleam
/// new_config()
/// |> add_ssl(certfile: "certs/cert.crt", keyfile: "certs/cert.key")
/// |> add_handler(fn(req) {
/// case req.path {
/// "/hello" -> gemtext_response([gemtext.heading1("Welcome to Glemini!")])
/// _ -> not_found_response()
/// }
/// })
/// |> start
/// ```
pub fn start(
config: ServerConfig,
) -> Result(Subject(supervisor.Message), StartError) {
glisten.handler(fn(_conn) { #(Nil, None) }, fn(req, _state, conn) {
let assert Packet(req) = req
let response =
handle_gemini_request(req, config.request_handler)
|> response_to_string
let assert Ok(_) = glisten.send(conn, bytes_builder.from_string(response))
actor.Stop(process.Normal)
})
|> glisten.serve_ssl(
port: config.port,
certfile: config.certfile,
keyfile: config.keyfile,
)
}
// Config
/// Creates a new server configuration with missing certificate and empty request handler.
pub fn new_config() -> ServerConfig {
ServerConfig(port: 1965, certfile: "", keyfile: "", request_handler: fn(_) {
ErrorResponse(51, None)
})
}
/// Adds ssl configuration to the server configuration.
pub fn add_ssl(
config: ServerConfig,
certfile certfile: String,
keyfile keyfile: String,
) -> ServerConfig {
ServerConfig(..config, certfile: certfile, keyfile: keyfile)
}
/// Adds a request handler to the server configuration.
/// The request handler is your main function where you'll do routing.
/// # Example handler
/// ```gleam
/// fn my_request_handler(req: Request) -> Response {
/// case req.path {
/// "/hello" -> gemtext_response([gemtext.heading1("Hello, world!")])
/// _ -> not_found_response()
/// }
/// }
/// ```
pub fn add_handler(
config: ServerConfig,
request_handler: fn(Request) -> Response,
) -> ServerConfig {
ServerConfig(..config, request_handler: request_handler)
}
// Response functions
// Success
/// Creates a success response, status 20, with a given mimetype and body.
pub fn success_response(mimetype: String, body: String) -> Response {
SuccessResponse(20, mimetype, body)
}
/// Creates a success response, status 20, with a gemtext body created from a list of gemtext lines.
pub fn gemtext_response(lines: gemtext.Lines) -> Response {
SuccessResponse(20, "text/gemini", gemtext.lines_to_string(lines))
}
// Input
/// Creates an input response, status 10, with a given prompt.
pub fn input_response(prompt: String) -> Response {
InputResponse(10, prompt)
}
/// Creates a sensitive input response, status 11, with a given prompt.
pub fn sensitive_input_response(prompt: String) -> Response {
InputResponse(11, prompt)
}
// Redirects
/// Creates a temporary redirect response, status 30, with a given uri.
pub fn temporary_redirect_response(uri: String) -> Response {
RedirectResponse(30, uri)
}
/// Creates a permanent redirect response, status 31, with a given uri.
pub fn permanent_redirect_response(uri: String) -> Response {
RedirectResponse(31, uri)
}
// Temporary errors
/// Creates a temporary failure response, status 40, with a given message.
pub fn temporary_failure_response(message: String) -> Response {
ErrorResponse(40, Some(message))
}
/// Creates a server unavailable response, status 41.
pub fn server_unavailable_response() -> Response {
ErrorResponse(41, None)
}
/// Creates a CGI error response, status 42.
pub fn cgi_error_response() -> Response {
ErrorResponse(42, None)
}
/// Creates a proxy error response, status 43.
pub fn proxy_error_response() -> Response {
ErrorResponse(43, None)
}
/// Creates a slow down response, status 44.
pub fn slow_down_response() -> Response {
ErrorResponse(44, None)
}
// Permanent errors
/// Creates a permanent failure response, status 50, with a given message.
pub fn permanent_failure_response(message: String) -> Response {
ErrorResponse(50, Some(message))
}
/// Creates a not found response, status 51.
pub fn not_found_response() -> Response {
ErrorResponse(51, None)
}
/// Creates a gone response, status 52.
pub fn gone_response() -> Response {
ErrorResponse(52, None)
}
/// Creates a proxy request refused response, status 53.
pub fn proxy_request_refused_response() -> Response {
ErrorResponse(53, None)
}
/// Creates a bad request response, status 59, with a given message.
pub fn bad_request_response(message: String) -> Response {
ErrorResponse(59, Some(message))
}
// Certificate errors
/// Creates a client certificate required response, status 60, with a given message.
pub fn client_certificate_required_response(message: String) -> Response {
ErrorResponse(60, Some(message))
}
/// Creates a certificate not authorized response, status 61, with a given message.
pub fn certificate_not_authorized_response(message: String) -> Response {
ErrorResponse(61, Some(message))
}
/// Creates a certificate not valid response, status 62, with a given message.
pub fn certificate_not_valid_response(message: String) -> Response {
ErrorResponse(62, Some(message))
}
// Helpers
/// Parse a request and pass it to the handler function.
fn handle_gemini_request(
req: BitArray,
handler: fn(Request) -> Response,
) -> Response {
case parse_request(req) {
Ok(request) -> handler(request)
Error(error) -> error_handler(error)
}
}
fn error_handler(error: GleminiError) -> Response {
case error {
RequestParseError -> bad_request_response("Couldn't parse request.")
RequestSchemeError -> bad_request_response("Bad request scheme.")
// _ -> temporary_failure_response("Unknown error")
}
}
/// Parse a request from a BitArray into a Request type.
fn parse_request(req: BitArray) -> Result(Request, GleminiError) {
use req <- result.try(
bit_array.to_string(req)
|> result.map(fn(x) { string.trim(x) })
|> result.replace_error(RequestParseError),
)
use uri <- result.try(
uri.parse(req) |> result.replace_error(RequestParseError),
)
case uri {
uri.Uri(
scheme: Some("gemini"),
host: Some(host),
query: Some(query),
path: path,
..,
) -> Ok(Request(host, path, query))
uri.Uri(scheme: Some("gemini"), host: Some(host), path: path, ..) ->
Ok(Request(host, path, ""))
_ -> Error(RequestSchemeError)
}
}
/// Convert a Response to a string.
fn response_to_string(res: Response) -> String {
case res {
SuccessResponse(status, mimetype, body) ->
int.to_string(status) <> " " <> mimetype <> "\r\n" <> body
InputResponse(status, prompt) ->
int.to_string(status) <> " " <> prompt <> "\r\n"
RedirectResponse(status, uri) ->
int.to_string(status) <> " " <> uri <> "\r\n"
ErrorResponse(status, message) ->
int.to_string(status)
<> " "
<> case message {
Some(message) -> message
None -> ""
}
<> "\r\n"
}
}