package simple_httpd

  1. Overview
  2. Docs
Simple HTTP server using ocaml 5 domains

Install

dune-project
 Dependency

Authors

Maintainers

Sources

1.0.1.tar.gz
md5=92fe48592979d21002e66416c21bf398
sha512=88f090ce45b81cf244f248670608ff71c0db206da5de859c5e784e4fc59698acb329218bfffc81f4ff9796ae465c23e1faf08aaee804cb22dfe50cea757a854d

doc/index.html

Simple_httpd library to write web server and application

Introduction

This library implements a HTTP/1.1 server for Linux using domains and algebraic effects. It uses epoll and eventfd to schedule the treatment of clients very efficiently, with one domain used to accept requests and several domains to handle these requests. Simple_httpd can listen to several addresses and ports and use this information together with the headers Host field to decide how to answer the request.

It is fast and can handle several thousands of simultaneous connections. It may use memory cache, sendfile system call and other TCP options to reach an efficiency comparable or better than nginx on static files.

Through a bunch of modules, described here Simple_httpd, we provide:

  • Routing, by address, port, host and url path.
  • Handling of static files possibly using a memory cache or a virtual file system. More precisely, it can serves a directory dynamically (hence reflecting changes has they occur on the file system) or use vfs_pack to compile a static folder (In this can changed nee recompilation and restarts), see below.
  • Basic management of session and cookies. Simple_httpd sessions can for instance keep an open connection to a database server. Session are saved and restored on server restart and the Simple_httpd.Auth module provide basic authentication. Its functorial interface allow for more complex authentication like CAS.
  • Support for ssl using ocaml-ssl. This includes using ktls if your linux kernel supports it.
  • A Simple_httpd.Host module dedicated to write single server handling several sites or applications.
  • A web site compiler vfs_pack, the will cache small files in memory and bigger files in a separate store. This allows not to store the source of your websites on the server. Files in store or memory may be pre-compressed with zip (a.k.a. deflate, gzip is planned).
  • vfs_pack includes an equivalent of php files named chaml that are compiled to OCaml, but much faster than php (and also safer). It will also parse html files allowing to detect error and minimizing them. Parsing and dynamic css are planned.
  • It is fast: Here are some curves showing the performance on static files compared to nginx and apache. Our 99% quantile is very good, meaning that Simple_httpd handles very quickly most request.

Benchmarks

Here is a plot of the latency obtained using vegeta with 1000 requests per seconds for 15s, on a small static file. Simple_httpd uses Simple_httpd.Dir.add_vfs using vfs_pack from the example/echo.ml file shown below:

Latencies for static files

Here is the same graphic for a small chaml/php dynamic file.

Latencies for dynamic files

Here are some other graphs showing the maximum number of requests possible, using h2load

comparison for small files

comparison for large files

apache and nginx are the usual servers with php-fpm and their default configuration on debian 13 except for accepting more connections than the default. There is a (reproducible) problem with nginx and php without ssl with a few extreme value that impact the mean, and also some errors in rare cases. Comparatively, simple_httpd serves all requests.

Here is a similar graph showing the difference between .php and .chaml on a similar file. It shows that simple_httpd can serve at least than 5 times more requests.

comparison chaml versus php

Quick start

A good start is the template folder of the distribution, documented here. It contains the skeleton for a server serving two sites with status report and statistics.

See also examples/echo.ml below, that demonstrates some of the features by declaring a few endpoints, including one for uploading files and a virtual file system.

To go further, you should start reading Simple_httpd the main module of the library and look at template that is provided.

(* echo.ml: a fairly complete example *)
open Simple_httpd
open Response_code
module H = Headers

(** Parse command line options *)

(** Default address, port, ssl configuration and folder for the site *)
let addr = ref "127.0.0.1"
let port = ref 8080
let top_dir = ref "/tmp" (* The folder where vfs_pack installed the file *)
let ssl_cert = ref ""
let ssl_priv = ref ""
let ktls = ref false

(** Server.args provides a bunch and standard option to control the
    maximum number of connections, logs, etc... *)
let args, parameters = Server.args ()

let _ =
  Arg.parse (Arg.align ([
      "--addr", Arg.Set_string addr, " set address";
      "-a", Arg.Set_string addr, " set address";
      "--port", Arg.Set_int port, " set port";
      "-p", Arg.Set_int port, " set port";
      "--dir", Arg.Set_string top_dir, " set the top dir for file path";
      "--ssl", Tuple[Set_string ssl_cert; Set_string ssl_priv], " give ssl certificate and private key";
      "--ssl-ktls", Set ktls, " add support for kernel TLS";
    ] @ args)) (fun _ -> raise (Arg.Bad "")) "echo [option]*"

(** We configure ssl *)
let ssl =
  if !ssl_cert <> "" then
    Some Address.{ cert = !ssl_cert; priv = !ssl_priv;
                   protocol = Ssl.TLSv1_3; ktls = !ktls }
  else None

(** Server initialisation *)
let listens = [Address.make ~addr:!addr ~port:!port ?ssl ()]
let server = Server.create parameters ~listens

(** Compose the above filter with the compression filter
    provided by [Simple_httpd.Camlzip], than will compress output
    when [deflate] is accepted *)
let filter_zip =
  Camlzip.filter ~compress_above:1024 ~buf_size:(16*1024) ()

(** Compose with the stat filter that provided minimum statistics *)
let filter, get_stats =
  let filter_stat, get_stats = Stats.filter () in
  (Filter.compose_cross filter_zip filter_stat, get_stats)

