Skip to content

Commit c3dcfab

Browse files
authored
Merge pull request #22 from goblint/include-http-server
Include Goblint-Http Server in GobView repository
2 parents 2cc57db + d3f34ad commit c3dcfab

File tree

13 files changed

+452
-5
lines changed

13 files changed

+452
-5
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ A Web Frontend for Goblint.
55
It allows inspecting the analyzed files and results of an analysis run with Goblint.
66
It is based on [jsoo-react](https://github.com/jchavarri/jsoo-react) and was originally developed by Alex Micheli for his Bachelor's thesis at TUM i2, significantly extended by Kerem Cakirer, and is now maintained by the Goblint team.
77

8+
## Goblint Http-Server
9+
10+
Http-Server for the communication between GobView and Goblint in server mode. It also serves the files for the web page.
11+
812
## Installing
913

1014
Follow the instructions in the [Read the Docs](https://goblint.readthedocs.io/en/latest/user-guide/inspecting/).

dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
(rule
22
(alias gobview)
33
(targets dist)
4-
(deps src/App.bc.js node_modules webpack.config.js)
4+
(deps src/App.bc.js goblint-http-server/goblint_http.exe node_modules webpack.config.js)
55
(action
66
(run npx webpack build)))
77

dune-project

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,18 @@
2424
(synopsis "Web frontend for Goblint")
2525
(depends
2626
dune
27-
(ocaml
28-
(>= 4.10.0))
27+
(ocaml (>= 4.10.0))
28+
batteries
29+
cohttp-lwt
30+
cohttp-lwt-unix
31+
cohttp-server-lwt-unix
32+
fileutils
33+
jsonrpc
34+
lwt
35+
lwt_ppx
36+
yojson
37+
ppx_yojson_conv ; TODO: switch to ppx_deriving_yojson like Goblint itself
38+
conduit-lwt-unix
2939
jsoo-react
3040
(goblint-cil (>= 2.0.0))
3141
ctypes_stubs_js

goblint-http-server/LICENSE

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright 2022 Kerem Çakırer
2+
3+
Permission to use, copy, modify, and/or distribute this software for any
4+
purpose with or without fee is hereby granted, provided that the above
5+
copyright notice and this permission notice appear in all copies.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
8+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
9+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
10+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
11+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
12+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
13+
PERFORMANCE OF THIS SOFTWARE.

goblint-http-server/api.ml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
open Batteries
2+
open State
3+
4+
module type Request = sig
5+
val name: string
6+
7+
type body
8+
type response
9+
10+
val body_of_yojson: Yojson.Safe.t -> body
11+
val yojson_of_response: response -> Yojson.Safe.t
12+
13+
val process: State.t -> body -> response Lwt.t
14+
end
15+
16+
let registry = Hashtbl.create 16
17+
18+
let register (module R : Request) = Hashtbl.add registry R.name (module R : Request)
19+
20+
module Ping = struct
21+
let name = "ping"
22+
type body = unit [@@deriving yojson]
23+
type response = unit [@@deriving yojson]
24+
let process state () = Goblint.ping state.goblint
25+
end
26+
27+
module Config = struct
28+
let name = "config"
29+
type body = string * Json.t [@@deriving yojson]
30+
type response = unit [@@deriving yojson]
31+
let process state (conf, json) = Goblint.config state.goblint conf json
32+
end
33+
34+
module Analyze = struct
35+
let name = "analyze"
36+
type body = [`All | `Functions of string list] option [@@deriving yojson]
37+
type response = unit [@@deriving yojson]
38+
let process state reanalyze =
39+
let%lwt save_run = Goblint.analyze state.goblint ?reanalyze in
40+
state.save_run <- Some save_run;
41+
Lwt.return_unit
42+
end
43+
44+
let () =
45+
register (module Ping);
46+
register (module Config);
47+
register (module Analyze)

goblint-http-server/dune

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
(executable
2+
(name goblint_http)
3+
(public_name goblint-http)
4+
(libraries
5+
batteries
6+
cohttp
7+
cohttp-lwt
8+
cohttp-lwt-unix
9+
cohttp-server-lwt-unix
10+
conduit-lwt-unix
11+
jsonrpc
12+
lwt.unix
13+
yojson
14+
uri)
15+
(preprocess
16+
(pps lwt_ppx ppx_yojson_conv)))

goblint-http-server/goblint.ml

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
open Batteries
2+
open Lwt.Infix
3+
4+
type t = {
5+
input: Lwt_io.input_channel;
6+
output: Lwt_io.output_channel;
7+
mutex: Lwt_mutex.t;
8+
mutable counter: int;
9+
}
10+
11+
let spawn path args =
12+
let base_args = [| path; "--enable"; "server.enabled"; "--set"; "server.mode"; "unix" |] in
13+
let cmd = args |> Array.of_list |> Array.append base_args in
14+
let _proc = Lwt_process.open_process_none (path, cmd) in
15+
let sock = Lwt_unix.socket PF_UNIX SOCK_STREAM 0 in
16+
(* Wait a bit for the server to be initialized. *)
17+
let%lwt () = Lwt_unix.sleep 0.5 in
18+
let%lwt () = Lwt_unix.connect sock (ADDR_UNIX "goblint.sock") in
19+
let input = Lwt_io.of_fd ~mode:Lwt_io.Input sock in
20+
let output = Lwt_io.of_fd ~mode:Lwt_io.Output sock in
21+
Lwt.return { input; output; mutex = Lwt_mutex.create (); counter = 0 }
22+
23+
let with_lock goblint = Lwt_mutex.with_lock goblint.mutex
24+
25+
let assert_ok (resp: Jsonrpc.Response.t) s = match resp.result with
26+
| Ok _ -> ()
27+
| Error e -> failwith (Format.sprintf "%s (%s)" s e.message)
28+
29+
let send goblint name params =
30+
let id = `Int goblint.counter in
31+
goblint.counter <- goblint.counter + 1;
32+
let req =
33+
Jsonrpc.Request.create ?params ~id ~method_:name ()
34+
|> Jsonrpc.Request.yojson_of_t
35+
|> Yojson.Safe.to_string in
36+
Printf.printf "send jsonrpc message:\n%s\n" req;
37+
let%lwt () = Lwt_io.fprintl goblint.output req in
38+
let%lwt resp =
39+
Lwt_io.read_line goblint.input
40+
>|= Yojson.Safe.from_string
41+
>|= Jsonrpc.Response.t_of_yojson in
42+
if resp.id <> id then
43+
failwith "Response ID doesn't match request ID";
44+
Lwt.return resp
45+
46+
let ping goblint =
47+
let ping () =
48+
let%lwt resp = send goblint "ping" None in
49+
assert_ok resp "Ping failed";
50+
Lwt.return_unit
51+
in with_lock goblint ping
52+
53+
let config_raw goblint name value =
54+
let params = `List [`String name; value] in
55+
let%lwt resp = send goblint "config" (Some params) in
56+
match resp.result with
57+
| Ok _ -> Lwt.return_unit
58+
| Error err -> invalid_arg err.message
59+
60+
let option_whitelist = [] |> Set.of_list
61+
62+
let config goblint name value =
63+
if not (Set.mem name option_whitelist) then
64+
invalid_arg (Printf.sprintf "Option '%s' is not in the whitelist" name);
65+
with_lock goblint (fun () -> config_raw goblint name value)
66+
67+
let temp_dir () = Utils.temp_dir "goblint-http-server." ""
68+
69+
let analyze ?reanalyze ?save_dir ?(gobview = false) goblint =
70+
let set_force_reanalyze () = match reanalyze with
71+
| Some `Functions xs ->
72+
config_raw goblint "incremental.force-reanalyze.funs" (`List (List.map (fun s -> `String s) xs))
73+
| _ -> Lwt.return_unit
74+
in
75+
let reset_force_reanalyze () = match reanalyze with
76+
| Some `Functions _ ->
77+
config_raw goblint "incremental.force-reanalyze.funs" (`List [])
78+
| _ -> Lwt.return_unit
79+
in
80+
let set_gobview () = if gobview then config_raw goblint "gobview" (`Bool true) else Lwt.return_unit in
81+
let reset_gobview () = if gobview then config_raw goblint "gobview" (`Bool false) else Lwt.return_unit in
82+
let analyze () =
83+
let reset = match reanalyze with
84+
| Some `All -> true
85+
| _ -> false
86+
in
87+
let params = `Assoc [("reset", `Bool reset)] in
88+
Lwt.finalize
89+
(fun () ->
90+
let save_run = match save_dir with
91+
| None -> temp_dir ()
92+
| Some d -> d in
93+
let%lwt () = config_raw goblint "save_run" (`String save_run) in
94+
let%lwt () = set_force_reanalyze () in
95+
let%lwt () = set_gobview () in
96+
let%lwt resp = send goblint "analyze" (Some params) in
97+
assert_ok resp "Analysis failed";
98+
Lwt.return save_run)
99+
(fun () ->
100+
let%lwt () = reset_force_reanalyze () in
101+
let%lwt () = reset_gobview () in
102+
Lwt.return_unit)
103+
in with_lock goblint analyze
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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 ())

goblint-http-server/json.ml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type t = Yojson.Safe.t
2+
3+
let t_of_yojson x = x
4+
let yojson_of_t x = x

goblint-http-server/state.ml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
type t = {
2+
goblint: Goblint.t;
3+
(* Descriptor for the Goblint server instance *)
4+
mutable save_run: string option;
5+
(* The directory from which we serve the marshalled analysis state *)
6+
}
7+
8+
let make (goblint: Goblint.t) = { goblint; save_run = None }

0 commit comments

Comments
 (0)