commit 59d957dd164940a6cf98caec83938b281174e4b2 Author: Martin Hart <114212671+m4rtinh4rt@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:06:08 +0200 Initial commit Diffstat:
120 files changed, 1115 insertions(+), 0 deletions(-)
diff --git a/README.md b/README.md @@ -0,0 +1 @@ +# geminid diff --git a/bin/dune b/bin/dune @@ -0,0 +1,4 @@ +(executable + (public_name gemini) + (name main) + (libraries ogemini daemonize landlock_ocaml seccomp)) diff --git a/bin/main.ml b/bin/main.ml @@ -0,0 +1,56 @@ +open Daemonize +open Ogemini +open Config +open Landlock_ocaml.Landlock +open Seccomp + +module Flag = struct + let usage = "geminid [OPTION]" + let foreground = ref false + let config_file = ref "" + + let speclist = + [ "-f", Arg.Set foreground, "Run geminid in foreground mode" + ; "-c", Arg.Set_string config_file, "Specify a non-default path for config file" + ] +end + +module Conf = struct + let default_config_file = "/etc/geminid.conf" + let config_exists filename = Unix.access filename [ Unix.F_OK; Unix.R_OK; Unix.W_OK ] +end + +let () = + Arg.parse Flag.speclist (fun _ -> ()) Flag.usage; + + let config_path = + (match !Flag.config_file with + | "" -> Conf.default_config_file + | _ -> !Flag.config_file) in + + (* Parse config file *) + parse config_path; + + if not (Sys.file_exists config.logfile) then ignore (open_out_gen [Open_creat] 0o600 config.logfile); + + (* Change user before sandbox, later we have no access to /etc/passwd *) + let user = Unix.getpwnam config.owner in + let _ = Unix.setuid user.pw_uid in + let _ = Unix.setgid user.pw_gid in + + (* Sandbox filesystem *) + let fd = landlock_init () in + ignore (landlock_new_rule fd config.logfile [LANDLOCK_ACCESS_FS_WRITE_FILE]); + ignore (landlock_new_rule fd (Filename.dirname config.cert) [LANDLOCK_ACCESS_FS_READ_DIR ; LANDLOCK_ACCESS_FS_READ_FILE]); + ignore (landlock_new_rule fd config.dir [LANDLOCK_ACCESS_FS_READ_DIR ; LANDLOCK_ACCESS_FS_READ_FILE]); + ignore (landlock_new_rule fd config_path [LANDLOCK_ACCESS_FS_READ_FILE]); + ignore (landlock_finish fd); + + if not !Flag.foreground then daemonize config.logfile; + + (* Restrict kernel surface *) + ignore (seccomp ()); + + print_endline "Starting server..."; + + Lwt_main.run @@ Server.main (config) diff --git a/build_deb.sh b/build_deb.sh @@ -0,0 +1,3 @@ +#!/bin/env sh + +dpkg-deb --build --root-owner-group gemini_1.0_1_amd64 diff --git a/certs/cert.pem b/certs/cert.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBPDCB76ADAgECAhR/kEHZP19GYZjrXlSXBB8l9pyvYzAFBgMrZXAwFDESMBAG +A1UEAwwJbG9jYWxob3N0MB4XDTIyMDQxODE3MDQxOVoXDTIzMDQxODE3MDQxOVow +FDESMBAGA1UEAwwJbG9jYWxob3N0MCowBQYDK2VwAyEAciSfe3qIXggEcHudhh6/ +wF0UR0r9fwcbOJs/pUhRDomjUzBRMB0GA1UdDgQWBBTNghiGhzyMRGrLqwoTKgF5 +OCRntzAfBgNVHSMEGDAWgBTNghiGhzyMRGrLqwoTKgF5OCRntzAPBgNVHRMBAf8E +BTADAQH/MAUGAytlcANBACVLtd86zRVvNXfKHOp3FNzWSDls9a/9tDkQyyXMPi0X +aKkBV/ux7QLe0mlAgoZA0etFamBWppzR5vqLgksJSQg= +-----END CERTIFICATE----- diff --git a/certs/key.pem b/certs/key.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEINcdqUJHJrojxwr7CKJ7BPjYRujuyOoEcEfyhb405cLo +-----END PRIVATE KEY----- diff --git a/config.toml b/config.toml @@ -0,0 +1,9 @@ +[servers] +owner = "user" +port = 1965 +host = "127.0.0.1" +cert = "certs/cert.pem" +key = "certs/key.pem" +dir = "static/" +logfile = "/tmp/log.txt" +max_req = 20 diff --git a/dune-project b/dune-project @@ -0,0 +1,2 @@ +(lang dune 2.9) +(name gemini) diff --git a/gemini.opam b/gemini.opam diff --git a/gemini_1.0_1_amd64.deb b/gemini_1.0_1_amd64.deb Binary files differ. diff --git a/gemini_1.0_1_amd64/DEBIAN/control b/gemini_1.0_1_amd64/DEBIAN/control @@ -0,0 +1,7 @@ +Source: gemini +Version: 1.0 +Maintainer: Volodymyr Patuta <me@vpatuta.xyz> +Build-Depends: libc6 (>= 2.35), libgmp10 (>= 2:6.2.1+dfsg), libseccomp2 (>= 0.0.0~20120605) +Description: Simple Gemini server for static files. +Package: gemini +Architecture: amd64 diff --git a/gemini_1.0_1_amd64/etc/geminid.conf b/gemini_1.0_1_amd64/etc/geminid.conf @@ -0,0 +1,9 @@ +[servers] +owner = "user" +port = 1965 +host = "127.0.0.1" +cert = "~/dev/gemini/certs/cert.pem" +key = "~/dev/gemini/certs/key.pem" +dir = "~/dev/gemini/static" +logfile = "/tmp/log.txt" +max_req = 20 diff --git a/gemini_1.0_1_amd64/usr/local/bin/gemini b/gemini_1.0_1_amd64/usr/local/bin/gemini Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/config/.config.objs/byte/config.cmi b/gemini_1.0_1_amd64/usr/local/lib/config/.config.objs/byte/config.cmi Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/config/.config.objs/byte/config.cmo b/gemini_1.0_1_amd64/usr/local/lib/config/.config.objs/byte/config.cmo Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/config/.config.objs/byte/config.cmt b/gemini_1.0_1_amd64/usr/local/lib/config/.config.objs/byte/config.cmt Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/config/.config.objs/native/config.cmx b/gemini_1.0_1_amd64/usr/local/lib/config/.config.objs/native/config.cmx Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/config/.merlin-conf/lib-config b/gemini_1.0_1_amd64/usr/local/lib/config/.merlin-conf/lib-config Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/config/config.a b/gemini_1.0_1_amd64/usr/local/lib/config/config.a Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/config/config.cmxa b/gemini_1.0_1_amd64/usr/local/lib/config/config.cmxa Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/config/config.ml b/gemini_1.0_1_amd64/usr/local/lib/config/config.ml @@ -0,0 +1,72 @@ +open Otoml + +type srv = + { mutable key : string + ; mutable cert : string + ; mutable dir : string + ; mutable logfile : string + ; mutable port : int + ; mutable max_req : int + ; mutable host : Unix.inet_addr + ; sock : Lwt_unix.file_descr + ; tls : Tls.Core.tls_version * Tls.Core.tls_version + ; mutable owner : string + } + +let config : srv = + { key = Filename.concat (Sys.getcwd ()) "certs/key.pem" + ; cert = Filename.concat (Sys.getcwd ()) "certs/cert.pem" + ; port = 1965 + ; dir = "./" + ; logfile = "/tmp/ogemini.log" + ; host = Unix.inet_addr_loopback + ; sock = Lwt_unix.(socket PF_INET SOCK_STREAM 0) + ; max_req = 50 + ; tls = Tls.Core.(`TLS_1_2, `TLS_1_3) + ; owner = "user" + } + +let parse filename = + let conf = Sys.file_exists filename in + match conf with + | false -> + Logs.warn + @@ fun m -> m "Configuration file %s not found, using default settings" filename + | true -> + (try + let toml = Otoml.Parser.from_file filename in + let cfg = ref config in + (match Otoml.find toml Otoml.get_string [ "servers"; "dir" ] with + | "" -> Logs.warn @@ fun m -> m "[servers] dir config is empty. Defaults to './'" + | dir -> !cfg.dir <- dir); + (match Otoml.find toml Otoml.get_string [ "servers"; "logfile" ] with + | "" -> Logs.warn @@ fun m -> m "[servers] logfile config is empty. Defaults to '/tmp/ogemini.log'" + | logfile -> !cfg.logfile <- logfile); + (match Otoml.find toml Otoml.get_string [ "servers"; "cert" ] with + | "" -> Logs.warn @@ fun m -> m "[servers] cert config is empty" + | cert -> !cfg.cert <- cert); + (match Otoml.find toml Otoml.get_string [ "servers"; "key" ] with + | "" -> Logs.warn @@ fun m -> m "[servers] key config is empty" + | key -> !cfg.key <- key); + (match Otoml.find toml Otoml.get_string [ "servers"; "host" ] with + | "" -> + Logs.warn @@ fun m -> m "[servers] host config is empty. Defaults to localhost" + | host -> !cfg.host <- Unix.inet_addr_of_string host); + (match Otoml.find toml (Otoml.get_integer ~strict:false) [ "servers"; "port" ] with + | port -> !cfg.port <- port); + (match Otoml.find toml (Otoml.get_string ~strict:false) [ "servers"; "owner" ] with + | "" -> Logs.warn @@ fun m -> m "[servers] owner config is empty" + | owner -> !cfg.owner <- owner); + match Otoml.find toml Otoml.get_integer [ "servers"; "max_req" ] with + | max_req -> !cfg.max_req <- max_req + with + | Sys_error err -> failwith + (Printf.sprintf + "Could not read config file: %s" + err) + | Otoml.Parse_error (pos, msg) -> + failwith + (Printf.sprintf + "Could not parse config file %s: %s" + filename + (Otoml.Parser.format_parse_error pos msg))) diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize.cmi b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize.cmi Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize.cmo b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize.cmo Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize.cmt b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize.cmt Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize__.cmi b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize__.cmi Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize__.cmo b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize__.cmo Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize__.cmt b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize__.cmt Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize__Log.cmi b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize__Log.cmi Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize__Log.cmo b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize__Log.cmo Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize__Log.cmt b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/byte/daemonize__Log.cmt Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/daemonize.impl.all-deps b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/daemonize.impl.all-deps @@ -0,0 +1,2 @@ +Daemonize__ +Log diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/daemonize.ml.d b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/daemonize.ml.d @@ -0,0 +1 @@ +lib/daemonize/daemonize.ml: List Log Lwt_unix Sys Unix diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/daemonize__Log.impl.all-deps b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/daemonize__Log.impl.all-deps @@ -0,0 +1 @@ +Daemonize__ diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/log.ml.d b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/log.ml.d @@ -0,0 +1 @@ +lib/daemonize/log.ml: Printf Unix diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/native/daemonize.cmx b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/native/daemonize.cmx Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/native/daemonize__.cmx b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/native/daemonize__.cmx Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/native/daemonize__Log.cmx b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.daemonize.objs/native/daemonize__Log.cmx Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/.merlin-conf/lib-daemonize b/gemini_1.0_1_amd64/usr/local/lib/daemonize/.merlin-conf/lib-daemonize Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/daemonize.a b/gemini_1.0_1_amd64/usr/local/lib/daemonize/daemonize.a Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/daemonize.cma b/gemini_1.0_1_amd64/usr/local/lib/daemonize/daemonize.cma Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/daemonize.cmxa b/gemini_1.0_1_amd64/usr/local/lib/daemonize/daemonize.cmxa Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/daemonize.cmxs b/gemini_1.0_1_amd64/usr/local/lib/daemonize/daemonize.cmxs Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/daemonize.ml b/gemini_1.0_1_amd64/usr/local/lib/daemonize/daemonize.ml @@ -0,0 +1,31 @@ +open Log + +let log_stop_signals signals pid = + List.map (fun s -> + Sys.set_signal s (Signal_handle (fun _ -> + log_msg ("DAEMON STOP pid: " ^ string_of_int pid); + exit 0; + ))) signals + +(* Set logfile to an empty string to enforce a no-log policy *) +let daemonize logfile = + if Lwt_unix.fork () <> 0 then + exit 0; + + ignore (Unix.umask (022)); + ignore (Unix.setsid ()); + + Unix.close Unix.stdout; + Unix.close Unix.stderr; + + if logfile <> "" then + (* Redirect stdout and stderr channels to logfile *) + let oc = open_out_gen [Open_append; Open_creat; Open_wronly] 0o600 logfile in + Unix.dup2 (Unix.descr_of_out_channel oc) Unix.stdout; + Unix.dup2 (Unix.descr_of_out_channel oc) Unix.stderr; + + let pid = (Unix.getpid ()) in + log_msg ("DAEMON START pid: " ^ string_of_int pid); + flush stdout; + + ignore (log_stop_signals [Sys.sigterm ; Sys.sigint] pid); diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/daemonize__.ml-gen b/gemini_1.0_1_amd64/usr/local/lib/daemonize/daemonize__.ml-gen @@ -0,0 +1,2 @@ +(** @canonical Daemonize.Log *) +module Log = Daemonize__Log diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/dune b/gemini_1.0_1_amd64/usr/local/lib/daemonize/dune @@ -0,0 +1,3 @@ +(library + (name daemonize) + (libraries lwt.unix unix)) diff --git a/gemini_1.0_1_amd64/usr/local/lib/daemonize/log.ml b/gemini_1.0_1_amd64/usr/local/lib/daemonize/log.ml @@ -0,0 +1,10 @@ +let datetime = + let date = Unix.localtime (Unix.time ()) in + Printf.sprintf + "%d/%d/%d %d:%d" (* YYYY/MM/DD H:M *) + (date.tm_year + 1900) (date.tm_mon + 1) + date.tm_mday date.tm_hour date.tm_min + +let log_msg msg = + Printf.printf "%s %s\n" datetime msg; + flush stdout diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/byte/landlock_ocaml.cmi b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/byte/landlock_ocaml.cmi Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/byte/landlock_ocaml.cmo b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/byte/landlock_ocaml.cmo Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/byte/landlock_ocaml.cmt b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/byte/landlock_ocaml.cmt Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/byte/landlock_ocaml__Landlock.cmi b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/byte/landlock_ocaml__Landlock.cmi Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/byte/landlock_ocaml__Landlock.cmo b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/byte/landlock_ocaml__Landlock.cmo Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/byte/landlock_ocaml__Landlock.cmt b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/byte/landlock_ocaml__Landlock.cmt Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/landlock.ml.d b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/landlock.ml.d @@ -0,0 +1 @@ +lib/landlock_ocaml/landlock.ml: diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/landlock_ocaml__Landlock.impl.all-deps b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/landlock_ocaml__Landlock.impl.all-deps @@ -0,0 +1 @@ +Landlock_ocaml diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/native/landlock_ocaml.cmx b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/native/landlock_ocaml.cmx Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/native/landlock_ocaml__Landlock.cmx b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.landlock_ocaml.objs/native/landlock_ocaml__Landlock.cmx Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.merlin-conf/lib-landlock_ocaml b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/.merlin-conf/lib-landlock_ocaml Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/dlllandlock_ocaml_stubs.so b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/dlllandlock_ocaml_stubs.so Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/dune b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/dune @@ -0,0 +1,3 @@ +(library + (name landlock_ocaml) + (foreign_stubs (language c) (names landlock_stubs))) diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock.ml b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock.ml @@ -0,0 +1,20 @@ +type permissions = + | LANDLOCK_ACCESS_FS_EXECUTE + | LANDLOCK_ACCESS_FS_WRITE_FILE + | LANDLOCK_ACCESS_FS_READ_FILE + | LANDLOCK_ACCESS_FS_READ_DIR + | LANDLOCK_ACCESS_FS_REMOVE_DIR + | LANDLOCK_ACCESS_FS_REMOVE_FILE + | LANDLOCK_ACCESS_FS_MAKE_CHAR + | LANDLOCK_ACCESS_FS_MAKE_DIR + | LANDLOCK_ACCESS_FS_MAKE_REG + | LANDLOCK_ACCESS_FS_MAKE_SOCK + | LANDLOCK_ACCESS_FS_MAKE_FIFO + | LANDLOCK_ACCESS_FS_MAKE_BLOCK + | LANDLOCK_ACCESS_FS_MAKE_SYM + +type permissions_mask = permissions list + +external landlock_init: unit -> int = "caml_landlock_init" +external landlock_new_rule: int -> string -> permissions_mask -> int = "caml_landlock" +external landlock_finish: int -> int = "caml_landlock_finish" diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock_ocaml.a b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock_ocaml.a Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock_ocaml.cma b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock_ocaml.cma Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock_ocaml.cmxa b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock_ocaml.cmxa Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock_ocaml.cmxs b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock_ocaml.cmxs Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock_ocaml.ml-gen b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock_ocaml.ml-gen @@ -0,0 +1,2 @@ +(** @canonical Landlock_ocaml.Landlock *) +module Landlock = Landlock_ocaml__Landlock diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock_stubs.c b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/landlock_stubs.c @@ -0,0 +1,139 @@ +#define _GNU_SOURCE +#include <fcntl.h> +#include <linux/landlock.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/prctl.h> +#include <sys/syscall.h> +#include <unistd.h> +#include <caml/mlvalues.h> + +#ifndef landlock_create_ruleset +static inline int +landlock_create_ruleset(const struct landlock_ruleset_attr *const attr, + const size_t size, const __u32 flags) +{ + return syscall(__NR_landlock_create_ruleset, attr, size, flags); +} +#endif + +#ifndef landlock_add_rule +static inline int landlock_add_rule(const int ruleset_fd, + const enum landlock_rule_type rule_type, + const void *const rule_attr, + const __u32 flags) +{ + return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, + flags); +} +#endif + +#ifndef landlock_restrict_self +static inline int landlock_restrict_self(const int ruleset_fd, + const __u32 flags) +{ + return syscall(__NR_landlock_restrict_self, ruleset_fd, flags); +} +#endif + +static const int permissions_table[] = { + LANDLOCK_ACCESS_FS_EXECUTE, + LANDLOCK_ACCESS_FS_WRITE_FILE, + LANDLOCK_ACCESS_FS_READ_FILE, + LANDLOCK_ACCESS_FS_READ_DIR, + LANDLOCK_ACCESS_FS_REMOVE_DIR, + LANDLOCK_ACCESS_FS_REMOVE_FILE, + LANDLOCK_ACCESS_FS_MAKE_CHAR, + LANDLOCK_ACCESS_FS_MAKE_DIR, + LANDLOCK_ACCESS_FS_MAKE_REG, + LANDLOCK_ACCESS_FS_MAKE_SOCK, + LANDLOCK_ACCESS_FS_MAKE_FIFO, + LANDLOCK_ACCESS_FS_MAKE_BLOCK, + LANDLOCK_ACCESS_FS_MAKE_SYM +}; + +static inline int +permissions_mask_val(value mask_list) +{ + int c_mask = 0; + while (mask_list != Val_emptylist) { + value head = Field(mask_list, 0); + c_mask |= permissions_table[Long_val(head)]; + mask_list = Field(mask_list, 1); + } + return c_mask; +} + +CAMLprim value +caml_landlock_init(value unit) +{ + int ruleset_fd; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_WRITE_FILE | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR | + LANDLOCK_ACCESS_FS_REMOVE_DIR | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_MAKE_CHAR | + LANDLOCK_ACCESS_FS_MAKE_DIR | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_MAKE_SOCK | + LANDLOCK_ACCESS_FS_MAKE_FIFO | + LANDLOCK_ACCESS_FS_MAKE_BLOCK | + LANDLOCK_ACCESS_FS_MAKE_SYM, + }; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) { + perror("Failed to create a ruleset"); + return -1; + } + return ruleset_fd; +} + +CAMLprim value +caml_landlock(value ruleset_fd, value *path, value perms) +{ + int err; + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = permissions_mask_val(perms) + }; + + path_beneath.parent_fd = open(String_val(path), O_PATH | O_CLOEXEC); + if (path_beneath.parent_fd < 0) { + perror("Failed to open file"); + close(ruleset_fd); + return -1; + } + + err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0); + + close(path_beneath.parent_fd); + if (err) { + perror("Failed to update ruleset"); + close(ruleset_fd); + return -1; + } + + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + perror("Failed to restrict privileges"); + close(ruleset_fd); + return -1; + } + return Val_int(ruleset_fd); +} + +CAMLprim value +caml_landlock_finish(value ruleset_fd) +{ + if (landlock_restrict_self(ruleset_fd, 0)) { + perror("Failed to enforce ruleset"); + close(ruleset_fd); + return -1; + } + close(ruleset_fd); + return 0; +} diff --git a/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/liblandlock_ocaml_stubs.a b/gemini_1.0_1_amd64/usr/local/lib/landlock_ocaml/liblandlock_ocaml_stubs.a Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.merlin-conf/lib-ogemini b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.merlin-conf/lib-ogemini Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini.cmi b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini.cmi Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini.cmo b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini.cmo Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini.cmt b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini.cmt Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini__Response.cmi b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini__Response.cmi Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini__Response.cmo b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini__Response.cmo Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini__Response.cmt b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini__Response.cmt Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini__Server.cmi b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini__Server.cmi Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini__Server.cmo b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini__Server.cmo Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini__Server.cmt b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/byte/ogemini__Server.cmt Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/native/ogemini.cmx b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/native/ogemini.cmx Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/native/ogemini__Response.cmx b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/native/ogemini__Response.cmx Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/native/ogemini__Server.cmx b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/native/ogemini__Server.cmx Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/ogemini__Response.impl.all-deps b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/ogemini__Response.impl.all-deps @@ -0,0 +1 @@ +Ogemini diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/ogemini__Server.impl.all-deps b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/ogemini__Server.impl.all-deps @@ -0,0 +1,2 @@ +Ogemini +Response diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/response.ml.d b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/response.ml.d @@ -0,0 +1 @@ +lib/ogemini/response.ml: Filename List Text Unix Uri diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/server.ml.d b/gemini_1.0_1_amd64/usr/local/lib/ogemini/.ogemini.objs/server.ml.d @@ -0,0 +1 @@ +lib/ogemini/server.ml: Config Cstruct Lwt Lwt_io Lwt_unix Printf Response Sys Text Tls Tls_lwt Unix Uri X509_lwt diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/dune b/gemini_1.0_1_amd64/usr/local/lib/ogemini/dune @@ -0,0 +1,4 @@ +(library + (name ogemini) + (libraries lwt lwt.unix tls tls.lwt text uri otoml fileutils)) +(env (dev (flags (:standard -w -33 -w -32)))) diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/ogemini.a b/gemini_1.0_1_amd64/usr/local/lib/ogemini/ogemini.a Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/ogemini.cma b/gemini_1.0_1_amd64/usr/local/lib/ogemini/ogemini.cma Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/ogemini.cmxa b/gemini_1.0_1_amd64/usr/local/lib/ogemini/ogemini.cmxa Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/ogemini.cmxs b/gemini_1.0_1_amd64/usr/local/lib/ogemini/ogemini.cmxs Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/ogemini.ml-gen b/gemini_1.0_1_amd64/usr/local/lib/ogemini/ogemini.ml-gen @@ -0,0 +1,6 @@ +(** @canonical Ogemini.Response *) +module Response = Ogemini__Response + + +(** @canonical Ogemini.Server *) +module Server = Ogemini__Server diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/response.ml b/gemini_1.0_1_amd64/usr/local/lib/ogemini/response.ml @@ -0,0 +1,62 @@ +type status = + | Input + | Sensitive_input + | Success + | Redirect_temporary + | Redirect_permanent + | Temporary_failure + | Server_unavailable + | CGI_error + | Proxy_error + | Slow_down + | Permanent_failure + | Not_found + | Gone + | Proxy_request_refused + | Bad_request + | Client_certificate_required + | Certificate_not_autorised + | Certificate_not_valid + +type mime = string +type head = status * string +type body = status * mime * in_channel + +type response = + | Head of head + | Body of body + +let int_of_status = function + | Input -> 10 + | Sensitive_input -> 11 + | Success -> 20 + | Redirect_temporary -> 30 + | Redirect_permanent -> 31 + | Temporary_failure -> 40 + | Server_unavailable -> 41 + | CGI_error -> 42 + | Proxy_error -> 43 + | Slow_down -> 44 + | Permanent_failure -> 50 + | Not_found -> 51 + | Gone -> 52 + | Proxy_request_refused -> 53 + | Bad_request -> 59 + | Client_certificate_required -> 60 + | Certificate_not_autorised -> 61 + | Certificate_not_valid -> 62 + +let handle uri = + (fun path -> + match Unix.(in_channel_of_descr @@ openfile path [ O_RDONLY; O_NONBLOCK ] 0) with + | exception _ -> Head (Not_found, "Page Not Found.") + | input_chan -> Body (Success, "text/gemini", input_chan)) + @@ Filename.concat "." + @@ Text.concat "/" + @@ List.filter (( <> ) "..") + @@ Text.split ~sep:"/" + @@ (function + | "" -> "index.gmi" + | p when Text.ends_with p "/" -> Filename.(concat p "index.gmi") + | p -> p) + @@ Uri.(pct_decode @@ path uri) diff --git a/gemini_1.0_1_amd64/usr/local/lib/ogemini/server.ml b/gemini_1.0_1_amd64/usr/local/lib/ogemini/server.ml @@ -0,0 +1,79 @@ +open Lwt +open Lwt.Syntax +open Config +open Response + +exception Bad of string + +let init_socket config = + let open Lwt_unix in + let sock = config.sock in + setsockopt sock SO_REUSEADDR true; + let addr = ADDR_INET (config.host, config.port) in + bind sock addr >|= fun () -> listen sock config.max_req + +let init_ssl config = + let+ cert = X509_lwt.private_of_pems ~cert:config.cert ~priv_key:config.key in + Tls.Config.server ~reneg:true ~certificates:(`Single cert) ~version:config.tls () + +let receive server config = Tls_lwt.Unix.accept server config.sock + +let read session = + let buf = Cstruct.create (1024 + 2) in + let+ len = Tls_lwt.Unix.read session buf in + let msg = Cstruct.to_string @@ Cstruct.sub buf 0 len in + let req = + if Text.ends_with msg "\r\n" + then ( + match Text.check @@ Uri.pct_decode @@ msg with + | None -> msg + | Some _ -> raise (Bad "Bad Request Encoding")) + else "" (* request is too long, so we send an empty one *) + in + Uri.of_string @@ Text.strip req + +let pipe fdin fdout = + let open Lwt_io in + write_lines fdout + @@ read_lines + @@ of_unix_fd ~mode:Input + @@ Unix.descr_of_in_channel fdin + +(** The status line is a single UTF-8 line formatted as: + <STATUS><SPACE><META><CR><LF> *) +let write_header session status meta = + Tls_lwt.Unix.write session + @@ Cstruct.of_string + @@ Text.encode ~encoding:"utf-8" + @@ Printf.sprintf "%d %s\r\n" (int_of_status status) meta + +let write_body session status meta fd = + write_header session status meta >>= fun () -> pipe fd (snd @@ Tls_lwt.of_t session) + +let write_response session resp = + (match resp with + | Head (status, meta) -> write_header session status meta + | Body (status, meta, fd) -> write_body session status meta fd) + >>= fun () -> Tls_lwt.Unix.close_tls session + + +let initialize config = + let* _ = init_socket config in + let* s = init_ssl config in + let _ = Sys.chdir config.dir in + Lwt.return s + +let main (config) = + let* server = initialize config in + let rec loop () = + let* session, addr = receive server config in + let a = match addr with + | Unix.ADDR_INET (a, _) -> (Unix.string_of_inet_addr a) ^ ":" ^ (string_of_int config.port) + | _ -> failwith "not INET" in + let* request = read session in + print_endline (a ^ " " ^ Uri.path request); + let resp = handle request in + let* res = write_response session resp in + loop res + in + loop () diff --git a/gemini_1.0_1_amd64/usr/local/lib/seccomp/.merlin-conf/lib-seccomp b/gemini_1.0_1_amd64/usr/local/lib/seccomp/.merlin-conf/lib-seccomp Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/seccomp/.seccomp.objs/byte/seccomp.cmi b/gemini_1.0_1_amd64/usr/local/lib/seccomp/.seccomp.objs/byte/seccomp.cmi Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/seccomp/.seccomp.objs/byte/seccomp.cmo b/gemini_1.0_1_amd64/usr/local/lib/seccomp/.seccomp.objs/byte/seccomp.cmo Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/seccomp/.seccomp.objs/byte/seccomp.cmt b/gemini_1.0_1_amd64/usr/local/lib/seccomp/.seccomp.objs/byte/seccomp.cmt Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/seccomp/.seccomp.objs/native/seccomp.cmx b/gemini_1.0_1_amd64/usr/local/lib/seccomp/.seccomp.objs/native/seccomp.cmx Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/seccomp/dllseccomp_stubs.so b/gemini_1.0_1_amd64/usr/local/lib/seccomp/dllseccomp_stubs.so Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/seccomp/libseccomp_stubs.a b/gemini_1.0_1_amd64/usr/local/lib/seccomp/libseccomp_stubs.a Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/seccomp/seccomp.a b/gemini_1.0_1_amd64/usr/local/lib/seccomp/seccomp.a Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/seccomp/seccomp.cmxa b/gemini_1.0_1_amd64/usr/local/lib/seccomp/seccomp.cmxa Binary files differ. diff --git a/gemini_1.0_1_amd64/usr/local/lib/seccomp/seccomp.ml b/gemini_1.0_1_amd64/usr/local/lib/seccomp/seccomp.ml @@ -0,0 +1 @@ +external seccomp: unit -> int = "caml_seccomp" diff --git a/gemini_1.0_1_amd64/usr/local/lib/seccomp/seccomp_stubs.c b/gemini_1.0_1_amd64/usr/local/lib/seccomp/seccomp_stubs.c @@ -0,0 +1,66 @@ +#include <seccomp.h> +#include <unistd.h> +#include <stdio.h> +#include <errno.h> +#include <fcntl.h> +#include <caml/mlvalues.h> + +CAMLprim value +caml_seccomp(value unit) +{ + scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); + + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(accept4), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(access), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(arch_prctl), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(bind), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(chdir), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(clone), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(connect), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(eventfd2), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(execve), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fcntl), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(futex), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getcwd), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getrandom), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getsockopt), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(landlock_add_rule), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(landlock_create_ruleset), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(landlock_restrict_self), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(listen), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(lseek), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mprotect), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(munmap), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(newfstatat), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(poll), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(prctl), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(pread64), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(prlimit64), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(pselect6), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(readlink), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rseq), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigaction), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigprocmask), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(set_robust_list), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(set_tid_address), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(setgid), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(setsockopt), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(setuid), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(sigaltstack), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(socket), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0); + + if (seccomp_load(ctx) < 0) { + fprintf(stderr, "seccomp_load failed\n"); + return -1; + } + + seccomp_release(ctx); + + return 0; +} diff --git a/lib/config/config.ml b/lib/config/config.ml @@ -0,0 +1,72 @@ +open Otoml + +type srv = + { mutable key : string + ; mutable cert : string + ; mutable dir : string + ; mutable logfile : string + ; mutable port : int + ; mutable max_req : int + ; mutable host : Unix.inet_addr + ; sock : Lwt_unix.file_descr + ; tls : Tls.Core.tls_version * Tls.Core.tls_version + ; mutable owner : string + } + +let config : srv = + { key = Filename.concat (Sys.getcwd ()) "certs/key.pem" + ; cert = Filename.concat (Sys.getcwd ()) "certs/cert.pem" + ; port = 1965 + ; dir = "./" + ; logfile = "/tmp/ogemini.log" + ; host = Unix.inet_addr_loopback + ; sock = Lwt_unix.(socket PF_INET SOCK_STREAM 0) + ; max_req = 50 + ; tls = Tls.Core.(`TLS_1_2, `TLS_1_3) + ; owner = "user" + } + +let parse filename = + let conf = Sys.file_exists filename in + match conf with + | false -> + Logs.warn + @@ fun m -> m "Configuration file %s not found, using default settings" filename + | true -> + (try + let toml = Otoml.Parser.from_file filename in + let cfg = ref config in + (match Otoml.find toml Otoml.get_string [ "servers"; "dir" ] with + | "" -> Logs.warn @@ fun m -> m "[servers] dir config is empty. Defaults to './'" + | dir -> !cfg.dir <- dir); + (match Otoml.find toml Otoml.get_string [ "servers"; "logfile" ] with + | "" -> Logs.warn @@ fun m -> m "[servers] logfile config is empty. Defaults to '/tmp/ogemini.log'" + | logfile -> !cfg.logfile <- logfile); + (match Otoml.find toml Otoml.get_string [ "servers"; "cert" ] with + | "" -> Logs.warn @@ fun m -> m "[servers] cert config is empty" + | cert -> !cfg.cert <- cert); + (match Otoml.find toml Otoml.get_string [ "servers"; "key" ] with + | "" -> Logs.warn @@ fun m -> m "[servers] key config is empty" + | key -> !cfg.key <- key); + (match Otoml.find toml Otoml.get_string [ "servers"; "host" ] with + | "" -> + Logs.warn @@ fun m -> m "[servers] host config is empty. Defaults to localhost" + | host -> !cfg.host <- Unix.inet_addr_of_string host); + (match Otoml.find toml (Otoml.get_integer ~strict:false) [ "servers"; "port" ] with + | port -> !cfg.port <- port); + (match Otoml.find toml (Otoml.get_string ~strict:false) [ "servers"; "owner" ] with + | "" -> Logs.warn @@ fun m -> m "[servers] owner config is empty" + | owner -> !cfg.owner <- owner); + match Otoml.find toml Otoml.get_integer [ "servers"; "max_req" ] with + | max_req -> !cfg.max_req <- max_req + with + | Sys_error err -> failwith + (Printf.sprintf + "Could not read config file: %s" + err) + | Otoml.Parse_error (pos, msg) -> + failwith + (Printf.sprintf + "Could not parse config file %s: %s" + filename + (Otoml.Parser.format_parse_error pos msg))) diff --git a/lib/config/dune b/lib/config/dune @@ -0,0 +1,4 @@ +(library + (name config) + (libraries otoml tls lwt lwt.unix)) +(env (dev (flags (:standard -w -33 -w -32)))) diff --git a/lib/daemonize/daemonize.ml b/lib/daemonize/daemonize.ml @@ -0,0 +1,31 @@ +open Log + +let log_stop_signals signals pid = + List.map (fun s -> + Sys.set_signal s (Signal_handle (fun _ -> + log_msg ("DAEMON STOP pid: " ^ string_of_int pid); + exit 0; + ))) signals + +(* Set logfile to an empty string to enforce a no-log policy *) +let daemonize logfile = + if Lwt_unix.fork () <> 0 then + exit 0; + + ignore (Unix.umask (022)); + ignore (Unix.setsid ()); + + Unix.close Unix.stdout; + Unix.close Unix.stderr; + + if logfile <> "" then + (* Redirect stdout and stderr channels to logfile *) + let oc = open_out_gen [Open_append; Open_creat; Open_wronly] 0o600 logfile in + Unix.dup2 (Unix.descr_of_out_channel oc) Unix.stdout; + Unix.dup2 (Unix.descr_of_out_channel oc) Unix.stderr; + + let pid = (Unix.getpid ()) in + log_msg ("DAEMON START pid: " ^ string_of_int pid); + flush stdout; + + ignore (log_stop_signals [Sys.sigterm ; Sys.sigint] pid); diff --git a/lib/daemonize/dune b/lib/daemonize/dune @@ -0,0 +1,3 @@ +(library + (name daemonize) + (libraries lwt.unix unix)) diff --git a/lib/daemonize/log.ml b/lib/daemonize/log.ml @@ -0,0 +1,10 @@ +let datetime = + let date = Unix.localtime (Unix.time ()) in + Printf.sprintf + "%d/%d/%d %d:%d" (* YYYY/MM/DD H:M *) + (date.tm_year + 1900) (date.tm_mon + 1) + date.tm_mday date.tm_hour date.tm_min + +let log_msg msg = + Printf.printf "%s %s\n" datetime msg; + flush stdout diff --git a/lib/landlock_ocaml/dune b/lib/landlock_ocaml/dune @@ -0,0 +1,3 @@ +(library + (name landlock_ocaml) + (foreign_stubs (language c) (names landlock_stubs))) diff --git a/lib/landlock_ocaml/landlock.ml b/lib/landlock_ocaml/landlock.ml @@ -0,0 +1,20 @@ +type permissions = + | LANDLOCK_ACCESS_FS_EXECUTE + | LANDLOCK_ACCESS_FS_WRITE_FILE + | LANDLOCK_ACCESS_FS_READ_FILE + | LANDLOCK_ACCESS_FS_READ_DIR + | LANDLOCK_ACCESS_FS_REMOVE_DIR + | LANDLOCK_ACCESS_FS_REMOVE_FILE + | LANDLOCK_ACCESS_FS_MAKE_CHAR + | LANDLOCK_ACCESS_FS_MAKE_DIR + | LANDLOCK_ACCESS_FS_MAKE_REG + | LANDLOCK_ACCESS_FS_MAKE_SOCK + | LANDLOCK_ACCESS_FS_MAKE_FIFO + | LANDLOCK_ACCESS_FS_MAKE_BLOCK + | LANDLOCK_ACCESS_FS_MAKE_SYM + +type permissions_mask = permissions list + +external landlock_init: unit -> int = "caml_landlock_init" +external landlock_new_rule: int -> string -> permissions_mask -> int = "caml_landlock" +external landlock_finish: int -> int = "caml_landlock_finish" diff --git a/lib/landlock_ocaml/landlock_stubs.c b/lib/landlock_ocaml/landlock_stubs.c @@ -0,0 +1,139 @@ +#define _GNU_SOURCE +#include <fcntl.h> +#include <linux/landlock.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/prctl.h> +#include <sys/syscall.h> +#include <unistd.h> +#include <caml/mlvalues.h> + +#ifndef landlock_create_ruleset +static inline int +landlock_create_ruleset(const struct landlock_ruleset_attr *const attr, + const size_t size, const __u32 flags) +{ + return syscall(__NR_landlock_create_ruleset, attr, size, flags); +} +#endif + +#ifndef landlock_add_rule +static inline int landlock_add_rule(const int ruleset_fd, + const enum landlock_rule_type rule_type, + const void *const rule_attr, + const __u32 flags) +{ + return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, + flags); +} +#endif + +#ifndef landlock_restrict_self +static inline int landlock_restrict_self(const int ruleset_fd, + const __u32 flags) +{ + return syscall(__NR_landlock_restrict_self, ruleset_fd, flags); +} +#endif + +static const int permissions_table[] = { + LANDLOCK_ACCESS_FS_EXECUTE, + LANDLOCK_ACCESS_FS_WRITE_FILE, + LANDLOCK_ACCESS_FS_READ_FILE, + LANDLOCK_ACCESS_FS_READ_DIR, + LANDLOCK_ACCESS_FS_REMOVE_DIR, + LANDLOCK_ACCESS_FS_REMOVE_FILE, + LANDLOCK_ACCESS_FS_MAKE_CHAR, + LANDLOCK_ACCESS_FS_MAKE_DIR, + LANDLOCK_ACCESS_FS_MAKE_REG, + LANDLOCK_ACCESS_FS_MAKE_SOCK, + LANDLOCK_ACCESS_FS_MAKE_FIFO, + LANDLOCK_ACCESS_FS_MAKE_BLOCK, + LANDLOCK_ACCESS_FS_MAKE_SYM +}; + +static inline int +permissions_mask_val(value mask_list) +{ + int c_mask = 0; + while (mask_list != Val_emptylist) { + value head = Field(mask_list, 0); + c_mask |= permissions_table[Long_val(head)]; + mask_list = Field(mask_list, 1); + } + return c_mask; +} + +CAMLprim value +caml_landlock_init(value unit) +{ + int ruleset_fd; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_WRITE_FILE | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR | + LANDLOCK_ACCESS_FS_REMOVE_DIR | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_MAKE_CHAR | + LANDLOCK_ACCESS_FS_MAKE_DIR | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_MAKE_SOCK | + LANDLOCK_ACCESS_FS_MAKE_FIFO | + LANDLOCK_ACCESS_FS_MAKE_BLOCK | + LANDLOCK_ACCESS_FS_MAKE_SYM, + }; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) { + perror("Failed to create a ruleset"); + return -1; + } + return ruleset_fd; +} + +CAMLprim value +caml_landlock(value ruleset_fd, value *path, value perms) +{ + int err; + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = permissions_mask_val(perms) + }; + + path_beneath.parent_fd = open(String_val(path), O_PATH | O_CLOEXEC); + if (path_beneath.parent_fd < 0) { + perror("Failed to open file"); + close(ruleset_fd); + return -1; + } + + err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0); + + close(path_beneath.parent_fd); + if (err) { + perror("Failed to update ruleset"); + close(ruleset_fd); + return -1; + } + + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + perror("Failed to restrict privileges"); + close(ruleset_fd); + return -1; + } + return Val_int(ruleset_fd); +} + +CAMLprim value +caml_landlock_finish(value ruleset_fd) +{ + if (landlock_restrict_self(ruleset_fd, 0)) { + perror("Failed to enforce ruleset"); + close(ruleset_fd); + return -1; + } + close(ruleset_fd); + return 0; +} diff --git a/lib/ogemini/dune b/lib/ogemini/dune @@ -0,0 +1,3 @@ +(library + (name ogemini) + (libraries lwt lwt.unix tls tls.lwt text uri config)) diff --git a/lib/ogemini/response.ml b/lib/ogemini/response.ml @@ -0,0 +1,62 @@ +type status = + | Input + | Sensitive_input + | Success + | Redirect_temporary + | Redirect_permanent + | Temporary_failure + | Server_unavailable + | CGI_error + | Proxy_error + | Slow_down + | Permanent_failure + | Not_found + | Gone + | Proxy_request_refused + | Bad_request + | Client_certificate_required + | Certificate_not_autorised + | Certificate_not_valid + +type mime = string +type head = status * string +type body = status * mime * in_channel + +type response = + | Head of head + | Body of body + +let int_of_status = function + | Input -> 10 + | Sensitive_input -> 11 + | Success -> 20 + | Redirect_temporary -> 30 + | Redirect_permanent -> 31 + | Temporary_failure -> 40 + | Server_unavailable -> 41 + | CGI_error -> 42 + | Proxy_error -> 43 + | Slow_down -> 44 + | Permanent_failure -> 50 + | Not_found -> 51 + | Gone -> 52 + | Proxy_request_refused -> 53 + | Bad_request -> 59 + | Client_certificate_required -> 60 + | Certificate_not_autorised -> 61 + | Certificate_not_valid -> 62 + +let handle uri = + (fun path -> + match Unix.(in_channel_of_descr @@ openfile path [ O_RDONLY; O_NONBLOCK ] 0) with + | exception _ -> Head (Not_found, "Page Not Found.") + | input_chan -> Body (Success, "text/gemini", input_chan)) + @@ Filename.concat "." + @@ Text.concat "/" + @@ List.filter (( <> ) "..") + @@ Text.split ~sep:"/" + @@ (function + | "" -> "index.gmi" + | p when Text.ends_with p "/" -> Filename.(concat p "index.gmi") + | p -> p) + @@ Uri.(pct_decode @@ path uri) diff --git a/lib/ogemini/server.ml b/lib/ogemini/server.ml @@ -0,0 +1,79 @@ +open Lwt +open Lwt.Syntax +open Config +open Response + +exception Bad of string + +let init_socket config = + let open Lwt_unix in + let sock = config.sock in + setsockopt sock SO_REUSEADDR true; + let addr = ADDR_INET (config.host, config.port) in + bind sock addr >|= fun () -> listen sock config.max_req + +let init_ssl config = + let+ cert = X509_lwt.private_of_pems ~cert:config.cert ~priv_key:config.key in + Tls.Config.server ~reneg:true ~certificates:(`Single cert) ~version:config.tls () + +let receive server config = Tls_lwt.Unix.accept server config.sock + +let read session = + let buf = Cstruct.create (1024 + 2) in + let+ len = Tls_lwt.Unix.read session buf in + let msg = Cstruct.to_string @@ Cstruct.sub buf 0 len in + let req = + if Text.ends_with msg "\r\n" + then ( + match Text.check @@ Uri.pct_decode @@ msg with + | None -> msg + | Some _ -> raise (Bad "Bad Request Encoding")) + else "" (* request is too long, so we send an empty one *) + in + Uri.of_string @@ Text.strip req + +let pipe fdin fdout = + let open Lwt_io in + write_lines fdout + @@ read_lines + @@ of_unix_fd ~mode:Input + @@ Unix.descr_of_in_channel fdin + +(** The status line is a single UTF-8 line formatted as: + <STATUS><SPACE><META><CR><LF> *) +let write_header session status meta = + Tls_lwt.Unix.write session + @@ Cstruct.of_string + @@ Text.encode ~encoding:"utf-8" + @@ Printf.sprintf "%d %s\r\n" (int_of_status status) meta + +let write_body session status meta fd = + write_header session status meta >>= fun () -> pipe fd (snd @@ Tls_lwt.of_t session) + +let write_response session resp = + (match resp with + | Head (status, meta) -> write_header session status meta + | Body (status, meta, fd) -> write_body session status meta fd) + >>= fun () -> Tls_lwt.Unix.close_tls session + + +let initialize config = + let* _ = init_socket config in + let* s = init_ssl config in + let _ = Sys.chdir config.dir in + Lwt.return s + +let main (config) = + let* server = initialize config in + let rec loop () = + let* session, addr = receive server config in + let a = match addr with + | Unix.ADDR_INET (a, _) -> (Unix.string_of_inet_addr a) ^ ":" ^ (string_of_int config.port) + | _ -> failwith "not INET" in + let* request = read session in + print_endline (a ^ " " ^ Uri.path request); + let resp = handle request in + let* res = write_response session resp in + loop res + in + loop () diff --git a/lib/seccomp/dune b/lib/seccomp/dune @@ -0,0 +1,4 @@ +(library + (name seccomp) + (foreign_stubs (language c) (names seccomp_stubs)) + (c_library_flags (-lseccomp))) diff --git a/lib/seccomp/seccomp.ml b/lib/seccomp/seccomp.ml @@ -0,0 +1 @@ +external seccomp: unit -> int = "caml_seccomp" diff --git a/lib/seccomp/seccomp_stubs.c b/lib/seccomp/seccomp_stubs.c @@ -0,0 +1,66 @@ +#include <seccomp.h> +#include <unistd.h> +#include <stdio.h> +#include <errno.h> +#include <fcntl.h> +#include <caml/mlvalues.h> + +CAMLprim value +caml_seccomp(value unit) +{ + scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); + + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(accept4), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(access), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(arch_prctl), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(bind), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(chdir), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(clone), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(connect), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(eventfd2), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(execve), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fcntl), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(futex), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getcwd), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getrandom), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getsockopt), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(landlock_add_rule), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(landlock_create_ruleset), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(landlock_restrict_self), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(listen), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(lseek), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mprotect), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(munmap), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(newfstatat), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(poll), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(prctl), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(pread64), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(prlimit64), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(pselect6), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(readlink), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rseq), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigaction), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigprocmask), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(set_robust_list), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(set_tid_address), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(setgid), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(setsockopt), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(setuid), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(sigaltstack), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(socket), 0); + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0); + + if (seccomp_load(ctx) < 0) { + fprintf(stderr, "seccomp_load failed\n"); + return -1; + } + + seccomp_release(ctx); + + return 0; +} diff --git a/static/index.gmi b/static/index.gmi @@ -0,0 +1 @@ +Hello World! diff --git a/test/dune b/test/dune @@ -0,0 +1,2 @@ +(test + (name gemini)) diff --git a/test/gemini.ml b/test/gemini.ml