initial commit

This commit is contained in:
Simon Cruanes 2019-11-13 23:38:38 -06:00
commit 3703436d18
12 changed files with 475 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.git
_build
*.install
.merlin

16
.travis.yml Normal file
View file

@ -0,0 +1,16 @@
language: c
install: wget https://raw.githubusercontent.com/ocaml/ocaml-ci-scripts/master/.travis-docker.sh
script: bash -ex .travis-docker.sh
services:
- docker
env:
global:
- PINS="simplehttpserver"
- DISTRO="ubuntu-16.04"
- PACKAGE="simplehttpserver"
matrix:
- OCAML_VERSION="4.03"
- OCAML_VERSION="4.04"
- OCAML_VERSION="4.06"
- OCAML_VERSION="4.07"
- OCAML_VERSION="4.09"

20
Makefile Normal file
View file

@ -0,0 +1,20 @@
all: build test
build:
@dune build @install
test:
@dune runtest --no-buffer --force
clean:
@dune clean
doc:
@dune build @doc
watch:
@dune build @all -w
.PHONY: benchs tests build watch

62
README.md Normal file
View file

@ -0,0 +1,62 @@
# SimpleHTTPServer [![build status](https://travis-ci.org/c-cube/simplehttpserver.svg?branch=master)](https://travis-ci.org/c-cube/simplehttpserver)
Minimal HTTP server using good old threads and `Scanf` for routing.
The basic echo server from `src/examples/echo.ml`:
```ocaml
module S = SimpleHTTPServer
let () =
let server = S.create () in
(* say hello *)
S.add_path_handler ~meth:`GET server
"/hello/%s@/" (fun _req name () -> S.Response.make_ok ("hello " ^name ^"!\n"));
(* echo request *)
S.add_path_handler server
"/echo" (fun req () -> S.Response.make_ok (Format.asprintf "echo:@ %a@." S.Request.pp req));
Printf.printf "listening on http://%s:%d\n%!" (S.addr server) (S.port server);
match S.run server with
| Ok () -> ()
| Error e -> raise e
```
```sh
$ ./echo.sh &
listening on http://127.0.0.1:8080
# the path "hello/name" greets you.
$ curl -X GET http://localhost:8080/hello/quadrarotaphile
hello quadrarotaphile!
# the path "echo" just prints the request.
$ curl -X GET http://localhost:8080/echo --data "coucou lol"
echo:
{meth=GET;
headers=Host: localhost:8080
User-Agent: curl/7.66.0
Accept: */*
Content-Length: 10
Content-Type: application/x-www-form-urlencoded;
path="/echo"; body="coucou lol"}
```
## Why?
Why not? If you just want a super basic local server (perhaps for exposing
data from a local demon, like Cups or Syncthing do), no need for a ton of
dependencies or high scalability libraries.
## Documentation
See https://c-cube.github.io/simplehttpserver/
## License
MIT.

1
dune-project Normal file
View file

@ -0,0 +1 @@
(lang dune 1.1)

2
echo.sh Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
exec dune exec "src/examples/echo.exe" -- $@

23
simplehttpserver.opam Normal file
View file

@ -0,0 +1,23 @@
opam-version: "2.0"
name: "simplehttpserver"
version: "0.1"
authors: ["Simon Cruanes"]
maintainer: "simon.cruanes.2007@m4x.org"
license: "MIT"
description: "Minimal HTTP server using good old threads"
build: [
["dune" "build" "@install" "-p" name "-j" jobs]
["dune" "build" "@doc" "-p" name] {with-doc}
["dune" "runtest" "-p" name] {with-test}
]
depends: [
"dune" { >= "1.1" }
"base-threads"
"ocaml" { >= "4.03.0" }
"odoc" {with-doc}
]
tags: [ "http" "thread" "server" ]
homepage: "https://github.com/c-cube/simplehttpserver/"
doc: "https://c-cube.github.io/simplehttpserver/"
bug-reports: "https://github.com/c-cube/simplehttpserver/issues"
dev-repo: "git+https://github.com/c-cube/simplehttpserver.git"

234
src/SimpleHTTPServer.ml Normal file
View file

@ -0,0 +1,234 @@
exception Bad_req of int * string
let bad_reqf c fmt = Printf.ksprintf (fun s ->raise (Bad_req (c,s))) fmt
module Meth = struct
type t = [
| `GET
| `PUT
| `POST
| `HEAD
]
let to_string = function
| `GET -> "GET"
| `PUT -> "PUT"
| `HEAD -> "HEAD"
| `POST -> "POST"
let pp out s = Format.pp_print_string out (to_string s)
let of_string = function
| "GET" -> `GET
| "PUT" -> `PUT
| "POST" -> `POST
| "HEAD" -> `HEAD
| s -> bad_reqf 400 "unknown method %S" s
end
module Headers = struct
type t = (string * string) list
let pp out l =
let pp_pair out (k,v) = Format.fprintf out "@[<h>%s: %s@]" k v in
Format.fprintf out "@[<v>%a@]" (Format.pp_print_list pp_pair) l
let parse_ (ic:in_channel) : t =
let rec loop acc =
let line = input_line ic in
if line = "\r" then (
List.rev acc
) else (
let k,v =
try Scanf.sscanf line "%s@: %s@\r" (fun k v->k,v)
with _ -> bad_reqf 400 "invalid header line: %S" line
in
loop ((k,v)::acc)
)
in
loop []
end
module Response_code = struct
type t = int
let descr = function
| 200 -> "OK"
| 400 -> "Bad request"
| 403 -> "Forbidden"
| 404 -> "Not found"
| 500 -> "Internal server error"
| 503 -> "Service unavailable"
| _ -> "Unknown response" (* TODO *)
end
module Request = struct
type t = {
meth: Meth.t;
headers: Headers.t;
path: string;
body: string
}
let headers self = self.headers
let meth self = self.meth
let path self = self.path
let body self = self.body
let pp out self : unit =
Format.fprintf out "{@[meth=%s;@ headers=%a;@ path=%S;@ body=%S@]}"
(Meth.to_string self.meth) Headers.pp self.headers
self.path self.body
let read_body ic (n:int) : string =
let buf = Bytes.make n ' ' in
let i = ref 0 in
while !i < n do
let read = input ic buf !i (n- !i) in
if read=0 then bad_reqf 400 "body is too short";
i := !i + read
done;
Bytes.unsafe_to_string buf
let parse_ (ic:in_channel) : (t option, Response_code.t * string) result =
try
let line = input_line ic in
let meth, path =
try Scanf.sscanf line "%s %s HTTP/1.1\r" (fun x y->x,y)
with _ -> raise (Bad_req (400, "Invalid request line"))
in
let meth = Meth.of_string meth in
let headers = Headers.parse_ ic in
let body = match List.assoc "Content-Length" headers |> int_of_string with
| exception Not_found -> ""
| exception _ -> bad_reqf 400 "invalid content-length"
| 0 -> ""
| n -> read_body ic n
in
Ok (Some {meth; path; body; headers})
with
| End_of_file -> Ok None
| Bad_req (c,s) -> Error (c,s)
| e ->
Error (400, Printexc.to_string e)
end
module Response = struct
type t = {
code: Response_code.t;
headers: Headers.t;
body: string;
}
let make ?(headers=[]) ~code body : t =
(* add 'content length' to response *)
let headers = List.filter (fun (k,_) -> k <> "Content-Length") headers in
let headers = ("Content-Length", string_of_int (String.length body)) :: headers in
{ code; headers; body; }
let make_ok ?headers body = make ~code:200 ?headers body
let make_not_found ?headers body = make ~code:404 ?headers body
let make_error ?headers body = make ~code:500 ?headers body
let pp out self : unit =
Format.fprintf out "{@[code=%d;@ headers=%a;@ body=%S@]}"
self.code Headers.pp self.headers self.body
let output_ (oc:out_channel) (self:t) : unit =
Printf.fprintf oc "HTTP/1.1 %d %s\r\n" self.code (Response_code.descr self.code);
List.iter (fun (k,v) -> Printf.fprintf oc "%s: %s\r\n" k v) self.headers;
Printf.fprintf oc "\r\n";
if self.body<>"" then (
output_string oc self.body;
);
flush oc
end
type t = {
addr: string;
port: int;
fork: (unit -> unit) -> unit;
masksigpipe: bool;
mutable handler: (Request.t -> Response.t);
mutable path_handlers : (Request.t -> (unit -> Response.t) option) list;
mutable running: bool;
}
let addr self = self.addr
let port self = self.port
let set_top_handler self f = self.handler <- f
let add_path_handler ?meth self fmt f =
let ph req: (unit -> Response.t) option =
match meth with
| Some m when m <> req.Request.meth -> None (* ignore *)
| _ ->
try Some (Scanf.sscanf req.Request.path fmt (f req))
with _ -> None
in
self.path_handlers <- ph :: self.path_handlers
let create
?(masksigpipe=true)
?(fork=(fun f -> ignore (Thread.create f () : Thread.t)))
?(addr="127.0.0.1") ?(port=8080) () : t =
let handler _req = Response.make_not_found "no top handler" in
{ fork; addr; port; masksigpipe; handler; running= true;
path_handlers=[]; }
let stop s = s.running <- false
let find_map f l =
let rec aux f = function
| [] -> None
| x::l' ->
match f x with
| Some _ as res -> res
| None -> aux f l'
in aux f l
let handle_client_ (self:t) (client_sock:Unix.file_descr) : unit =
let ic = Unix.in_channel_of_descr client_sock in
let oc = Unix.out_channel_of_descr client_sock in
let handler = self.handler in
let ph_handlers = self.path_handlers in
let continue = ref true in
while !continue && self.running do
match Request.parse_ ic with
| Ok None -> continue := false
| Error (c,s) ->
let res = Response.make ~code:c s in
Response.output_ oc res
| Ok (Some req) ->
let res =
let run_handler =
match find_map (fun ph -> ph req) ph_handlers with
| Some f -> f
| None -> (fun () -> handler req)
in
try run_handler()
with
| e ->
Response.make ~code:500 ("server error: " ^ Printexc.to_string e)
in
Response.output_ oc res
| exception Sys_error _ ->
continue := false; (* connection broken somehow *)
Unix.close client_sock;
done
let run (self:t) : (unit,_) result =
try
if self.masksigpipe then (
ignore (Unix.sigprocmask Unix.SIG_BLOCK [Sys.sigpipe] : _ list);
);
let sock = Unix.socket ~cloexec:true PF_INET Unix.SOCK_STREAM 0 in
let inet_addr = Unix.inet_addr_of_string self.addr in
Unix.bind sock (Unix.ADDR_INET (inet_addr, self.port));
Unix.listen sock 10;
while self.running do
let client_sock, _ = Unix.accept sock in
self.fork
(fun () -> handle_client_ self client_sock);
done;
Ok ()
with e -> Error e

88
src/SimpleHTTPServer.mli Normal file
View file

@ -0,0 +1,88 @@
module Meth : sig
type t = [
| `GET
| `PUT
| `POST
| `HEAD
]
val pp : Format.formatter -> t -> unit
val to_string : t -> string
end
module Headers : sig
type t = (string * string) list
val pp : Format.formatter -> t -> unit
end
module Request : sig
type t
val pp : Format.formatter -> t -> unit
val headers : t -> Headers.t
val meth : t -> Meth.t
val path : t -> string
val body : t -> string
end
module Response_code : sig
type t = int
val descr : t -> string
end
module Response : sig
type t
val make :
?headers:Headers.t ->
code:Response_code.t ->
string ->
t
val make_ok : ?headers:Headers.t -> string -> t
val make_not_found : ?headers:Headers.t -> string -> t
val make_error : ?headers:Headers.t -> string -> t
val pp : Format.formatter -> t -> unit
end
type t
val create :
?masksigpipe:bool ->
?fork:((unit -> unit) -> unit) ->
?addr:string ->
?port:int ->
unit ->
t
val addr : t -> string
val port : t -> int
val set_top_handler : t -> (Request.t -> Response.t) -> unit
(** Setup a handler called by default.
If not installed, unhandled paths will return a 404 not found. *)
val add_path_handler :
?meth:Meth.t ->
t ->
('a, Scanf.Scanning.in_channel, 'b, 'c -> unit -> Response.t, 'a -> 'd, 'd) format6 ->
(Request.t -> 'c) -> unit
(** [add_path_handler server "/some/path/%s@/%d/" f]
calls [f request "foo" 42 ()] when a request with path "some/path/foo/42/"
is received.
This uses {!Scanf}'s splitting, which has some gotchas (in particular,
["%s"] is eager, so it's generally necessary to delimit its
scope with a ["@/"] delimiter. The "@" before a character indicates it's
a separator.
@param meth if provided, only accept requests with the given method
*)
val stop : t -> unit
val run : t -> (unit, exn) result

6
src/dune Normal file
View file

@ -0,0 +1,6 @@
(library
(name simplehttpserver)
(public_name simplehttpserver)
(libraries threads)
(wrapped false))

4
src/examples/dune Normal file
View file

@ -0,0 +1,4 @@
(executables
(names echo)
(libraries simplehttpserver))

15
src/examples/echo.ml Normal file
View file

@ -0,0 +1,15 @@
module S = SimpleHTTPServer
let () =
let server = S.create () in
(* say hello *)
S.add_path_handler ~meth:`GET server
"/hello/%s@/" (fun _req name () -> S.Response.make_ok ("hello " ^name ^"!\n"));
(* echo request *)
S.add_path_handler server
"/echo" (fun req () -> S.Response.make_ok (Format.asprintf "echo:@ %a@." S.Request.pp req));
Printf.printf "listening on http://%s:%d\n%!" (S.addr server) (S.port server);
match S.run server with
| Ok () -> ()
| Error e -> raise e