New upstream version 6.1.0
authorStephane Glondu <steph@glondu.net>
Sat, 14 Mar 2026 08:43:02 +0000 (09:43 +0100)
committerStephane Glondu <steph@glondu.net>
Sat, 14 Mar 2026 08:43:02 +0000 (09:43 +0100)
56 files changed:
.github/workflows/nix.yml
.github/workflows/workflow.yml
.ocamlformat
CHANGES.md
cohttp-async.opam
cohttp-async/bin/cohttp_server_async.ml
cohttp-async/test/test_async_integration.ml
cohttp-bench.opam
cohttp-curl-async.opam
cohttp-curl-lwt.opam
cohttp-curl.opam
cohttp-eio.opam
cohttp-eio/examples/client_tls.ml
cohttp-eio/examples/dune
cohttp-lwt-jsoo.opam
cohttp-lwt-jsoo/src/cohttp_lwt_jsoo.ml
cohttp-lwt-unix.opam
cohttp-lwt-unix/examples/client_lwt_proxy.ml [new file with mode: 0644]
cohttp-lwt-unix/examples/dune
cohttp-lwt-unix/src/cohttp_lwt_unix.ml
cohttp-lwt-unix/src/net.ml
cohttp-lwt-unix/src/net.mli
cohttp-lwt-unix/test/test_client.ml
cohttp-lwt-unix/test/test_sanity.ml
cohttp-lwt.opam
cohttp-lwt/src/connection.ml
cohttp-lwt/src/connection_cache.ml
cohttp-lwt/src/connection_cache.mli [new file with mode: 0644]
cohttp-lwt/src/dune
cohttp-lwt/src/s.ml
cohttp-mirage.opam
cohttp-mirage/src/client.ml
cohttp-mirage/src/client.mli
cohttp-mirage/src/io.ml
cohttp-mirage/src/io.mli
cohttp-mirage/src/net.ml
cohttp-mirage/src/net.mli
cohttp-mirage/src/static.ml
cohttp-mirage/src/static.mli
cohttp-server-lwt-unix.opam
cohttp-server-lwt-unix/src/cohttp_server_lwt_unix.ml
cohttp-server-lwt-unix/src/cohttp_server_lwt_unix.mli
cohttp-top.opam
cohttp.opam
cohttp/src/accept.mli
cohttp/src/code.mli
cohttp/src/dune
cohttp/src/header.mli
cohttp/src/request.ml
cohttp/src/s.ml
cohttp/test/test_accept.ml
dune-project
flake.lock
flake.nix
http.opam
http/src/http.mli

index 9727c48b18adbf9d7e20ef63d3d66ac9afe1b8eb..03ea1b1a158091ca0baa72e34b2c1584a44ff34b 100644 (file)
@@ -5,8 +5,6 @@ on:
 jobs:
   tests:
     runs-on: ubuntu-latest
-    env:
-      NIXPKGS_ALLOW_INSECURE: 1
     steps:
       - name: Checkout tree
         uses: actions/checkout@v4
@@ -14,5 +12,7 @@ jobs:
           submodules: true
       - uses: cachix/install-nix-action@v30
         with:
-          nix_path: nixpkgs=channel:nixos-unstable
-      - run: nix develop -c dune build @runtest @check
+          extra_nix_config: |
+            extra-substituters = https://anmonteiro.nix-cache.workers.dev
+            extra-trusted-public-keys = ocaml.nix-cache.com-1:/xI2h2+56rwFfKyyFVbkJSeGqSIYMC/Je+7XXqGKDIY=
+      - run: nix develop -L .# -c dune build @runtest @check
index c407e41d998bc6c862b8d97ba4d618a2f3281d64..ceca1229ff8350b6e019713537ffb3279565e438 100644 (file)
@@ -69,7 +69,7 @@ jobs:
           - ubuntu-latest
           - macos-latest
         ocaml-compiler:
-          - "5"
+          - "5.2"
         local-packages:
           - |
             *.opam
@@ -151,7 +151,7 @@ jobs:
         os:
           - ubuntu-latest
         ocaml-compiler:
-          - "5"
+          - "5.2"
         local-packages:
           - |
             *.opam
index 1595ace95796556ea355748811854e6f1919930a..291f8a05cf17846bfba34af221499a1f18e41dfd 100644 (file)
@@ -1,4 +1,4 @@
-version = 0.26.2
+version=0.27.0
 profile=conventional
 break-infix=fit-or-vertical
 parse-docstrings=true
