Skip to content

Commit a1b3a4e

Browse files
authored
Merge pull request #1129 from tarides/shonfeder/eio-forward-proxy-unit-tests
Add unit tests for cohttp-eio forward proxy
2 parents 8f07e7f + 204f689 commit a1b3a4e

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed

cohttp-eio/tests/dune

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
(test
22
(name test)
3+
(modules test)
34
(libraries alcotest cohttp-eio eio eio.mock eio_main logs.fmt)
45
(package cohttp-eio)
56
(preprocess
67
(pps ppx_here)))
8+
9+
(test
10+
(name test_forward_proxy)
11+
(modules test_forward_proxy)
12+
(libraries alcotest cohttp-eio eio eio.mock eio_main logs.fmt)
13+
(package cohttp-eio))
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
(* Tests the core behaviour if the forward proxy *)
2+
3+
let () =
4+
Logs.set_level ~all:true @@ Some Logs.Debug;
5+
Logs.set_reporter (Logs_fmt.reporter ())
6+
7+
(* Used to pass data out of the server *)
8+
module Req_data = struct
9+
let side_channel : Http.Request.t Eio.Stream.t = Eio.Stream.create 1
10+
let send t = Eio.Stream.add side_channel t
11+
12+
let get () =
13+
if Eio.Stream.is_empty side_channel then failwith "no requests pending";
14+
Eio.Stream.take side_channel
15+
end
16+
17+
let t_meth : Http.Method.t Alcotest.testable =
18+
Alcotest.testable Http.Method.pp (fun a b -> Http.Method.compare a b = 0)
19+
20+
(* The proxy server sends every request to the `Req_data` side channel and
21+
always responds with 200. *)
22+
let run_proxy_server server_port net sw =
23+
let handler ~sw _conn request body =
24+
let _ = Eio.Buf_read.(of_flow ~max_size:max_int body |> take_all) in
25+
Eio.Fiber.fork ~sw (fun () -> Req_data.send request);
26+
Cohttp_eio.Server.respond_string ~status:`OK ~body:"" ()
27+
in
28+
let socket =
29+
Eio.Net.listen net ~sw ~backlog:128 ~reuse_addr:true ~reuse_port:true
30+
(`Tcp (Eio.Net.Ipaddr.V4.loopback, server_port))
31+
and server = Cohttp_eio.Server.make ~callback:(handler ~sw) () in
32+
Eio.Fiber.fork_daemon ~sw @@ fun () ->
33+
let () = Cohttp_eio.Server.run socket server ~on_error:raise in
34+
`Stop_daemon
35+
36+
let () =
37+
(* Different tests run in parallel, so the port should be unique among
38+
tests *)
39+
let server_port = 4243 in
40+
let () =
41+
Cohttp_eio.Client.set_proxies
42+
~default_proxy:
43+
(Uri.of_string @@ Printf.sprintf "http://127.0.0.1:%d" server_port)
44+
()
45+
in
46+
Eio_main.run @@ fun env ->
47+
Eio.Switch.run @@ fun sw ->
48+
let () = run_proxy_server server_port env#net sw in
49+
let client =
50+
let noop_https_wrapper = Some (fun _ f -> f) in
51+
Cohttp_eio.Client.make ~https:noop_https_wrapper env#net
52+
in
53+
let get_success uri =
54+
let resp, _ = Cohttp_eio.Client.get ~sw client uri in
55+
match Http.Response.status resp with
56+
| `OK -> ()
57+
| unexpected ->
58+
Alcotest.failf "unexpected response from test_forward_proxy server %a"
59+
Http.Status.pp unexpected
60+
in
61+
62+
(* TESTS CASES *)
63+
let direct_proxied_request () =
64+
(* When the remote host is over HTTP *)
65+
let uri = Uri.of_string "http://foo.org" in
66+
get_success uri;
67+
let req = Req_data.get () in
68+
let meth = Http.Request.meth req in
69+
Alcotest.(check' t_meth)
70+
~msg:"should be a GET request" ~actual:meth ~expected:`GET;
71+
let host =
72+
let headers = Http.Request.headers req in
73+
Http.Header.get headers "host"
74+
in
75+
Alcotest.(check' (option string))
76+
~msg:"should request from remote host" ~actual:host
77+
~expected:(Some "foo.org")
78+
and tunnelled_proxied_request () =
79+
(* When the remote host is over HTTPS *)
80+
let uri = Uri.of_string "https://foo.org" in
81+
get_success uri;
82+
let req = Req_data.get () in
83+
let meth = Http.Request.meth req in
84+
Alcotest.(check' t_meth)
85+
~msg:"should first initiate a CONNECT request" ~actual:meth
86+
~expected:`CONNECT;
87+
let host =
88+
let headers = Http.Request.headers req in
89+
Http.Header.get headers "host"
90+
in
91+
Alcotest.(check' (option string))
92+
~msg:"should request from remote host (with port)" ~actual:host
93+
~expected:(Some "foo.org:443");
94+
95+
let req' = Req_data.get () in
96+
let meth' = Http.Request.meth req' in
97+
Alcotest.(check' t_meth)
98+
~msg:"should then send a GET request" ~actual:meth' ~expected:`GET;
99+
let host =
100+
let headers = Http.Request.headers req in
101+
Http.Header.get headers "host"
102+
in
103+
Alcotest.(check' (option string))
104+
~msg:"should request from remote host (with port)" ~actual:host
105+
~expected:(Some "foo.org:443")
106+
and unset_proxy () =
107+
let () = Cohttp_eio.Client.set_proxies ?default_proxy:None () in
108+
(* .invalid domains are guaranteed to not have hosts:
109+
https://www.rfc-editor.org/rfc/rfc2606 *)
110+
let uri = Uri.of_string "http://foo.invalid" in
111+
match Cohttp_eio.Client.get ~sw client uri with
112+
| exception Failure _ ->
113+
(* This should fail, since we are not using the proxy *)
114+
()
115+
| unexepcted_resp, _ ->
116+
Alcotest.failf
117+
"Resolution of uri should have failed, but succeeded with %a"
118+
Http.Response.pp unexepcted_resp
119+
in
120+
Alcotest.run "cohttp-eio client"
121+
[
122+
( "cohttp-eio forward proxy",
123+
[
124+
("direct get", `Quick, direct_proxied_request);
125+
("tunnelled proxied request", `Quick, tunnelled_proxied_request);
126+
("unessting the proxy config", `Quick, unset_proxy);
127+
] );
128+
]

0 commit comments

Comments
 (0)