package yurt

  1. Overview
  2. Docs

Source file yurt_server.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
open Yurt_route
open Yurt_request_ctx

open Lwt
include Cohttp_lwt_unix.Server

type server = {
    host : string;
    port : int;
    mutable routes : (string * route * endpoint) list;
    mutable tls_config : Conduit_lwt_unix.server_tls_config option;
    mutable logger : Lwt_log.logger;
}

let tls_server_key_of_config (crt, key, pass, _) =
    `TLS (crt, key, pass)

let server ?tls_config ?logger:(logger=(!Lwt_log.default))  (host : string) (port : int) : server =
    {
        host = host;
        port = port;
        routes = [];
        tls_config = tls_config;
        logger = logger;
    }

let find_string j path =
    match Ezjsonm.find j path with
    | `String s -> s
    | `Float f -> string_of_float f
    | _ -> raise Not_found

let find_float j path =
    match Ezjsonm.find j path with
    | `String s -> float_of_string s
    | `Float f -> f
    | _ -> raise Not_found

let find_tls_config j port =
    try
        let crt = find_string j ["ssl-certificate"] in
        let key = find_string j ["ssl-key"] in
        Some (`Crt_file_path crt, `Key_file_path key, `No_password, `Port port)
    with Not_found -> None

let server_from_config filename =
    try
        let ic = open_in filename in
        let j = Ezjsonm.from_channel ic in
        let host = find_string j ["host"] in
        let port = find_float j ["port"] |> int_of_float in
        let () = close_in ic in
        let tls_config = find_tls_config j port in
        server ?tls_config host port
    with Not_found ->
        print_endline "Invalid config file";
        exit 1

 exception End_route_iteration of (Response.t * Body.t) Lwt.t

 (* Logging *)

 let log_debug (s : server) section msg =
     Lwt_log.ign_debug ~section:(Lwt_log.Section.make section)  ~logger:s.logger msg

 let log_info (s : server) section msg =
     Lwt_log.ign_info ~section:(Lwt_log.Section.make section) ~logger:s.logger msg

 let log_notice (s : server) section msg =
     Lwt_log.ign_notice ~section:(Lwt_log.Section.make section) ~logger:s.logger msg

 let log_warning (s : server) section msg =
     Lwt_log.ign_warning  ~section:(Lwt_log.Section.make section) ~logger:s.logger msg

 let log_error (s : server) section msg =
     Lwt_log.ign_error ~section:(Lwt_log.Section.make section) ~logger:s.logger msg

 let log_fatal (s : server) section msg =
     Lwt_log.ign_fatal ~section:(Lwt_log.Section.make section) ~logger:s.logger msg

(** Configure TLS for server *)
let configure_tls ?password:(password=`No_password) (s : server) (crt_file : string) (key_file : string) : server =
    s.tls_config <- Some (`Crt_file_path crt_file, `Key_file_path key_file, password, `Port s.port); s

(** Finish with a string stream *)
let stream ?flush ?headers ?(status = 200) (s : string Lwt_stream.t) =
    let status = Cohttp.Code.status_of_code status in
    respond ?flush ?headers ~status ~body:(Body.of_stream s) ()

let string ?flush ?headers ?(status = 200) string =
    let status = Cohttp.Code.status_of_code status in
    respond ?flush ?headers ~status ~body:(Body.of_string string) ()

(** Finish with JSON *)
let json ?flush ?headers ?(status = 200) j =
    let status = Cohttp.Code.status_of_code status in
    respond_string ?flush ?headers ~status ~body:(Ezjsonm.to_string j) ()

(** Finish with HTML *)
let html ?flush ?headers ?(status = 200) (h : Yurt_html.t) =
    let status = Cohttp.Code.status_of_code status in
    respond_string ?flush ?headers ~status ~body:(Yurt_html.to_string h) ()

let file ?headers filename =
    respond_file ?headers ~fname:filename ()

let redirect ?headers (url : string) =
    respond_redirect ?headers ~uri:(Uri.of_string url) ()

(** Sets a route for a compiled regex + endpoint function *)
let register (s : server) (r : (string * route * endpoint) list) =
    s.routes <- s.routes @ (List.map (fun (meth, x, ep) ->
        (String.uppercase_ascii meth, x, ep)) r); s

(** Register a single route *)
let register_route_string (s : server) (meth : string) (route : string) (ep : endpoint) =
    register s [meth, Yurt_route.of_string route, ep]

(** Register a single route *)
let register_route (s : server) (meth : string) (r : route) (ep : endpoint) =
    register s [meth, r, ep]

(** Register a route for a directory *)
let register_static_file_route  ?headers (s: server) (path : string) (prefix : string) =
    register_route s "GET" (`Route [`Path prefix; `Match ("path", ".*")]) (fun _req params _body ->
    if not (Yurt_util.is_safe_path path) then
        respond_not_found ()
    else
        let filename = Filename.concat path (Yurt_route.string params "path") in
        respond_file ?headers ~fname:filename ())

