|
| 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