(** Add a route answering 'Hello world' to [http://localhost/hello/world] *)
let _ =
  Server.add_route_handler ~meth:GET server ~filter
    Route.(exact "hello" @/ string @/ return)
    (fun name _req -> Response.make_string (Printf.sprintf "hello %s" name))

(** Add an echo request *)
let _ =
  Server.add_route_handler server ~filter
    Route.(exact "echo" @/ return)
    (fun req ->
      let q =
        Request.query req |> List.map (fun (k,v) -> Printf.sprintf "%S = %S" k v)
        |> String.concat ";"
      in
      Response.make_string
        (Format.asprintf "echo:@ %a@ (query: %s)@." Request.pp req q))

(** Add file upload *)
let _ =
  Server.add_route_handler_stream ~meth:PUT server ~filter
    Route.(exact "upload" @/ string @/ return)
    (fun path req ->
        Log.f (Req 0) (fun k->k "start upload %S, headers:\n%s\n\n%!" path
                     (Format.asprintf "%a" Headers.pp (Request.headers req)));
        try
          let oc = open_out @@ Filename.concat !top_dir path in
          Input.to_chan oc (Request.body req);
          flush oc;
          Response.make_string "uploaded file"
        with e ->
          Response.fail ~code:internal_server_error
            "couldn't upload file: %s" (Printexc.to_string e)
      )

(** Access to the statistics (the page can be tuned) *)
let _ =
  Server.add_route_handler_chaml server ~filter:filter_zip
    Route.(exact "stats" @/ return) get_stats

(** Access to the status of the server (the page can be tuned)  *)
let _ =
  Server.add_route_handler_chaml server
    ~filter:filter_zip Route.(exact "status" @/ return) (Status.html server)

(** Add a virtual file system VFS, produced by [simple-httpd-vfs-pack] from
    an actual folder *)
let _ =
  let vfs = Vfs.make ~top_dir:!top_dir () in
  Dir.add_vfs server
    ~config:(Dir.config ~download:true
               ~dir_behavior:Dir.Index_or_lists ())
    ~vfs:vfs
    ~prefix:"vfs" (* url is vfs/filename *)

(** Run a shell command (VERY UNSAFE!) *)
let _ =
  Server.add_route_handler ~meth:GET server ~filter
    Route.(exact "shell" @/ string @/ return)
    (fun cmd req ->
      let args = List.fold_left (fun acc (k,v) ->
                     let acc = if k <> "" then k::acc else acc in
                     let acc = if v <> "" then v::acc else acc in
                     acc) [cmd] (Request.query req)
      in
      let client = Request.client req in
      let args = Array.of_list (List.rev args) in
      let (_, io) = Process.create ~client cmd args in
      Response.make_stream (Input.of_io io);
    )

(** Add a directory, this is dynamic and changes are reflected immediately *)
let _ =
  Dir.add_dir_path server
    ~config:(Dir.config ~download:true
               ~dir_behavior:Dir.Index_or_lists ())
    ~prefix:"dir" ~dir:"../examples/files"

(** Add a route sending a compressed stream for the given file in the current
    directory, this example shows how to use Unix command within a request.
    Note that this example is unsafe, some stronger check should be performed on
    the filename *)
let _ =
  Server.add_route_handler ~meth:GET server ~filter
    Route.(exact "zcat" @/ string @/ return)
    (fun path _req ->
        if not (Filename.is_implicit path) then
	  Response.fail_raise ~code:Response_code.unauthorized "Unauthorized";
        let ic = open_in path in
        let str = Input.of_chan ic in
        let mime_type =
          try
            let p = Unix.open_process_in (Printf.sprintf "file -i -b %S" path) in
            try
              let s = [H.Content_Type, String.trim (input_line p)] in
              ignore @@ Unix.close_process_in p;
              s
            with _ -> ignore @@ Unix.close_process_in p; []
          with _ -> []
        in
        Response.make_stream ~headers:mime_type str
      )

(** Add an interactive terminal page using websocket! Must be protected by
    authentication and needs and html page to host it and xterm. *)
let _ =
  Server.add_route_handler ~meth:GET server
    Route.(exact "shell" @/ return) WebSocket.terminal_handler

(** Main page using the Html module (deprecated by vfs_pack and many other
    solutions, but still useful in some cases.*)
let _ =
  Server.add_route_handler_chaml server ~filter Route.return
    {chaml|
     <!DOCTYPE html>
     <html>
       <head>
         <title>index of echo</title>
       </head>
       <body>
	 <h3>welcome</h3>
	 <p><b>endpoints are</b></p>
	 <ul>
	   <li><pre>/ (GET)</pre> this file!</li>
           <li><pre>/hello/:name (GET)</pre> echo message</li>
           <li><pre><a href="/echo">echo</a></pre> echo back query</li>
           <li><pre>/upload/:path (PUT)</pre> to upload a file</li>
           <li><pre>/zcat/:path (GET)</pre> to download a file (deflate transfer-encoding)</li>
           <li><pre><a href="/stats/">/stats (GET)</a></pre> to access statistics</li>
           <li><pre><a href="/status/">/status (GET)</a></pre> to get server status</li>
           <li><pre><a href="/vfs/">/vfs (GET)</a></pre> to access a VFS
             embedded in the binary</li>
           <li><pre><a href="/vfs/shell.html">/vfs/shell.html (GET)</a></pre> to access a shell
             using web socket and xterm.js</li>
	 </ul>
       </body>
     </html>|chaml}

(** Start the server *)
let _ =
  Server.run server