mirror of
https://github.com/c-cube/tiny_httpd.git
synced 2025-12-06 11:15:35 -05:00
initial commit
This commit is contained in:
commit
3703436d18
12 changed files with 475 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.git
|
||||||
|
_build
|
||||||
|
*.install
|
||||||
|
.merlin
|
||||||
16
.travis.yml
Normal file
16
.travis.yml
Normal 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
20
Makefile
Normal 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
62
README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
|
||||||
|
# SimpleHTTPServer [](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
1
dune-project
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
(lang dune 1.1)
|
||||||
2
echo.sh
Executable file
2
echo.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
exec dune exec "src/examples/echo.exe" -- $@
|
||||||
23
simplehttpserver.opam
Normal file
23
simplehttpserver.opam
Normal 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
234
src/SimpleHTTPServer.ml
Normal 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
88
src/SimpleHTTPServer.mli
Normal 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
6
src/dune
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
(library
|
||||||
|
(name simplehttpserver)
|
||||||
|
(public_name simplehttpserver)
|
||||||
|
(libraries threads)
|
||||||
|
(wrapped false))
|
||||||
4
src/examples/dune
Normal file
4
src/examples/dune
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
(executables
|
||||||
|
(names echo)
|
||||||
|
(libraries simplehttpserver))
|
||||||
15
src/examples/echo.ml
Normal file
15
src/examples/echo.ml
Normal 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
|
||||||
Loading…
Add table
Reference in a new issue