|
| 1 | +open Batteries |
| 2 | +open Cohttp_lwt |
| 3 | +open Cohttp_lwt_unix |
| 4 | +open Lwt.Infix |
| 5 | + |
| 6 | +module Yojson_conv = Ppx_yojson_conv_lib.Yojson_conv |
| 7 | + |
| 8 | +let docroot = ref "run" |
| 9 | +let index = ref "index.html" |
| 10 | +let addr = ref "127.0.0.1" |
| 11 | +let port = ref 8080 |
| 12 | +let goblint = ref "goblint" |
| 13 | +let rest = ref [] |
| 14 | + |
| 15 | +let specs = |
| 16 | + [ |
| 17 | + ("-docroot", Arg.Set_string docroot, "Serving directory"); |
| 18 | + ("-index", Arg.Set_string index, "Name of index file in directory"); |
| 19 | + ("-addr", Arg.Set_string addr, "Listen address"); |
| 20 | + ("-port", Arg.Set_int port, "Listen port"); |
| 21 | + ("-with-goblint", Arg.Set_string goblint, "Path to the Goblint executable"); |
| 22 | + ("-goblint", Arg.Rest_all (fun args -> rest := args), "Pass the rest of the arguments to Goblint"); |
| 23 | + ] |
| 24 | + |
| 25 | +let paths = ref [] |
| 26 | + |
| 27 | +let process state name body = |
| 28 | + match Hashtbl.find_option Api.registry name with |
| 29 | + | None -> Server.respond_not_found () |
| 30 | + | Some (module R) -> |
| 31 | + let%lwt body = Body.to_string body in |
| 32 | + let body = if body = "" then "null" else body in |
| 33 | + match Yojson.Safe.from_string body with |
| 34 | + | exception Yojson.Json_error err -> Server.respond_error ~status:`Bad_request ~body:err () |
| 35 | + | json -> |
| 36 | + match R.body_of_yojson json with |
| 37 | + | exception Yojson_conv.Of_yojson_error (exn, _) -> |
| 38 | + Server.respond_error ~status:`Bad_request ~body:(Printexc.to_string exn) () |
| 39 | + | body -> |
| 40 | + Lwt.catch |
| 41 | + (fun () -> |
| 42 | + R.process state body |
| 43 | + >|= R.yojson_of_response |
| 44 | + >|= Yojson.Safe.to_string |
| 45 | + >>= fun body -> Server.respond_string ~status:`OK ~body ()) |
| 46 | + (fun exn -> Server.respond_error ~status:`Bad_request ~body:(Printexc.to_string exn) ()) |
| 47 | + |
| 48 | +(* The serving of files is implemented similar as in the binary https://github.com/mirage/ocaml-cohttp/blob/master/cohttp-lwt-unix/bin/cohttp_server_lwt.ml *) |
| 49 | +let serve_file ~docroot ~uri = |
| 50 | + let fname = Cohttp.Path.resolve_local_file ~docroot ~uri in |
| 51 | + Server.respond_file ~fname () |
| 52 | + |
| 53 | +let sort lst = |
| 54 | + let compare_kind = function |
| 55 | + | Some Unix.S_DIR, Some Unix.S_DIR -> 0 |
| 56 | + | Some Unix.S_DIR, _ -> -1 |
| 57 | + | _, Some Unix.S_DIR -> 1 |
| 58 | + | Some Unix.S_REG, Some Unix.S_REG -> 0 |
| 59 | + | Some Unix.S_REG, _ -> 1 |
| 60 | + | _, Some Unix.S_REG -> -1 |
| 61 | + | _, _ -> 0 in |
| 62 | + List.sort |
| 63 | + (fun (ka, a) (kb, b) -> |
| 64 | + let c = compare_kind (ka, kb) in |
| 65 | + if c <> 0 then c |
| 66 | + else String.compare (String.lowercase_ascii a) (String.lowercase_ascii b)) |
| 67 | + lst |
| 68 | + |
| 69 | +let html_of_listing uri path listing = |
| 70 | + let li l = |
| 71 | + Printf.sprintf "<li><a href=\"%s\">%s</a></li>" (Uri.to_string l) in |
| 72 | + let html = |
| 73 | + List.map |
| 74 | + (fun (kind, f) -> |
| 75 | + let encoded_f = Uri.pct_encode f in |
| 76 | + match kind with |
| 77 | + | Some Unix.S_DIR -> |
| 78 | + let link = Uri.with_path uri (Filename.concat path (Filename.concat encoded_f "")) in |
| 79 | + li link (Printf.sprintf "<i>%s/</i>" f) |
| 80 | + | Some Unix.S_REG -> |
| 81 | + let link = Uri.with_path uri (Filename.concat path encoded_f) in |
| 82 | + li link f |
| 83 | + | Some _ -> |
| 84 | + Printf.sprintf "<li><s>%s</s></li>" f |
| 85 | + | None -> Printf.sprintf "<li>Error with file: %s</li>" f) |
| 86 | + listing |
| 87 | + in |
| 88 | + let contents = String.concat "\n" html in |
| 89 | + Printf.sprintf |
| 90 | + "<html><body><h2>Directory Listing for <em>%s</em></h2><ul>%s</ul><hr \ |
| 91 | + /></body></html>" |
| 92 | + (Uri.pct_decode path) contents |
| 93 | + |
| 94 | +let serve uri path = |
| 95 | + let file_name = Cohttp.Path.resolve_local_file ~docroot:!docroot ~uri in |
| 96 | + Lwt.catch |
| 97 | + (fun () -> |
| 98 | + Lwt_unix.lstat file_name >>= fun stat -> (* for symbolic links lstat returns S_LNK, which will result in a |
| 99 | + forbidden error in this implementation. Use stat instead if symbolic links to folders or files should be handled |
| 100 | + just like folders or files respectively *) |
| 101 | + match stat.Unix.st_kind with |
| 102 | + | Unix.S_DIR -> ( |
| 103 | + let path_len = String.length path in |
| 104 | + if path_len <> 0 && path.[path_len - 1] <> '/' then ( |
| 105 | + Server.respond_redirect ~uri:(Uri.with_path uri (path ^ "/")) ()) |
| 106 | + else ( |
| 107 | + match Sys.file_exists (Filename.concat file_name !index) with |
| 108 | + | true -> ( |
| 109 | + let uri = Uri.with_path uri (Filename.concat path !index) in |
| 110 | + serve_file ~docroot:!docroot ~uri) |
| 111 | + | false -> |
| 112 | + let%lwt files = Lwt_stream.to_list |
| 113 | + (Lwt_stream.filter (fun s -> s <> "." && s <> "..") (Lwt_unix.files_of_directory file_name)) in |
| 114 | + let%lwt listing = Lwt_list.map_s (fun f -> |
| 115 | + let file_name = Filename.concat file_name f in |
| 116 | + Lwt.try_bind |
| 117 | + (fun () -> Lwt_unix.LargeFile.stat file_name) |
| 118 | + (fun stat -> |
| 119 | + Lwt.return |
| 120 | + ( Some |
| 121 | + stat.Unix.LargeFile.st_kind, |
| 122 | + f )) |
| 123 | + (fun _exn -> Lwt.return (None, f))) files in |
| 124 | + let body = html_of_listing uri path (sort listing) in |
| 125 | + Server.respond_string ~status:`OK ~body ())) |
| 126 | + | Unix.S_REG -> serve_file ~docroot:!docroot ~uri |
| 127 | + | _ -> ( |
| 128 | + let body = Printf.sprintf "<html><body><h2>Forbidden</h2><p><b>%s</b> is not a normal file or \ |
| 129 | + directory</p><hr/></body></html>" path in |
| 130 | + Server.respond_string ~status:`OK ~body ())) |
| 131 | + (function |
| 132 | + | Unix.Unix_error (Unix.ENOENT, "stat", p) as e -> |
| 133 | + if p = file_name then ( |
| 134 | + Server.respond_not_found ()) |
| 135 | + else Lwt.fail e |
| 136 | + | e -> Lwt.fail e) |
| 137 | + |
| 138 | +let callback state _ req body = |
| 139 | + let uri = Request.uri req in |
| 140 | + let path = Uri.path uri in |
| 141 | + let parts = String.split_on_char '/' path |> List.filter (not % String.is_empty) in |
| 142 | + let meth = Request.meth req in |
| 143 | + match meth, parts with |
| 144 | + | `POST, ["api"; name] -> process state name body |
| 145 | + | `GET, _ -> serve uri path |
| 146 | + | _ -> Server.respond_not_found () |
| 147 | + |
| 148 | +let main () = |
| 149 | + let%lwt state = Goblint.spawn !goblint (!rest @ !paths) >|= State.make in |
| 150 | + (* run Goblint once with option gobview enabled to copy the index.html and main.js files into the served directory *) |
| 151 | + let%lwt _ = Goblint.analyze ~save_dir:!docroot ~gobview:true state.goblint in |
| 152 | + let callback = callback state in |
| 153 | + let server = Server.make ~callback () in |
| 154 | + Server.create ~mode:(`TCP (`Port !port)) server |
| 155 | + |
| 156 | +let () = |
| 157 | + let program = Sys.argv.(0) in |
| 158 | + let usage = Printf.sprintf "%s [-docroot DOCROOT] [-index INDEX] [-addr ADDR] [-port PORT] ... path [path ...]" program in |
| 159 | + Arg.parse specs (fun s -> paths := s :: !paths) usage; |
| 160 | + Lwt_main.run (main ()) |
0 commit comments