index b0baa2b89651148f177b060ef86c1c79d0db895d..5f70bd80a3c6e65d380702c2aa5d1bff9fe270f0 100644 (file)
@@ -1,3 +1,8 @@
+# v6.1.0 (2025-03-03)
+
+- cohttp-lwt-unix: Add http/https proxy support for client requests (@art-w @MisterDA, #1080)
+- cohttp-mirage: Support conduit 8.0.0 (@hannesm, #1104)
+
 ## v6.0.0 (2024-11-21)
 
 - bump minimum dune version to 3.8 (@avsm)
@@ -16,7 +21,7 @@
 - cohttp: `Cohttp.Request.make_for_client` no longer allows setting both
   `~chunked:true` and `~body_length`.
 - cohttp-lwt-unix: Don't blow up when certificates are not available and no-network requests are made. (akuhlens #1027)
-  + Makes `cohttp-lwt.S.default_ctx` lazy. 
+  + Makes `cohttp-lwt.S.default_ctx` lazy.
 
 ## v6.0.0~beta2 (2024-01-05)
 
index 4a8e5d65a2a8d019c35b617368320c0261a5a6da..523458090fa3fcc29528f8bdda7d64a6e25a906e 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP implementation for the Async concurrency library"
@@ -26,7 +26,7 @@ doc: "https://mirage.github.io/ocaml-cohttp/"
 bug-reports: "https://github.com/mirage/ocaml-cohttp/issues"
 depends: [
   "dune" {>= "3.8"}
-  "ocaml" {>= "4.14"}
+  "ocaml" {>= "4.14" & < "5.3.0"}
   "http" {= version}
   "cohttp" {= version}
   "async_kernel" {>= "v0.16.0"}
index 8398e0272561bfd3e3f7bba4570e4eeba1d22954..5ecaa22ddc24d8c165e13db238dc752a418a079d 100644 (file)
@@ -116,9 +116,9 @@ let start_server docroot port index cert_file key_file verbose () =
   Server.create
     ~on_handler_error:
       (`Call
-        (fun addr exn ->
-          Logs.err (fun f -> f "Error from %s" (Socket.Address.to_string addr));
-          Logs.err (fun f -> f "%s" @@ Exn.to_string exn)))
+         (fun addr exn ->
+           Logs.err (fun f -> f "Error from %s" (Socket.Address.to_string addr));
+           Logs.err (fun f -> f "%s" @@ Exn.to_string exn)))
     ~mode
     (Tcp.Where_to_listen.of_port port)
     (handler ~info ~docroot ~index)
index b700b1a88f68e7145e7f0673420362f65beadcda..a2cfb9a2bfe14486b62654dd942b806c1df79550 100644 (file)
@@ -28,27 +28,27 @@ let server =
   ]
   (* pipelined_chunk *)
   @ (response_bodies |> List.map ~f:(Fn.compose const ok))
-  (* large response chunked *)
-  [
-    (fun _ _ ->
-      let body =
-        let r, w = Pipe.create () in
-        let chunk = chunk chunk_size in
-        for _ = 0 to chunks - 1 do
-          Pipe.write_without_pushback w chunk
-        done;
-        Pipe.close w;
-        r
-      in
-      Server.respond_with_pipe ~code:`OK body >>| response);
-    (* pipelined_expert *)
-    expert (fun _ic oc ->
-        Async_unix.Writer.write oc "8\r\nexpert 1\r\n0\r\n\r\n";
-        Async_unix.Writer.flushed oc);
-    expert (fun ic oc ->
-        Async_unix.Writer.write oc "8\r\nexpert 2\r\n0\r\n\r\n";
-        Async_unix.Writer.flushed oc >>= fun () -> Async_unix.Reader.close ic);
-  ]
+  (* large response chunked *)
+  [
+      (fun _ _ ->
+        let body =
+          let r, w = Pipe.create () in
+          let chunk = chunk chunk_size in
+          for _ = 0 to chunks - 1 do
+            Pipe.write_without_pushback w chunk
+          done;
+          Pipe.close w;
+          r
+        in
+        Server.respond_with_pipe ~code:`OK body >>| response);
+      (* pipelined_expert *)
+      expert (fun _ic oc ->
+          Async_unix.Writer.write oc "8\r\nexpert 1\r\n0\r\n\r\n";
+          Async_unix.Writer.flushed oc);
+      expert (fun ic oc ->
+          Async_unix.Writer.write oc "8\r\nexpert 2\r\n0\r\n\r\n";
+          Async_unix.Writer.flushed oc >>= fun () -> Async_unix.Reader.close ic);
+    ]
   |> response_sequence
 
 let ts =
index e187ec9b6175c75384a3163e0a18edb7781f7527..3f7dbcd317cf2a01596557a2e799e6129485a460 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Benchmarks binaries for Cohttp"
index e9e1e743a1ee456a107b93a570e8e2028b8b926d..d855be44a65d2c7bcbb73d2b7b6c740cb89e4c06 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Cohttp client using Curl & Async as the backend"
index 66aaf78452cc8635e60ac34a12cf411e13ee6076..49b43c8abfdb61d36175bf0b0c601143c4973691 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Cohttp client using Curl & Lwt as the backend"
index 5b0a91ad5e7cf0fcb849770647249a09f10a3064..dcd07e12064d41d86bd42c6be9674b055e216184 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Shared code between the individual cohttp-curl clients"
index 3f454df0da70387a1c3fb64f6010f86873fd0b17..d3ef96d85cc4304f46cd4a85e3b4c56084bf9360 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP implementation with eio backend"
@@ -30,7 +30,7 @@ depends: [
   "logs"
   "uri"
   "tls-eio" {with-test & >= "1.0.0"}
-  "mirage-crypto-rng-eio" {with-test & >= "0.11.2"}
+  "mirage-crypto-rng" {with-test & >= "1.2.0"}
   "ca-certs" {with-test & >= "1.0.0"}
   "fmt"
   "ptime"
index 5289c7c83b275cf4c6ae8cf81359596b82161bac..dc32986cd3f4a5765e0dd5d4979485eec7cbcf9c 100644 (file)
@@ -26,7 +26,7 @@ let https ~authenticator =
 
 let () =
   Eio_main.run @@ fun env ->
-  Mirage_crypto_rng_eio.run (module Mirage_crypto_rng.Fortuna) env @@ fun () ->
+  Mirage_crypto_rng_unix.use_default ();
   let client = Client.make ~https:(Some (https ~authenticator)) env#net in
   Eio.Switch.run @@ fun sw ->
   let resp, body =
index 9b0f20751278cf82f4d8d4552b130934226ed27c..8786ecb1f4742fd8bf9d32993c88d2d3e7c45656 100644 (file)
@@ -10,7 +10,7 @@
   logs.threaded
   tls-eio
   ca-certs
-  mirage-crypto-rng-eio))
+  mirage-crypto-rng.unix))
 
 (alias
  (name runtest)
index 06ae32fef9eae49da817dc7211b01455334e5125..e6d779cbe6b5f7d3609ec75a67d5db0f13748289 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP implementation for the Js_of_ocaml JavaScript compiler"
index 2d68024316683085eab31c20268fbe13e4526f8f..9c1bd2577c5ca4d1f6f21053798a6c16ea5a0269 100644 (file)
@@ -63,11 +63,11 @@ module Body_builder (P : Params) = struct
     let chunkerizer () =
       if !pos = body_len then Lwt.return C.Transfer.Done
       else if !pos + P.chunk_size >= body_len then (
-        let str = text ## (substring_toEnd !pos) in
+        let str = text##(substring_toEnd !pos) in
         pos := body_len;
         Lwt.return (C.Transfer.Final_chunk (P.convert_body_string str)))
       else
-        let str = text ## (substring !pos (!pos + P.chunk_size)) in
+        let str = text##(substring !pos (!pos + P.chunk_size)) in
         pos := !pos + P.chunk_size;
         Lwt.return (C.Transfer.Chunk (P.convert_body_string str))
     in
@@ -205,11 +205,10 @@ module Make_client_async (P : Params) = Make_api (struct
       xml##.responseType := Js.string "arraybuffer";
     let (res : (Http.Response.t Lwt.t * CLB.t) Lwt.t), wake = Lwt.task () in
     let () =
-      xml
-      ## (_open
-            (Js.string (C.Code.string_of_method meth))
-            (Js.string (Uri.to_string uri))
-            Js._true)
+      xml##(_open
+              (Js.string (C.Code.string_of_method meth))
+              (Js.string (Uri.to_string uri))
+              Js._true)
       (* asynchronous call *)
     in
     (* set request headers *)
@@ -221,7 +220,7 @@ module Make_client_async (P : Params) = Make_api (struct
             (fun k v ->
               (* some headers lead to errors in the javascript console, should
                  we filter then out here? *)
-              xml ## (setRequestHeader (Js.string k) (Js.string v)))
+              xml##(setRequestHeader (Js.string k) (Js.string v)))
             headers
     in
 
@@ -264,7 +263,7 @@ module Make_client_async (P : Params) = Make_api (struct
 
     (* perform call *)
     (match body with
-    | None -> Lwt.return xml ## (send Js.null)
+    | None -> Lwt.return xml##(send Js.null)
     | Some body ->
         CLB.to_string body >>= fun body ->
         let bs = binary_string body in
@@ -292,11 +291,10 @@ module Make_client_sync (P : Params) = Make_api (struct
     if Lazy.force xhr_response_supported then
       xml##.responseType := Js.string "arraybuffer";
     let () =
-      xml
-      ## (_open
-            (Js.string (C.Code.string_of_method meth))
-            (Js.string (Uri.to_string uri))
-            Js._false)
+      xml##(_open
+              (Js.string (C.Code.string_of_method meth))
+              (Js.string (Uri.to_string uri))
+              Js._false)
       (* synchronous call *)
     in
     (* set request headers *)
@@ -308,16 +306,16 @@ module Make_client_sync (P : Params) = Make_api (struct
             (fun k v ->
               (* some headers lead to errors in the javascript console, should
                  we filter then out here? *)
-              xml ## (setRequestHeader (Js.string k) (Js.string v)))
+              xml##(setRequestHeader (Js.string k) (Js.string v)))
             headers
     in
     (* perform call *)
     (match body with
-    | None -> Lwt.return xml ## (send Js.null)
+    | None -> Lwt.return xml##(send Js.null)
     | Some body ->
         CLB.to_string body >|= fun body ->
         let bs = binary_string body in
-        xml ## (send (Js.Opt.return (Obj.magic bs))))
+        xml##(send (Js.Opt.return (Obj.magic bs))))
     >>= fun _body ->
     let body = Bb.construct_body xml in
     (* (re-)construct the response *)
index 1128ae54fda22b720ff883807bb22d10581db28b..5255559a6fe86536aa35fc255de2210bad8af506 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP implementation for Unix and Windows using Lwt"
@@ -35,8 +35,8 @@ depends: [
   "cohttp-lwt" {= version}
   "cmdliner" {>= "1.1.0"}
   "lwt" {>= "3.0.0"}
-  "conduit-lwt" {>= "5.0.0"}
-  "conduit-lwt-unix" {>= "5.0.0"}
+  "conduit-lwt" {>= "7.1.0"}
+  "conduit-lwt-unix" {>= "7.1.0"}
   "fmt" {>= "0.8.2"}
   "base-unix"
   "ppx_sexp_conv" {>= "v0.13.0"}
diff --git a/cohttp-lwt-unix/examples/client_lwt_proxy.ml b/cohttp-lwt-unix/examples/client_lwt_proxy.ml
new file mode 100644 (file)
index 0000000..0293003
--- /dev/null
@@ -0,0 +1,145 @@
+open Lwt
+open Cohttp
+open Cohttp_lwt_unix
+
+let () =
+  if not @@ Debug.debug_active () then (
+    Fmt_tty.setup_std_outputs ~style_renderer:`Ansi_tty ~utf_8:true ();
+    Logs.set_level ~all:true (Some Logs.Debug);
+    Logs.set_reporter Debug.default_reporter)
+
+let proxy_uri = ref None
+let uri = ref []
+let proxy_authorization = ref None
+let set_proxy_uri uri = proxy_uri := Some (Uri.of_string uri)
+
+let set_proxy_authorization auth =
+  proxy_authorization :=
+    Some (Cohttp.Auth.credential_of_string ("Basic " ^ Base64.encode_exn auth))
+
+let usage_msg =
+  {|Usage: test_client_proxy -proxy <uri> <resource>
+Examples:
+$ test_client_proxy -proxy http://localhost:8080 http://example.com
+$ test_client_proxy -proxy https://localhost:8080 https://example.com
+Options:|}
+
+let anon_fun args = uri := !uri @ [ args ]
+
+let speclist =
+  [
+    ("-proxy", Arg.String set_proxy_uri, "<uri>  Proxy uri");
+    ("-proxyauth", Arg.String set_proxy_authorization, " Proxy authorization");
+  ]
+
+(* Boilerplate code to handle redirects *)
+
+let rec http_get_and_follow ~max_redirects ?headers uri =
+  let open Lwt.Syntax in
+  let* ans = Cohttp_lwt_unix.Client.get ?headers uri in
+  follow_redirect ~max_redirects ?headers uri ans
+
+and follow_redirect ~max_redirects ?headers request_uri (response, body) =
+  let open Lwt.Syntax in
+  let status = Http.Response.status response in
+  (* The unconsumed body would otherwise leak memory *)
+  let* () =
+    if status <> `OK then Cohttp_lwt.Body.drain_body body else Lwt.return_unit
+  in
+  match status with
+  | `OK -> Lwt.return (response, body)
+  | `Permanent_redirect | `Moved_permanently ->
+      handle_redirect ~permanent:true ~max_redirects ?headers request_uri
+        response
+  | `Found | `Temporary_redirect ->
+      handle_redirect ~permanent:false ~max_redirects ?headers request_uri
+        response
+  | `Not_found | `Gone -> failwith "Not found"
+  | status ->
+      Printf.ksprintf failwith "Unhandled status: %s"
+        (Cohttp.Code.string_of_status status)
+
+and handle_redirect ~permanent ~max_redirects ?headers request_uri response =
+  if max_redirects <= 0 then failwith "Too many redirects"
+  else
+    let headers' = Http.Response.headers response in
+    let location = Http.Header.get headers' "location" in
+    match location with
+    | None -> failwith "Redirection without Location header"
+    | Some url ->
+        let open Lwt.Syntax in
+        let uri = Uri.of_string url in
+        let* () =
+          if permanent then
+            Logs_lwt.warn (fun m ->
+                m "Permanent redirection from %s to %s"
+                  (Uri.to_string request_uri)
+                  url)
+          else Lwt.return_unit
+        in
+        http_get_and_follow ?headers uri ~max_redirects:(max_redirects - 1)
+
+(* Interesting stuff *)
+
+let getenv_opt k =
+  match Sys.getenv_opt k with
+  | Some v -> Some (k, Uri.of_string v)
+  | None -> None
+
+let getenv_opt_case k =
+  match getenv_opt (String.lowercase_ascii k) with
+  | None -> getenv_opt (String.uppercase_ascii k)
+  | v -> v
+
+let main ~proxy ~uri ~credential () =
+  let all_proxy, no_proxy, scheme_proxy =
+    match proxy with
+    | None ->
+        ( Option.map Uri.of_string (Sys.getenv_opt "ALL_PROXY"),
+          Sys.getenv_opt "NO_PROXY",
+          [
+            getenv_opt_case "httpunix_proxy";
+            getenv_opt_case "https_proxy";
+            getenv_opt "http_proxy";
+          ]
+          |> List.filter_map (function
+               | Some (k, v) -> Some (String.(sub k 0 (rindex k '_')), v)
+               | n -> n) )
+    | v -> (v, None, [])
+  in
+
+  let proxy_headers =
+    Option.map
+      (fun credential ->
+        Http.Header.init_with "Proxy-Authorization"
+          (Cohttp.Auth.string_of_credential credential))
+      credential
+  in
+
+  let module Cache = Cohttp_lwt_unix.Connection_proxy in
+  let cache =
+    Cache.create ?all_proxy ~scheme_proxy ?no_proxy ?proxy_headers ()
+  in
+  Client.set_cache (Cache.call cache);
+
+  http_get_and_follow ~max_redirects:2 (Uri.of_string uri)
+  >>= fun (resp, body) ->
+  let code = resp |> Response.status |> Code.code_of_status in
+  Printf.printf "Response code: %d\n" code;
+  Printf.printf "Headers: %s\n" (resp |> Response.headers |> Header.to_string);
+  body |> Cohttp_lwt.Body.to_string >|= fun body ->
+  Printf.printf "Body of length: %d\n" (String.length body);
+  print_endline ("Received body\n" ^ body)
+
+(* Argument parsing *)
+
+let () =
+  Arg.parse speclist anon_fun usage_msg;
+  if List.length !uri <> 1 then (
+    prerr_endline "Expected a single resource uri.";
+    prerr_endline usage_msg;
+    exit 1);
+  let proxy = !proxy_uri
+  and uri = List.hd !uri
+  and credential = !proxy_authorization in
+  Lwt_main.run (main ~proxy ~uri ~credential ())
index c323e76d54293c97946bb95a9af6f0f18b5b2172..b596cd6b3855bd8f3ed67b57a0cec10566c30b8c 100644 (file)
@@ -1,8 +1,13 @@
 (executables
- (names client_lwt client_lwt_timeout docker_lwt server_lwt)
- (libraries cohttp-lwt-unix))
+ (names client_lwt client_lwt_timeout docker_lwt server_lwt client_lwt_proxy)
+ (libraries cohttp-lwt-unix fmt.tty))
 
 (alias
  (name runtest)
  (package cohttp-lwt-unix)
- (deps client_lwt.exe client_lwt_timeout.exe docker_lwt.exe server_lwt.exe))
+ (deps
+  client_lwt.exe
+  client_lwt_timeout.exe
+  docker_lwt.exe
+  server_lwt.exe
+  client_lwt_proxy.exe))
index 1202020392ca73d8f7ea75e6c47d4a798a03a577..db82391241e96411ce159b7810b0aee16f2b6fb4 100644 (file)
@@ -38,6 +38,14 @@ module Connection_cache =
       let sleep_ns ns = Lwt_unix.sleep (Int64.to_float ns /. 1_000_000_000.)
     end)
 