(** Register a route for single file *)
let register_single_file_route ?headers (s: server) (filename : string)  (rt : string) =
    register_route s "GET" (`Route [`Path rt]) (fun _req _body _params ->
        respond_file ?headers ~fname:filename ())

let options (r : string) (ep : endpoint) (s : server) =
    register_route_string s "OPTIONS" r ep

let get (r : string) (ep : endpoint) (s : server) =
    register_route_string s "GET" r ep

let post (r : string) (ep : endpoint) (s : server) =
    register_route_string s "POST" r ep

let put (r : string) (ep : endpoint) (s : server) =
    register_route_string s "PUT" r ep

let update (r : string) (ep : endpoint) (s : server) =
    register_route_string s "UPDATE" r ep

let delete (r : string) (ep : endpoint) (s : server) =
    register_route_string s "DELETE" r ep

let folder (p : string) (r : string) (s : server) =
    register_static_file_route s p r

let static_file (p : string) (f : string) (s : server) =
    register_single_file_route s p f

(** Start the server *)
let wrap (s : server) srv =
    let callback _conn req body =
        let uri = Uri.path (Request.uri req) in
        try
            let (_, _route, _endpoint) = List.find (fun (_method, _route, _endpoint) ->
                _method = Cohttp.Code.string_of_method (Request.meth req) && Yurt_route.matches _route uri) s.routes in
            _endpoint req (params _route uri) body
        with _ -> respond_not_found () in
    srv (make ~callback ())

(** Run as daemon *)
let daemonize ?directory ?syslog (s : server) =
    Lwt_daemon.daemonize ~stdin:`Close ~stdout:(`Log s.logger) ~stderr:(`Log s.logger) ?directory ?syslog ()

(** Start a configured server with attached endpoints *)
let start (s : server) =
    match s.tls_config with
    | Some config ->
        Conduit_lwt_unix.init ?src:(Some s.host) ?tls_server_key:(Some (tls_server_key_of_config config)) ()
        >>= (fun ctx ->
            let ctx' = Cohttp_lwt_unix.Net.init ?ctx:(Some ctx) () in
            wrap s (create ~mode:(`TLS config) ~ctx:ctx'))
     | None ->
        Conduit_lwt_unix.init ?src:(Some s.host) ?tls_server_key:None ()
        >>= (fun ctx ->
            let ctx' = Cohttp_lwt_unix.Net.init ?ctx:(Some ctx) () in
            wrap s (create ~mode:(`TCP (`Port s.port)) ~ctx:ctx'))

exception Cannot_start_server

let run s =
     try Lwt_main.run (start s)
     with _ -> raise Cannot_start_server

(** Add a handler *)
let (>|) (s : server) (fn :  server -> server ) : server =
    fn s

(** Run a function that returns unit in the handler definition chain *)
let (>||) (s : server) (fn : server -> unit) : server =
    fn s; s

OCaml

Innovation. Community. Security.