mirror of
https://github.com/c-cube/tiny_httpd.git
synced 2025-12-06 03:05:29 -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