+module Connection_proxy =
+  Cohttp_lwt.Connection_cache.Make_proxy
+    (Connection)
+    (struct
+      (* : Mirage_time.S *)
+      let sleep_ns ns = Lwt_unix.sleep (Int64.to_float ns /. 1_000_000_000.)
+    end)
+
 module Client : sig
   (** The [Client] module implements the full UNIX HTTP client interface,
       including the UNIX-specific functions defined in {!C}. *)
index cd0edefab272af594fd8ee46345ea74f4444d1d8..6a49c8ed3c27bb2fed065b7ce8a1876c78aa1cd2 100644 (file)
@@ -35,15 +35,21 @@ let default_ctx =
     }
 
 type endp = Conduit.endp
+type client = Conduit_lwt_unix.client
 
 let resolve ~ctx uri = Resolver_lwt.resolve_uri ~uri ctx.resolver
 
-let connect_endp ~ctx:{ ctx; _ } endp =
-  Conduit_lwt_unix.endp_to_client ~ctx endp >>= fun client ->
+let tunnel hostname (channels : IO.ic * IO.oc) : client =
+  `TLS_tunnel (`Hostname hostname, (fst channels).chan, snd channels)
+
+let connect_client ~ctx:{ ctx; _ } client =
   Conduit_lwt_unix.connect ~ctx client >|= fun (flow, ic, oc) ->
   let ic = Input_channel.create ic in
   (flow, ic, oc)
 
+let connect_endp ~ctx endp =
+  Conduit_lwt_unix.endp_to_client ~ctx:ctx.ctx endp >>= connect_client ~ctx
+
 let connect_uri ~ctx uri = resolve ~ctx uri >>= connect_endp ~ctx
 
 let close c =
index c3f216b7eab7ab328c6b9567bc4b9e6f4a369dd4..42c09fd7e2862f213ebefec94cdcf1db1a17bd1d 100644 (file)
@@ -24,6 +24,7 @@ include
     with module IO = Io
      and type ctx := ctx
      and type endp = Conduit.endp
+     and type client = Conduit_lwt_unix.client
 
 val init : ?ctx:Conduit_lwt_unix.ctx -> ?resolver:Resolver_lwt.t -> unit -> ctx
 (** [init ?ctx ?resolver ()] is a network context that is the same as the
index 8ab5c34bd5213ad0e8682a24920b5f9bea40305f..47d7f6e24bc013044b5eedca26e045feecd4476e 100644 (file)
@@ -114,7 +114,10 @@ let test_client uri =
   assert_equal ~printer:Fun.id "Spring" body;
 
   (* simple request function accepting custom requests. *)
-  let handler ?headers ?body meth uri = Client.call ?headers ?body meth uri in
+  let handler ?headers ?body ?absolute_form meth uri =
+    ignore absolute_form;
+    Client.call ?headers ?body meth uri
+  in
   tests handler uri
 
 (* The Client.{call, get, put, ...} functions by default use a new
@@ -143,7 +146,8 @@ let test_non_persistent uri =
   (* the resolved endpoint may be buffered to avoid stressing the resolver: *)
   Connection.Net.resolve ~ctx:(Lazy.force Connection.Net.default_ctx) uri
   >>= fun endp ->
-  let handler ?headers ?body meth uri =
+  let handler ?headers ?body ?absolute_form meth uri =
+    ignore absolute_form;
     Connection.connect ~persistent:false endp >>= fun connection ->
     Connection.call connection ?headers ?body meth uri
   in
@@ -159,7 +163,8 @@ let test_unknown uri =
   Connection.connect ~persistent:false endp >>= fun c ->
   let connection = ref c in
   (* reference to open connection *)
-  let rec handler ?headers ?body meth uri =
+  let rec handler ?headers ?body ?absolute_form meth uri =
+    ignore absolute_form;
     Lwt.catch
       (fun () -> Connection.call !connection ?headers ?body meth uri)
       (function
index 90adbdfc86264d81dbcb88d27fef832f286e04d7..5c8202fd2333043c937c9fa9f957a72bd44bc78e 100644 (file)
@@ -50,22 +50,22 @@ let server =
       (fun _ _ ->
         Lwt.return
           (`Expert
-            (let headers =
-               Http.(
-                 Header.add_transfer_encoding (Header.init ()) Transfer.Chunked)
-             in
-             ( Http.Response.make ~headers (),
-               fun _ic oc -> Lwt_io.write oc "8\r\nexpert 1\r\n0\r\n\r\n" ))));
+             (let headers =
+                Http.(
+                  Header.add_transfer_encoding (Header.init ()) Transfer.Chunked)
+              in
+              ( Http.Response.make ~headers (),
+                fun _ic oc -> Lwt_io.write oc "8\r\nexpert 1\r\n0\r\n\r\n" ))));
       (fun _ _ ->
         Lwt.return
           (`Expert
-            ( (* Alternatively, cohttp.response.make injects the Chunked encoding when no
+             ( (* Alternatively, cohttp.response.make injects the Chunked encoding when no
                  encoding is already in the headers. *)
-              Cohttp.Response.make (),
-              fun ic oc ->
-                Lwt_io.write oc "8\r\nexpert 2\r\n0\r\n\r\n" >>= fun () ->
-                Lwt_io.flush oc >>= fun () ->
-                Cohttp_lwt_unix.Private.Input_channel.close ic )));
+               Cohttp.Response.make (),
+               fun ic oc ->
+                 Lwt_io.write oc "8\r\nexpert 2\r\n0\r\n\r\n" >>= fun () ->
+                 Lwt_io.flush oc >>= fun () ->
+                 Cohttp_lwt_unix.Private.Input_channel.close ic )));
     ]
   |> response_sequence
 
index 3624545fd8c8fb1a146a92a4f95b4defb6a04503..487026e6626a3616802a2f4c56cbdb3d3543eb04 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP implementation using the Lwt concurrency library"
@@ -32,6 +32,7 @@ depends: [
   "cohttp" {= version}
   "lwt" {>= "5.4.0"}
   "sexplib0"
+  "ipaddr" {>= "5.6.0"}
   "ppx_sexp_conv" {>= "v0.13.0"}
   "logs"
   "uri" {>= "2.0.0"}
index 879ed0810bb50d8c92b43d9d809a68c3118ac0b0..0c454a724cfbfa186f37a0df49fcbbdbf7cd6267 100644 (file)
@@ -33,6 +33,7 @@ module Make (Net : S.Net) : S.Connection with module Net = Net = struct
   (* enable warning when https://github.com/mirage/ocaml-conduit/pull/319 is released *)
 
   type req_resr = {
+    absolute_form : bool;
     uri : Uri.t;
     meth : Cohttp.Code.meth;
     headers : Header.t;
@@ -164,12 +165,15 @@ module Make (Net : S.Net) : S.Connection with module Net = Net = struct
             queue_fail connection connection.in_flight e;
             Lwt.return_unit)
 
-  let call connection ?headers ?(body = `Empty) meth uri =
+  let call connection ?headers ?(body = `Empty) ?(absolute_form = false) meth
+      uri =
     let headers = match headers with Some h -> h | None -> Header.init () in
     match connection.state with
     | Connecting _ | Full _ ->
         let res, res_r = Lwt.wait () in
-        Queue.push { uri; meth; headers; body; res_r } connection.waiting;
+        Queue.push
+          { absolute_form; uri; meth; headers; body; res_r }
+          connection.waiting;
         Lwt_condition.broadcast connection.condition ();
         res
     | Closing _ | Half _ | Closed | Failed _ -> raise Retry
@@ -193,7 +197,7 @@ module Make (Net : S.Net) : S.Connection with module Net = Net = struct
       *)
         Lwt.return_unit
     | Full (ic, oc) | Closing (ic, oc) ->
-        let ({ uri; meth; headers; body; res_r } as work) =
+        let ({ absolute_form; uri; meth; headers; body; res_r } as work) =
           Queue.take connection.waiting
         in
 
@@ -222,7 +226,7 @@ module Make (Net : S.Net) : S.Connection with module Net = Net = struct
           else headers
         in
 
-        let req = Request.make ~encoding ~meth ~headers uri in
+        let req = Request.make ~encoding ~meth ~headers ~absolute_form uri in
 
         Queue.push work connection.in_flight;
 
@@ -293,6 +297,34 @@ module Make (Net : S.Net) : S.Connection with module Net = Net = struct
       on_failure;
     connection
 
+  let create_tunnel ?(finalise = fun _ -> Lwt.return_unit)
+      ?(ctx = Lazy.force Net.default_ctx) proxy remote_host =
+    match proxy.state with
+    | Full (ic, oc) ->
+        let client = Net.tunnel remote_host (ic, oc) in
+        let channels =
+          Net.connect_client ~ctx client >>= fun (_, ic, oc) -> return (ic, oc)
+        in
+        let connection =
+          {
+            finalise;
+            in_flight = Queue.create ();
+            waiting = Queue.create ();
+            state = Connecting channels;
+            condition = Lwt_condition.create ();
+            persistent = `True;
+          }
+        in
+        let on_failure e = connection.state <- Failed e in
+        Lwt.on_any channels
+          (fun channels ->
+            connection.state <- Full channels;
+            Lwt.dont_wait (fun () -> reader connection) on_failure;
+            Lwt.dont_wait (fun () -> writer connection) on_failure)
+          on_failure;
+        connection
+    | _ -> failwith "Proxy connection wasn't in right state."
+
   let connect ?finalise ?persistent ?ctx uri =
     let connection = create ?finalise ?persistent ?ctx uri in
     match connection.state with
index 0dd46432d71dccc37e15b179649a89a729c2adfd..695c520333f54561155d05c64248f9e326fdcbbf 100644 (file)
@@ -1,15 +1,6 @@
 exception Retry = Connection.Retry
 
-(** This functor establishes a new connection for each request. *)
-module Make_no_cache (Connection : S.Connection) : sig
-  include S.Connection_cache
-
-  val create : ?ctx:Connection.Net.ctx -> unit -> t
-  (** [create ?ctx ()] creates a connection for handling a single request. The
-      connection accepts only a single request and will automatically be closed
-      as soon as possible.
-      @param ctx See {!Connection.Net.ctx} *)
-end = struct
+module Make_no_cache (Connection : S.Connection) = struct
   module Net = Connection.Net
   module IO = Net.IO
   open IO
@@ -18,12 +9,15 @@ end = struct
 
   let call = Fun.id
 
-  let create ?(ctx = Lazy.force Net.default_ctx) () ?headers ?body meth uri =
+  let create ?(ctx = Lazy.force Net.default_ctx) () ?headers ?body
+      ?absolute_form meth uri =
     Net.resolve ~ctx uri
     (* TODO: Support chunked encoding without ~persistent:true ? *)
     >>= Connection.connect ~ctx ~persistent:true
     >>= fun connection ->
-    let res = Connection.call connection ?headers ?body meth uri in
+    let res =
+      Connection.call connection ?headers ?body ?absolute_form meth uri
+    in
     (* this can be simplified when https://github.com/mirage/ocaml-conduit/pull/319 is released. *)
     Lwt.dont_wait
       (fun () ->
@@ -38,38 +32,7 @@ end = struct
     res
 end
 
-(** This functor keeps a cache of connections for reuse. Connections are reused
-    based on their remote {!type:Conduit.endp} (effectively IP / port). *)
-module Make (Connection : S.Connection) (Sleep : S.Sleep) : sig
-  include S.Connection_cache
-
-  val create :
-    ?ctx:Connection.Net.ctx ->
-    ?keep:int64 ->
-    ?retry:int ->
-    ?parallel:int ->
-    ?depth:int ->
-    unit ->
-    t
-  (** Create a new connection cache
-
-      @param ctx Conduit context to use. See {!type:Connection.Net.ctx}.
-      @param keep Number of nanoseconds to keep an idle connection around.
-      @param retry
-        Number of times a {e gracefully} failed request is automatically
-        retried. {e graceful} means failed with {!exception:Connection.Retry}.
-        Requests with a [`Stream] {!module:Body} cannot be retried
-        automatically. Such requests will fail with
-        {!exception:Connection.Retry} and a new {!module:Body} will need to be
-        provided to retry.
-      @param parallel
-        maximum number of connections to establish to a single endpoint. Beware:
-        A single hostname may resolve to multiple endpoints. In such a case
-        connections may be created in excess to what was intended.
-      @param depth
-        maximum number of requests to queue and / or send on a single
-        connection. *)
-end = struct
+module Make (Connection : S.Connection) (Sleep : S.Sleep) = struct
   module Net = Connection.Net
   module IO = Net.IO
   open IO
@@ -83,10 +46,11 @@ end = struct
     retry : int;
     parallel : int;
     depth : int;
+    proxy : Uri.t option;
   }
 
   let create ?(ctx = Lazy.force Net.default_ctx) ?(keep = 60_000_000_000L)
-      ?(retry = 2) ?(parallel = 4) ?(depth = 100) () =
+      ?(retry = 2) ?(parallel = 4) ?(depth = 100) ?proxy () =
     {
       cache = Hashtbl.create ~random:true 10;
       ctx;
@@ -94,6 +58,7 @@ end = struct
       retry;
       parallel;
       depth;
+      proxy;
     }
 
   let rec get_connection self endp =
@@ -109,7 +74,8 @@ end = struct
       Lwt.return_unit
     in
     let create () =
-      let connection = Connection.create ~finalise ~ctx:self.ctx endp
+      let connection =
+        Connection.create ~persistent:true ~finalise ~ctx:self.ctx endp
       and timeout = ref Lwt.return_unit in
       let rec busy () =
         Lwt.cancel !timeout;
@@ -152,12 +118,31 @@ end = struct
               (fun _ -> get_connection self endp)
               (fun _ -> get_connection self endp))
 
-  let call self ?headers ?body meth uri =
-    Net.resolve ~ctx:self.ctx uri >>= fun endp ->
+  let prepare self ?headers ?absolute_form meth uri =
+    match self.proxy with
+    | None ->
+        let absolute_form = Option.value ~default:false absolute_form in
+        Net.resolve ~ctx:self.ctx uri >>= fun endp ->
+        Lwt.return (endp, absolute_form, headers)
+    | Some proxy_uri ->
+        let absolute_form =
+          Option.value
+            ~default:
+              (not
+                 (meth = `CONNECT
+                 || (meth = `OPTIONS && Uri.path_and_query uri = "*")))
+            absolute_form
+        in
+        Net.resolve ~ctx:self.ctx proxy_uri >>= fun endp ->
+        Lwt.return (endp, absolute_form, headers)
+
+  let call self ?headers ?body ?absolute_form meth uri =
+    prepare self ?headers ?absolute_form meth uri
+    >>= fun (endp, absolute_form, headers) ->
     let rec request retry =
       get_connection self endp >>= fun conn ->
       Lwt.catch
-        (fun () -> Connection.call conn ?headers ?body meth uri)
+        (fun () -> Connection.call conn ?headers ?body ~absolute_form meth uri)
         (function
           | Retry -> (
               match body with
@@ -168,3 +153,310 @@ end = struct
     in
     request self.retry
 end
+
+module Make_tunnel (Connection : S.Connection) (Sleep : S.Sleep) : sig
+  include S.Connection_cache
+
+  val create :
+    ?ctx:Connection.Net.ctx ->
+    ?keep:int64 ->
+    ?retry:int ->
+    ?parallel:int ->
+    ?depth:int ->
+    ?proxy_headers:Http.Header.t ->
+    Uri.t ->
+    unit ->
+    t
+end = struct
+  module Net = Connection.Net
+  module IO = Net.IO
+  open IO
+
+  type ctx = Net.ctx
+  type tunnel = { proxy : Connection.t; remote : Connection.t }
+
+  type t = {
+    cache : (string, tunnel) Hashtbl.t; (* remote host * tunnel *)
+    proxy_uri : Uri.t;
+    mutable proxy_endp : Net.endp option;
+    proxy_headers : Http.Header.t;
+    ctx : ctx;
+    keep : int64;
+    retry : int;
+    parallel : int;
+    depth : int;
+  }
+
+  let proxy_default_scheme uri =
+    match Uri.scheme uri with
+    | None -> Uri.with_scheme uri (Some "http")
+    | _ -> uri
+
+  let create ?(ctx = Lazy.force Net.default_ctx) ?(keep = 60_000_000_000L)
+      ?(retry = 2) ?(parallel = 4) ?(depth = 100)
+      ?(proxy_headers = Http.Header.init ()) proxy_uri () =
+    if Uri.host proxy_uri = None then
+      Printf.ksprintf invalid_arg "No host was provided in URI %s."
+        (Uri.to_string proxy_uri);
+    {
+      cache = Hashtbl.create ~random:true 10;
+      proxy_uri = proxy_default_scheme proxy_uri;
+      proxy_endp = None;
+      proxy_headers;
+      ctx;
+      keep;
+      retry;
+      parallel;
+      depth;
+    }
+
+  let rec request conn ?headers ?body ?absolute_form meth uri retry =
+    Lwt.catch
+      (fun () -> Connection.call conn ?headers ?body ?absolute_form meth uri)
+      (function
+        | Retry -> (
+            match body with
+            | Some (`Stream _) -> Lwt.fail Retry
+            | None | Some `Empty | Some (`String _) | Some (`Strings _) ->
+                if retry <= 0 then Lwt.fail Retry
+                else
+                  request conn ?headers ?body ?absolute_form meth uri (retry - 1)
+            )
+        | e -> Lwt.fail e)
+
+  let rec get_connection self ~proxy_endp ~remote ~remote_host ~remote_uri =
+    let finalise connection =
+      let rec remove keep =
+        let current = Hashtbl.find self.cache remote in
+        Hashtbl.remove self.cache remote;
+        if current.proxy == connection || current.remote == connection then
+          List.iter (Hashtbl.add self.cache remote) keep
+        else remove (current :: keep)
+      in
+      remove [];
+      Lwt.return_unit
+    in
+    let create () =
+      let proxy =
+        Connection.create ~persistent:true ~finalise ~ctx:self.ctx proxy_endp
+      in
+      request proxy ~headers:self.proxy_headers `CONNECT remote_uri self.retry
+      >>= fun (resp, _body) ->
+      let code = resp |> Http.Response.status |> Cohttp.Code.code_of_status in
+      if not (Cohttp.Code.is_success code) then
+        Printf.ksprintf failwith "Could not setup tunnel. Response code: %d\n"
+          code;
+      let remote =
+        Connection.create_tunnel ~finalise ~ctx:self.ctx proxy remote_host
+      and timeout = ref Lwt.return_unit in
+      let rec busy () =
+        Lwt.cancel !timeout;
+        if Connection.length remote = 0 then (
+          timeout :=
+            Sleep.sleep_ns self.keep >>= fun () ->
+            Connection.close remote;
+            Connection.close proxy;
+            (* failure is ignored *)
+            Lwt.return_unit);
+        Lwt.on_termination (Connection.notify remote) busy
+      in
+      busy ();
+      Lwt.return { proxy; remote }
+    in
+    match Hashtbl.find_all self.cache remote with
+    | [] ->
+        create () >>= fun tunnel ->
+        Hashtbl.add self.cache remote tunnel;
+        Lwt.return tunnel
+    | tunnels -> (
+        let rec search length = function
+          | [ a ] -> (a, length + 1)
+          | a :: b :: tl
+            when Connection.length a.remote < Connection.length b.remote ->
+              search (length + 1) (a :: tl)
+          | _ :: tl -> search (length + 1) tl
+          | [] -> assert false
+        in
+        match search 0 tunnels with
+        | shallowest, _ when Connection.length shallowest.remote = 0 ->
+            Lwt.return shallowest
+        | _, length when length < self.parallel ->
+            create () >>= fun tunnel ->
+            Hashtbl.add self.cache remote tunnel;
+            Lwt.return tunnel
+        | shallowest, _ when Connection.length shallowest.remote < self.depth ->
+            Lwt.return shallowest
+        | _ ->
+            Lwt.try_bind
+              (fun () ->
+                Lwt.choose
+                  (List.map
+                     (fun { remote; _ } -> Connection.notify remote)
+                     tunnels))
+              (fun _ ->
+                get_connection self ~proxy_endp ~remote ~remote_host ~remote_uri)
+              (fun _ ->
+                get_connection self ~proxy_endp ~remote ~remote_host ~remote_uri)
+        )
+
+  let call self ?headers ?body ?absolute_form meth uri =
+    (match self.proxy_endp with
+    | None ->
+        Net.resolve ~ctx:self.ctx self.proxy_uri >>= fun proxy_endp ->
+        self.proxy_endp <- Some proxy_endp;
+        Lwt.return proxy_endp
+    | Some proxy_endp -> Lwt.return proxy_endp)
+    >>= fun proxy_endp ->
+    let remote_port =
+      match Uri_services.tcp_port_of_uri uri with
+      | Some p -> p
+      | None -> failwith "A port is required for the CONNECT method."
+    in
+    let remote_host = Option.get (Uri.host uri) in
+    let remote = remote_host ^ ":" ^ string_of_int remote_port
+    and remote_uri = Uri.with_port uri (Some remote_port) in
+    get_connection self ~proxy_endp ~remote ~remote_host ~remote_uri
+    >>= fun tunnel ->
+    request tunnel.remote ?headers ?body ?absolute_form meth uri self.retry
+end
+
+type no_proxy_pattern = Name of string | Ipaddr_prefix of Ipaddr.Prefix.t
+type no_proxy = Wildcard | Patterns of no_proxy_pattern list
+
+let trim_dots ~first_leading s =
+  let len = String.length s in
+  let i = ref 0 in
+  if first_leading && !i < len && String.unsafe_get s !i = '.' then incr i;
+  let j = ref (len - 1) in
+  while !j >= !i && String.unsafe_get s !j = '.' do
+    decr j
+  done;
+  if !j >= !i then String.sub s !i (!j - !i + 1) else ""
+
+let strncasecompare a b n =
+  let a = String.(sub a 0 (min (length a) n) |> lowercase_ascii)
+  and b = String.(sub b 0 (min (length b) n) |> lowercase_ascii) in
+  String.compare a b = 0
+
+let no_proxy_from_env no_proxy =
+  if no_proxy = "*" then Wildcard
+  else
+    let patterns =
+      no_proxy
+      |> String.split_on_char ','
+      |> List.filter_map (fun pattern ->
+             if pattern = "" then None else Some (String.trim pattern))
+      |> List.map (fun pattern ->
+             match Ipaddr.of_string pattern with
+             | Ok addr -> Ipaddr_prefix (Ipaddr.Prefix.of_addr addr)
+             | Error _ -> (
+                 match Ipaddr.Prefix.of_string pattern with
+                 | Ok prefix -> Ipaddr_prefix prefix
+                 | Error _ -> Name (trim_dots ~first_leading:true pattern)))
+    in
+    Patterns patterns
+
+let check_no_proxy_patterns host = function
+  | Wildcard -> true
+  | _ when String.length host = 0 -> true
+  | Patterns patterns -> (
+      match Ipaddr.of_string host with
+      | Ok hostip ->
+          List.exists
+            (function
+              | Name _ -> false
+              | Ipaddr_prefix network -> Ipaddr.Prefix.mem hostip network)
+            patterns
+      | Error _ ->
+          let name = trim_dots ~first_leading:false host in
+          List.exists
+            (function
+              | Ipaddr_prefix _ -> false
+              | Name pattern ->
+                  let patternlen = String.length pattern
+                  and namelen = String.length name in
+                  if patternlen = namelen then
+                    strncasecompare pattern name namelen
+                  else if patternlen < namelen then
+                    name.[namelen - patternlen - 1] = '.'
+                    && strncasecompare pattern
+                         (String.sub name (namelen - patternlen)
+                            (patternlen - namelen - patternlen))
+                         patternlen
+                  else false)
+            patterns)
+
+let tunnel_schemes = [ "https" ]
+
+module Make_proxy (Connection : S.Connection) (Sleep : S.Sleep) = struct
+  module Connection_cache = Make (Connection) (Sleep)
+  module Connection_tunnel = Make_tunnel (Connection) (Sleep)
+
+  type proxy = Direct of Connection_cache.t | Tunnel of Connection_tunnel.t
+
+  type t = {
+    proxies : (string * proxy) list;
+    direct : proxy option;
+    tunnel : proxy option;
+    no_proxy : Connection_cache.t;
+    no_proxy_patterns : no_proxy;
+  }
+
+  let create ?ctx ?keep ?retry ?parallel ?depth ?(scheme_proxy = []) ?all_proxy
+      ?no_proxy ?proxy_headers () =
+    let create_default () =
+      Connection_cache.create ?ctx ?keep ?retry ?parallel ?depth ()
+    and create_direct proxy =
+      Connection_cache.create ?ctx ?keep ?retry ?parallel ?depth ~proxy ()
+    and create_tunnel proxy_uri =
+      Connection_tunnel.create ?ctx ?keep ?retry ?parallel ?depth ?proxy_headers
+        proxy_uri ()
+    in
+    let no_proxy_patterns =
+      match no_proxy with
+      | None -> Patterns []
+      | Some no_proxy -> no_proxy_from_env no_proxy
+    in
+    let no_proxy = create_default () in
+    let proxies =
+      List.map
+        (fun (scheme, uri) ->
+          let proxy =
+            if List.mem scheme tunnel_schemes then Tunnel (create_tunnel uri)
+            else Direct (create_direct uri)
+          in
+          (scheme, proxy))
+        scheme_proxy
+    in
+    let direct, tunnel =
+      match all_proxy with
+      | Some uri ->
+          (Some (Direct (create_direct uri)), Some (Tunnel (create_tunnel uri)))
+      | None -> (None, None)
+    in
+    { no_proxy; direct; tunnel; proxies; no_proxy_patterns }
+
+  let call self ?headers ?body ?absolute_form meth uri =
+    let proxy =
+      if
+        check_no_proxy_patterns
+          (Uri.host_with_default ~default:"" uri)
+          self.no_proxy_patterns
+      then None
+        (* Connection_cache.call self.no_proxy ?headers ?body ?absolute_form meth uri *)
+      else
+        let scheme = Option.value ~default:"" (Uri.scheme uri) in
+        match List.assoc scheme self.proxies with
+        | proxy -> Some proxy
+        | exception Not_found ->
+            if List.mem scheme tunnel_schemes then self.tunnel else self.direct
+    in
+    match proxy with
+    | None ->
+        Connection_cache.call self.no_proxy ?headers ?body ?absolute_form meth
+          uri
+    | Some (Tunnel proxy) ->
+        Connection_tunnel.call proxy ?headers ?body ?absolute_form meth uri
+    | Some (Direct proxy) ->
+        Connection_cache.call proxy ?headers ?body ?absolute_form meth uri
+end
diff --git a/cohttp-lwt/src/connection_cache.mli b/cohttp-lwt/src/connection_cache.mli
new file mode 100644 (file)
index 0000000..60708df
--- /dev/null
@@ -0,0 +1,96 @@
+(** This functor establishes a new connection for each request. *)
+module Make_no_cache (Connection : S.Connection) : sig
+  include S.Connection_cache
+
+  val create : ?ctx:Connection.Net.ctx -> unit -> t
+  (** [create ?ctx ()] creates a connection for handling a single request. The
+      connection accepts only a single request and will automatically be closed
+      as soon as possible.
+      @param ctx See {!Connection.Net.ctx} *)
+end
+
+(** This functor keeps a cache of connections for reuse. Connections are reused
+    based on their remote {!type:Conduit.endp} (effectively IP / port). *)
+module Make (Connection : S.Connection) (Sleep : S.Sleep) : sig
+  include S.Connection_cache
+
+  val create :
+    ?ctx:Connection.Net.ctx ->
+    ?keep:int64 ->
+    ?retry:int ->
+    ?parallel:int ->
+    ?depth:int ->
+    ?proxy:Uri.t ->
+    unit ->
+    t
+  (** Create a new connection cache
+
+      @param ctx Conduit context to use. See {!type:Connection.Net.ctx}.
+      @param keep Number of nanoseconds to keep an idle connection around.
+      @param retry
+        Number of times a {e gracefully} failed request is automatically
+        retried. {e graceful} means failed with {!exception:Connection.Retry}.
+        Requests with a [`Stream] {!module:Body} cannot be retried
+        automatically. Such requests will fail with
+        {!exception:Connection.Retry} and a new {!module:Body} will need to be
+        provided to retry.
+      @param parallel
+        maximum number of connections to establish to a single endpoint. Beware:
+        A single hostname may resolve to multiple endpoints. In such a case
+        connections may be created in excess to what was intended.
+      @param depth
+        maximum number of requests to queue and / or send on a single
+        connection.
+      @param proxy A direct (non-tunneling) proxy to use. *)
+end
+[@@warning "-unused-functor-parameter"]
+
+(** This functor keeps a cache of connections for reuse. Connections are reused
+    based on their remote {!type:Conduit.endp} (effectively IP / port). It also
+    supports automatically connecting and reconnecting to direct and tunneling
+    proxies, based on the remote URI scheme (HTTP will select direct proxies,
+    HTTPS tunneling proxies). *)
+module Make_proxy (Connection : S.Connection) (Sleep : S.Sleep) : sig
+  include S.Connection_cache
+
+  val create :
+    ?ctx:Connection.Net.ctx ->
+    ?keep:int64 ->
+    ?retry:int ->
+    ?parallel:int ->
+    ?depth:int ->
+    ?scheme_proxy:(string * Uri.t) list ->
+    ?all_proxy:Uri.t ->
+    ?no_proxy:string ->
+    ?proxy_headers:Http.Header.t ->
+    unit ->
+    t
+  (** Create a new connection cache. The outer connections to the proxy and the
+      inner connections share the same parameters.
+
+      @param ctx Conduit context to use. See {!type:Connection.Net.ctx}.
+      @param keep Number of nanoseconds to keep an idle connection around.
+      @param retry
+        Number of times a {e gracefully} failed request is automatically
+        retried. {e graceful} means failed with {!exception:Connection.Retry}.
+        Requests with a [`Stream] {!module:Body} cannot be retried
+        automatically. Such requests will fail with
+        {!exception:Connection.Retry} and a new {!module:Body} will need to be
+        provided to retry.
+      @param parallel
+        maximum number of connections to establish to a single endpoint. Beware:
+        A single hostname may resolve to multiple endpoints. In such a case
+        connections may be created in excess to what was intended.
+      @param depth
+        maximum number of requests to queue and / or send on a single
+        connection.
+      @param scheme_proxy The proxy URI associated to each (remote) scheme.
+      @param all_proxy
+        The default proxy to use. Proxy for specific schemes have precedence
+        over this.
+      @param no_proxy
+        Disable proxies for specific hosts, specified as curl's [NO_PROXY].
+      @see <https://everything.curl.dev/usingcurl/proxies/env.html#no-proxy>
+      @param proxy_headers Headers to pass to the proxy. *)
+end
+[@@warning "-unused-functor-parameter"]
index 12c5df2906893c2b1704184dee5059ab10682f2f..d2d062103a071de28f1098198d914c452fcdcadc 100644 (file)
@@ -4,4 +4,4 @@
  (synopsis "Lwt backend")
  (preprocess
   (pps ppx_sexp_conv))
- (libraries lwt uri http_bytebuffer cohttp logs logs.lwt))
+ (libraries lwt uri uri.services http_bytebuffer cohttp logs logs.lwt ipaddr))
index 18d8f7cabf56e117ac04eaf70f717259436f1fa3..1043c55ee38353efd1f6685e6a57e583f3929d82 100644 (file)
@@ -20,6 +20,7 @@ end
 module type Net = sig
   module IO : IO
 
+  type client
   type endp
 
   type ctx [@@deriving sexp_of]
@@ -40,6 +41,8 @@ module type Net = sig
   (** [resolve ~ctx uri] resolves [uri] into an endpoint description. This is
       [Resolver_lwt.resolve_uri ~uri ctx.resolver]. *)
 
+  val tunnel : string -> IO.ic * IO.oc -> client
+
   val connect_uri : ctx:ctx -> Uri.t -> (IO.conn * IO.ic * IO.oc) IO.t
   (** [connect_uri ~ctx uri] starts a {i flow} on the given [uri]. The choice of
       the protocol (with or without encryption) is done by the {i scheme} of the
@@ -62,6 +65,7 @@ module type Net = sig
   (** [connect_endp ~ctx endp] starts a {i flow} to the given [endp]. [endp]
       describes address and protocol of the endpoint to connect to. *)
 
+  val connect_client : ctx:ctx -> client -> (IO.conn * IO.ic * IO.oc) IO.t
   val close_in : IO.ic -> unit
   val close_out : IO.oc -> unit
   val close : IO.ic -> IO.oc -> unit
@@ -76,6 +80,7 @@ end
 type call =
   ?headers:Http.Header.t ->
   ?body:Body.t ->
+  ?absolute_form:bool ->
   Http.Method.t ->
   Uri.t ->
   (Cohttp.Response.t * Body.t) Lwt.t
@@ -89,15 +94,14 @@ type call =
       module can be used to consume [response_body]. Use {!Body.drain_body} if
       you don't consume the body by other means.
 
-      Leaks are detected by the GC and logged as debug messages, these can be
-      enabled activating the debug logging. For example, this can be done as
-      follows in
-      [cohttp-lwt-unix]
+    Leaks are detected by the GC and logged as debug messages, these can be
+    enabled activating the debug logging. For example, this can be done as
+    follows in [cohttp-lwt-unix]
 
-      {[
-        Cohttp_lwt_unix.Debug.activate_debug ();
-        Logs.set_level (Some Logs.Warning)
-      ]}
+    {[
+      Cohttp_lwt_unix.Debug.activate_debug ();
+      Logs.set_level (Some Logs.Warning)
+    ]}
 
     @raise {!Connection.Retry}
       on recoverable errors like the remote endpoint closing the connection
@@ -138,6 +142,9 @@ module type Connection = sig
       @param ctx See [Net.ctx]
       @param endp The remote address, port and protocol to connect to. *)
 
+  val create_tunnel :
+    ?finalise:(t -> unit Net.IO.t) -> ?ctx:Net.ctx -> t -> string -> t
+
   val connect :
     ?finalise:(t -> unit Net.IO.t) ->
     ?persistent:bool ->
@@ -191,9 +198,9 @@ module type Client = sig
         {!val:Connection_cache.Make_no_cache.create} is used to resolve uri and
         create a dedicated connection with [ctx].
 
-        In most cases you should use the more specific helper calls in the
-        interface rather than invoke this function directly. See {!head}, {!get}
-        and {!post} for some examples. *)
+      In most cases you should use the more specific helper calls in the
+      interface rather than invoke this function directly. See {!head}, {!get}
+      and {!post} for some examples. *)
   include
     Cohttp.Generic.Client.S
       with type 'a io = 'a Lwt.t
index 334df048a155a4b1b3ad685d9d6a27458478edda..ef93290139d4f317d6d4c4e38c9f15824e6f4294 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP implementation for the MirageOS unikernel"
@@ -32,8 +32,8 @@ depends: [
   "ocaml" {>= "4.08"}
   "mirage-flow" {>= "2.0.0"}
   "mirage-channel" {>= "4.0.0"}
-  "conduit" {>= "2.0.2"}
-  "conduit-mirage" {>= "2.3.0"}
+  "conduit" {>= "8.0.0"}
+  "conduit-mirage" {>= "8.0.0"}
   "mirage-kv" {>= "3.0.0"}
   "lwt" {>= "2.4.3"}
   "cohttp-lwt" {= version}
index 15a00fe3d4af9f8e9e777276d3ffe5a6b5fd4138..1f06e74e19d2344289a7e1bb037959f1c3233a71 100644 (file)
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
- * cohttp v6.0.0
+ * cohttp v6.1.0
  *)
 
-module Make
-    (P : Mirage_clock.PCLOCK)
-    (R : Resolver_mirage.S)
-    (S : Conduit_mirage.S) =
-struct
-  module Net = Net.Make (P) (R) (S)
+module Make (R : Resolver_mirage.S) (S : Conduit_mirage.S) = struct
+  module Net = Net.Make (R) (S)
   module Connection = Cohttp_lwt.Connection.Make (Net)
   include Cohttp_lwt.Client.Make (Connection)
 
index a67cb2345de2561196459d1d3594d12c20525441..b4d1d9e117bc64fc22dbe8ca97a1ded4cd0516a3 100644 (file)
@@ -1,7 +1,4 @@
-module Make
-    (_ : Mirage_clock.PCLOCK)
-    (R : Resolver_mirage.S)
-    (S : Conduit_mirage.S) : sig
+module Make (R : Resolver_mirage.S) (S : Conduit_mirage.S) : sig
   module Connection : Cohttp_lwt.S.Connection
   include Cohttp_lwt.S.Client with type ctx = Connection.Net.ctx
 
index 5b2830d3f0cb2f7a37facf747c81745e628dcede..cfd7147d7ad2f20be9ad517cdb4638d43083974c 100644 (file)
@@ -14,7 +14,7 @@
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
- * cohttp v6.0.0
+ * cohttp v6.1.0
  *)
 
 open Lwt.Infix
index a6ea44e5f4b9ee9c997a44cb52b30a9ecfcef328..b9422cabce3d7e812eb7084945a8b2af1caec6db 100644 (file)
@@ -14,7 +14,7 @@
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
- * cohttp v6.0.0
+ * cohttp v6.1.0
  *)
 
 (** Cohttp IO implementation using Mirage channels. *)
index 72964fccb4c3b2505eefa4109940fda5b9ab47dc..83758883832307873b6dad4330fdab240f8b5a70 100644 (file)
@@ -1,11 +1,6 @@
-module Make
-    (P : Mirage_clock.PCLOCK)
-    (R : Resolver_mirage.S)
-    (S : Conduit_mirage.S) =
-struct
+module Make (R : Resolver_mirage.S) (S : Conduit_mirage.S) = struct
   module Channel = Mirage_channel.Make (S.Flow)
   module Input_channel = Input_channel.Make (Channel)
-  module Endpoint = Conduit_mirage.Endpoint (P)
   module IO = Io.Make (Channel)
   open IO
 
@@ -21,11 +16,15 @@ struct
     lazy { resolver = R.localhost; conduit = None; authenticator = None }
 
   type endp = Conduit.endp
+  type client
 
+  let tunnel = failwith "Unimplemented"
+  let connect_client = failwith "Unimplemented"
   let resolve ~ctx uri = R.resolve_uri ~uri ctx.resolver
 
   let connect_endp ~ctx endp =
-    Endpoint.client ?tls_authenticator:ctx.authenticator endp >>= fun client ->
+    Conduit_mirage.Endpoint.client ?tls_authenticator:ctx.authenticator endp
+    >>= fun client ->
     match ctx.conduit with
     | None -> failwith "conduit not initialised"
     | Some c ->
index 0a64dd7a51dc841e6ebb858b9c0aad4714e11af1..ee48f2fd7c5be3be9d3e2f1d51a97d80aff5e04e 100644 (file)
@@ -1,7 +1,4 @@
-module Make
-    (_ : Mirage_clock.PCLOCK)
-    (R : Resolver_mirage.S)
-    (S : Conduit_mirage.S) : sig
+module Make (R : Resolver_mirage.S) (S : Conduit_mirage.S) : sig
   type ctx = {
     resolver : R.t;
     conduit : S.t option;
index ccddd316b52846f038c024ec65c9c7fea3d19102..8b4addec7b12e4371f3539370345cd78df4255b5 100644 (file)
@@ -14,7 +14,7 @@
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
- * cohttp v6.0.0
+ * cohttp v6.1.0
  *)
 
 module Key = Mirage_kv.Key
index 4bd3f0ebc53cd9366b923ec4a3e0120bddd55009..6de49c76ca44e9d5495c12a2039acaef603370a7 100644 (file)
@@ -14,7 +14,7 @@
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
- * cohttp v6.0.0
+ * cohttp v6.1.0
  *)
 
 (** Serve static HTTP sites from a Mirage key-value store. *)
index 787420e5411907c9cfcc0a718af4b09440f29898..f462a7f58a63c95a49509d828f61536e0ba8a84f 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Lightweight Cohttp + Lwt based HTTP server"
index 2a6da71d4c3a15976c365162eb18cfcd7f14908c..054ac93d2fc182a7603591710efc53f617d01b98 100644 (file)
@@ -205,11 +205,11 @@ module Context = struct
     Int64.of_string_opt ("0x" ^ hex)
 
   let step_chunked :
-        'a.
-        t ->
-        f:(Body.Substring.t -> 'acc -> 'acc Lwt.t) ->
-        init:'acc ->
-        'acc option Lwt.t =
+      'a.
+      t ->
+      f:(Body.Substring.t -> 'acc -> 'acc Lwt.t) ->
+      init:'acc ->
+      'acc option Lwt.t =
    fun t ~f ~init ->
     Input_channel.read_line_opt t.ic >>= function
     | None -> Lwt.return_none (* TODO invalid input *)
index b0c76e315b413ba7ebfd77a5dad289a0590f5ac2..4eb68097a411847f116fa5fa6c415f460e080838 100644 (file)
@@ -61,7 +61,8 @@ module Body : sig
   (** [stream ?encoding f] respond with body generated by repeatedly applying
       [f]. When [f] returns [None], it will be considered terminated.
 
-      [?encoding] is the encoding to use. By default this is [Encoding.chunked]. *)
+      [?encoding] is the encoding to use. By default this is [Encoding.chunked].
+  *)
 end
 
 module Context : sig
index d1cbe2eb068f19c8d4b9472915800382c985d6ac..fe477861d24314acc8c3140beed088d1eeb91464 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP toplevel pretty printers for HTTP types"
index d4958c2bec1b2014d4ed4371e4edd9c16c400353..355227300d041305470a2527edd6a57e05cd44b1 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "An OCaml library for HTTP clients and servers"
index 6095fd36d196587715b54c600772f80337791a87..585502a576a562bf3ea6cb31ce7ee308b9e353e5 100644 (file)
@@ -50,7 +50,8 @@ type encoding = Accept_types.encoding =
 (** Basic language range tag. ["en-gb"] is represented as
     [Language ["en"; "gb"]].
 
-    @see <https://tools.ietf.org/html/rfc7231#section-5.3.5> the specification. *)
+    @see <https://tools.ietf.org/html/rfc7231#section-5.3.5> the specification.
+*)
 type language = Accept_types.language = Language of string list | AnyLanguage
 [@@deriving sexp]
 
index f645561f689b28038bdf0a84e3b7e35699f3ef8f..d6d9792afe116cbd3e660ecddd8757006ea0e251 100644 (file)
@@ -171,7 +171,8 @@ val reason_phrase_of_code : int -> string
 (** Give a description of the given int code. *)
 
 val is_informational : int -> bool
-(** Is the given int code belong to the class of "informational" return code ? *)
+(** Is the given int code belong to the class of "informational" return code ?
+*)
 
 val is_success : int -> bool
 (** Is the given int code belong to the class of "success" return code ? *)
index bdc14c034bcf14042754f377ced1932387fc54d8..34636e5a173586e42e7807bb5266e62618409f32 100644 (file)
@@ -17,7 +17,8 @@
   sexplib0
   stringext
   uri
-  uri-sexp))
+  uri-sexp
+  uri.services))
 
 (ocamllex accept_lexer)
 
index 7ed211509df7f0fbcb8d92af8181d51a7e0f4494..0405e2dfd389d1f933f31de139290ef5c94200c8 100644 (file)
@@ -40,7 +40,8 @@ val to_list : t -> (string * string) list
 (** [to_list h] converts HTTP headers [h] to a list. Order and case is
     preserved.
 
-    {e Invariant (with case insensitive comparison):} [to_list (of_list l) = l] *)
+    {e Invariant (with case insensitive comparison):} [to_list (of_list l) = l]
+*)
 
 val init_with : string -> string -> t
 (** [init_with k v] construct a fresh HTTP headers with a single header with
@@ -118,7 +119,8 @@ val get_multi_concat : ?list_value_only:bool -> t -> string -> string option
     the last value paired with [k] in [h].
 
     {e Invariant:}
-    [forall h, k not a list-value header. get_multi_concat ~list-value-only:true h k = get h k] *)
+    [forall h, k not a list-value header. get_multi_concat ~list-value-only:true
+     h k = get h k] *)
 
 val update : t -> string -> (string option -> string option) -> t
 (** [update h k f] returns an header list containing the same headers as [h],
@@ -190,7 +192,8 @@ val clean_dup : t -> t
 
     Finally, following
     {{:https://tools.ietf.org/html/rfc7230#section-3.2.2} RFC7230§3.2.2}, the
-    header [Set-cookie] is treated as an exception and ignored by [clean_dup]. *)
+    header [Set-cookie] is treated as an exception and ignored by [clean_dup].
+*)
 
 val get_content_range : t -> Int64.t option
 val get_media_type : t -> string option
@@ -213,7 +216,8 @@ val add_links : t -> Link.t list -> t
 val get_links : t -> Link.t list
 
 val user_agent : string
-(** The User-Agent header used by this library, including the version of cohttp. *)
+(** The User-Agent header used by this library, including the version of cohttp.
+*)
 
 val prepend_user_agent : t -> string -> t
 (** Prepend [user_agent] to the product token already declared in the
index 05ea6454132c581b31baebc17b9be0aad7d18718..cd5f93782f72c8b6dee4ace9aa7328dde79cbd26 100644 (file)
@@ -42,16 +42,26 @@ let version t = t.version
 let encoding t = Header.get_transfer_encoding t.headers
 
 let make ?(meth = `GET) ?(version = `HTTP_1_1) ?encoding
-    ?(headers = Header.init ()) uri =
-  let headers =
-    Header.add_unless_exists headers "host"
-      (match Uri.scheme uri with
-      | Some "httpunix" -> ""
-      | _ -> (
-          Uri.host_with_default ~default:"localhost" uri
-          ^
-          match Uri.port uri with Some p -> ":" ^ string_of_int p | None -> ""))
+    ?(headers = Header.init ()) ?(absolute_form = false) uri =
+  let port () =
+    match Uri.port uri with
+    | Some p -> ":" ^ string_of_int p
+    | None when meth = `CONNECT -> (
+        match Uri_services.tcp_port_of_uri uri with
+        | None -> failwith "A port is required for the CONNECT method."
+        | Some p -> ":" ^ string_of_int p)
+    | None -> ""
+  in
+  let host =
+    match Header.get headers "host" with
+    | None -> (
+        match Uri.scheme uri with
+        | Some "httpunix" -> ""
+        | _ -> Uri.host_with_default ~default:"localhost" uri ^ port ())
+    | Some host -> if String.contains host ':' then host else host ^ port ()
   in
+
+  let headers = Header.replace headers "host" host in
   let headers =
     Header.add_unless_exists headers "user-agent" Header.user_agent
   in
@@ -66,7 +76,9 @@ let make ?(meth = `GET) ?(version = `HTTP_1_1) ?encoding
         Header.add_authorization headers auth
     | _, _, _ -> headers
   in
-  let resource = Uri.path_and_query uri in
+  let resource =
+    if absolute_form then Uri.to_string uri else Uri.path_and_query uri
+  in
   let headers =
     match encoding with
     | None -> headers
@@ -80,7 +92,7 @@ let is_keep_alive t = Http.Request.is_keep_alive t
    adding content headers if appropriate.
    @param chunked Forces chunked encoding
 *)
-let make_for_client ?headers ?chunked ?body_length meth uri =
+let make_for_client ?headers ?chunked ?body_length ?absolute_form meth uri =
   let encoding =
     match (chunked, body_length) with
     | Some true, None -> Transfer.Chunked
@@ -89,7 +101,7 @@ let make_for_client ?headers ?chunked ?body_length meth uri =
     | Some true, Some _ ->
         invalid_arg "cannot set both ?chunked and ?body_length:"
   in
-  make ~meth ~encoding ?headers uri
+  make ~meth ~encoding ?headers ?absolute_form uri
 
 let pp_hum ppf r =
   Format.fprintf ppf "%s" (r |> sexp_of_t |> Sexplib0.Sexp.to_string_hum)
@@ -119,7 +131,9 @@ let uri { resource; headers; meth; _ } =
       | Some _ -> (
           Uri.(
             (* we have an absoluteURI *)
-            match path uri with "" -> with_path uri "/" | _ -> uri))
+            match path uri with
+            | "" -> with_path uri "/"
+            | _ -> uri))
       | None -> (
           let empty = Uri.of_string "" in
           let empty_base = Uri.of_string "///" in
@@ -181,7 +195,9 @@ module Make (IO : S.IO) = struct
     let fst_line =
       Printf.sprintf "%s %s %s\r\n"
         (Http.Method.to_string req.meth)
-        (if req.resource = "" then "/" else req.resource)
+        (if req.meth = `CONNECT then Option.get (Header.get req.headers "host")
+         else if req.resource = "" then "/"
+         else req.resource)
         (Http.Version.to_string req.version)
     in
     IO.write oc fst_line >>= fun _ -> Header_IO.write req.headers oc
index 915433ff9fd8d776b1fd4f9cf27c8f92894d5f98..8daf31664ba1d36d66c9d87e7119e24260cbd104 100644 (file)
@@ -102,6 +102,7 @@ module type Request = sig
     ?version:Code.version ->
     ?encoding:Transfer.encoding ->
     ?headers:Header.t ->
+    ?absolute_form:bool ->
     Uri.t ->
     t
   (** [make ()] is a value of {!type:t}. The default values for the request, if
@@ -119,6 +120,7 @@ module type Request = sig
     ?headers:Header.t ->
     ?chunked:bool ->
     ?body_length:int64 ->
+    ?absolute_form:bool ->
     Code.meth ->
     Uri.t ->
     t
index 914830073f7eb970ab755dd0b79abf4e1798de46..1f339288f1fb2cacfad568acbf7d01473b897312 100644 (file)
 
 module A = Cohttp.Accept
 
-let suite_of :
-    type a.
+let suite_of : type a.
     (string option -> a) -> a Alcotest.testable -> (string * a) list -> _ list =
  fun pf t ->
   List.map (fun (s, expected) ->
       let test () = Alcotest.check t s (pf (Some s)) expected in
       (s, `Quick, test))
 
-let suite_of_fail :
-    type a.
+let suite_of_fail : type a.
     (string option -> a) -> a Alcotest.testable -> (string * exn) list -> _ list
     =
  fun pf _ ->
@@ -41,8 +39,8 @@ let suite_to_string_of : type a. (a -> string) -> (a * string) list -> _ list =
       let test () = Alcotest.(check string expected_str expected_str (pf v)) in
       (expected_str, `Quick, test))
 
-let suite_to_string_of_fail :
-    type a. (a -> string) -> (a * string * exn) list -> _ list =
+let suite_to_string_of_fail : type a.
+    (a -> string) -> (a * string * exn) list -> _ list =
  fun pf ->
   List.map (fun (v, descr, e) ->
       let test () = Alcotest.(check_raises descr e (fun () -> ignore (pf v))) in
index 2a6ce3c8050ab8dab8111f77bd991f1745fb7f36..f03e57b885742fff22f6917bd6f857a6630a7685 100644 (file)
@@ -1,7 +1,7 @@
 (lang dune 3.8)
 
 (name cohttp)
-(version v6.0.0)
+(version v6.1.0)
 
 (license ISC)
 
@@ -79,6 +79,8 @@
   (lwt
    (>= 5.4.0))
   sexplib0
+  (ipaddr
+   (>= 5.6.0))
   (ppx_sexp_conv
    (>= v0.13.0))
   logs
   (lwt
    (>= 3.0.0))
   (conduit-lwt
-   (>= 5.0.0))
+   (>= 7.1.0))
   (conduit-lwt-unix
-   (>= 5.0.0))
+   (>= 7.1.0))
   (fmt
    (>= 0.8.2))
   base-unix
   "An implementation of an HTTP client and server using the Async\nconcurrency library. See the `Cohttp_async` module for information\non how to use this.  The package also installs `cohttp-curl-async`\nand a `cohttp-server-async` binaries for quick uses of a HTTP(S)\nclient and server respectively.\n")
  (depends
   (ocaml
-   (>= 4.14))
+   (and (>= 4.14) (< 5.3.0)))
   (http
    (= :version))
   (cohttp
   (mirage-channel
    (>= 4.0.0))
   (conduit
-   (>= 2.0.2))
+   (>= 8.0.0))
   (conduit-mirage
-   (>= 2.3.0))
+   (>= 8.0.0))
   (mirage-kv
    (>= 3.0.0))
   (lwt
   logs
   uri
   (tls-eio (and :with-test (>= 1.0.0)))
-  (mirage-crypto-rng-eio (and :with-test (>= 0.11.2)))
+  (mirage-crypto-rng (and :with-test (>= 1.2.0)))
   (ca-certs (and :with-test  (>= "1.0.0")))
   fmt
   ptime
index 95e638b1d1eb17772bc180d5e11459fcf0825bc0..c5a6bd70c630f8aab7c70a1631fa01009f30dcbd 100644 (file)
@@ -5,29 +5,11 @@
         "systems": "systems"
       },
       "locked": {
-        "lastModified": 1710146030,
-        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+        "lastModified": 1731533236,
+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
-        "type": "github"
-      },
-      "original": {
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "type": "github"
-      }
-    },
-    "flake-utils_2": {
-      "inputs": {
-        "systems": "systems_2"
-      },
-      "locked": {
-        "lastModified": 1710146030,
-        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
         "type": "github"
       },
       "original": {
     },
     "nixpkgs": {
       "inputs": {
-        "flake-utils": "flake-utils_2",
         "nixpkgs": "nixpkgs_2"
       },
       "locked": {
-        "lastModified": 1725916231,
-        "narHash": "sha256-kaU41Z43Uv2As0Sor8FPACJfWjkbUsWnZMtbCgqicvU=",
+        "lastModified": 1734153276,
+        "narHash": "sha256-/cvtpMFp0HArEpFi0PrPMsheauc3IJ7qWpSHnw8so2M=",
         "owner": "nix-ocaml",
         "repo": "nix-overlays",
-        "rev": "d63aa7b62251c70bbf0a28a67c30555077a2b758",
+        "rev": "4247b28ce426ccdea09a1ec014fa52785bc7ba1d",
         "type": "github"
       },
       "original": {
     },
     "nixpkgs_2": {
       "locked": {
-        "lastModified": 1725857262,
-        "narHash": "sha256-m9n0PncgZepVgmjOO1rfVXMgUACDOwZbhjSRjJ/NUpM=",
+        "lastModified": 1734100912,
+        "narHash": "sha256-93T/KB1ppdhnaV4u5uSwO6HutSq2RzcnkqVX9YKYslE=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "5af6aefbcc55670e36663fd1f8a796e1e323001a",
+        "rev": "2a7ebf12140f6d97941d5f8cc38e9323212ecbad",
         "type": "github"
       },
       "original": {
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "5af6aefbcc55670e36663fd1f8a796e1e323001a",
+        "rev": "2a7ebf12140f6d97941d5f8cc38e9323212ecbad",
         "type": "github"
       }
     },
         "repo": "default",
         "type": "github"
       }
-    },
-    "systems_2": {
-      "locked": {
-        "lastModified": 1681028828,
-        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
-        "owner": "nix-systems",
-        "repo": "default",
-        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
-        "type": "github"
-      },
-      "original": {
-        "owner": "nix-systems",
-        "repo": "default",
-        "type": "github"
-      }
     }
   },
   "root": "root",
index c02dfced2e2abf773db447dbff2ff153fd77eb0b..da89e0e1b8628f4919401eb3fc02c7f5d8bd18a0 100644 (file)
--- a/flake.nix
+++ b/flake.nix
           default = http;
           http = pkg {
             pname = "http";
+            propagatedBuildInputs = [ ppx_expect ];
             checkInputs = [ alcotest base_quickcheck ppx_expect crowbar ];
           };
           cohttp = pkg {
             pname = "cohttp";
             checkInputs = [ fmt alcotest ];
             propagatedBuildInputs = [
-              stringext http re uri uri-sexp logs sexplib0 ppx_sexp_conv
+              base64 stringext http re uri uri-sexp logs sexplib0 ppx_sexp_conv
             ];
           };
           cohttp-top = pkg {
@@ -56,8 +57,9 @@
           };
           cohttp-curl-lwt = pkg {
             pname = "cohttp-curl-lwt";
-            checkInputs = [ cohttp-lwt-unix cohttp cohttp-lwt conduit-lwt ounit2 uri ];
-            propagatedBuildInputs = [ ocurl http stringext lwt ];
+            checkInputs = [ alcotest cohttp-lwt-unix cohttp cohttp-lwt conduit-lwt ounit2 uri ];
+            propagatedBuildInputs = [ ocurl cohttp-curl http stringext lwt ];
+            __darwinAllowLocalNetworking = true;
           };
           cohttp-curl-async = pkg {
             pname = "cohttp-curl-async";
@@ -94,6 +96,7 @@
               http cohttp cohttp-lwt cmdliner lwt conduit-lwt
               conduit-lwt-unix fmt ppx_sexp_conv magic-mime logs
             ];
+            __darwinAllowLocalNetworking = true;
           };
           cohttp-server-lwt-unix = pkg {
             pname = "cohttp-server-lwt-unix";
index 2aef68fd4e39db787866dfbabeaace78082fb6b9..0909a062d6e74e7c756d4ab40ae8e04fb6ec4a04 100644 (file)
--- a/http.opam
+++ b/http.opam
@@ -1,4 +1,4 @@
-version: "6.0.0"
+version: "6.1.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Type definitions of HTTP essentials"
index b2ddc0870b2cae9ffd81adc5460eadb82f659c16..32713cf64c66ba7f4752e9e3cd2ff4af9157116d 100644 (file)
@@ -278,7 +278,8 @@ module Header : sig
       returned value is the last value paired with [k] in [h].
 
       {e Invariant:}
-      [forall h, k not a list-value header. get_multi_concat ~list-value-only:true h k = get h k] *)
+      [forall h, k not a list-value header. get_multi_concat
+       ~list-value-only:true h k = get h k] *)
 
   val update : t -> string -> (string option -> string option) -> t
   (** [update h k f] returns an header list containing the same headers as [h],
@@ -357,7 +358,8 @@ module Header : sig
 
       Finally, following
       {{:https://tools.ietf.org/html/rfc7230#section-3.2.2} RFC7230§3.2.2}, the
-      header [Set-cookie] is treated as an exception and ignored by [clean_dup]. *)
+      header [Set-cookie] is treated as an exception and ignored by [clean_dup].
+  *)
 
   val get_content_range : t -> Int64.t option
   val get_connection_close : t -> bool