New upstream version 5.0.0
authorJulien Puydt <jpuydt@debian.org>
Tue, 4 Oct 2022 09:14:01 +0000 (11:14 +0200)
committerJulien Puydt <jpuydt@debian.org>
Tue, 4 Oct 2022 09:14:01 +0000 (11:14 +0200)
57 files changed:
.github/workflows/workflow.yml [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.gitmodules [new file with mode: 0644]
.ocamlformat [new file with mode: 0644]
CHANGES.md
Makefile
README.md
cohttp-async.opam
cohttp-async/bin/cohttp_curl_async.ml
cohttp-async/bin/cohttp_server_async.ml
cohttp-async/bin/dune
cohttp-lwt-jsoo.opam
cohttp-lwt-jsoo/src/cohttp_lwt_jsoo.ml
cohttp-lwt-jsoo/src/cohttp_lwt_jsoo.mli
cohttp-lwt-jsoo/src/dune
cohttp-lwt-unix.opam
cohttp-lwt-unix/bin/cohttp_curl_lwt.ml
cohttp-lwt-unix/bin/cohttp_proxy_lwt.ml
cohttp-lwt-unix/bin/cohttp_server_lwt.ml
cohttp-lwt-unix/bin/dune
cohttp-lwt-unix/src/debug.ml
cohttp-lwt-unix/src/debug.mli
cohttp-lwt-unix/src/io.mli
cohttp-lwt-unix/src/net.ml
cohttp-lwt-unix/src/server.ml
cohttp-lwt-unix/src/server.mli
cohttp-lwt-unix/test/test_parser.ml
cohttp-lwt.opam
cohttp-lwt/src/client.ml
cohttp-lwt/src/client.mli
cohttp-lwt/src/s.ml
cohttp-lwt/src/server.mli
cohttp-mirage.opam
cohttp-mirage/src/client.ml
cohttp-mirage/src/io.ml
cohttp-mirage/src/io.mli
cohttp-mirage/src/static.ml
cohttp-mirage/src/static.mli
cohttp-top.opam
cohttp.opam
cohttp/fuzz/dune [new file with mode: 0644]
cohttp/fuzz/fuzz_header.ml [new file with mode: 0644]
cohttp/fuzz/inputs/input [new file with mode: 0644]
cohttp/src/header.ml
cohttp/src/header.mli
cohttp/src/header_io.ml
cohttp/test/dune
cohttp/test/test_header.ml
cohttp/test/unitary_test_header.ml [new file with mode: 0644]
cohttp_lwt_jsoo_test/package.json [new file with mode: 0644]
cohttp_lwt_jsoo_test/src/cohttp_lwt_jsoo_test.ml [new file with mode: 0644]
cohttp_lwt_jsoo_test/src/dune [new file with mode: 0644]
cohttp_lwt_jsoo_test/src/test.js [new file with mode: 0755]
cohttp_lwt_jsoo_test/yarn.lock [new file with mode: 0644]
dune-project
examples/async/s3_cp.ml
jbuild-workspace.dev [deleted file]

diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml
new file mode 100644 (file)
index 0000000..c6b46eb
--- /dev/null
@@ -0,0 +1,49 @@
+name: Main workflow
+
+on:
+  pull_request:
+  push:
+  schedule:
+    # Prime the caches every Monday
+    - cron: 0 1 * * MON
+
+jobs:
+  build:
+    strategy:
+      fail-fast: false
+      matrix:
+        os:
+          - macos-latest
+          - ubuntu-latest
+        ocaml-compiler:
+          - 4.08.x
+          - 4.09.x
+          - 4.10.x
+          - 4.11.x
+          - 4.12.x
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v2
+
+      - name: Use OCaml ${{ matrix.ocaml-compiler }}
+        uses: ocaml/setup-ocaml@v2
+        with:
+          ocaml-compiler: ${{ matrix.ocaml-compiler }}
+          dune-cache: ${{ matrix.os == 'ubuntu-latest' }}
+
+      - run: yarn --frozen-lockfile
+        working-directory: cohttp_lwt_jsoo_test
+
+      - run: echo "PKG_CONFIG_PATH=$(brew --prefix openssl)/lib/pkgconfig" >>"$GITHUB_ENV"
+        if: ${{ matrix.os == 'macos-latest' }}
+
+      - run: opam install . --deps-only --with-test
+
+      - run: opam exec -- make build
+
+      - run: opam exec -- make test
+
+      - run: opam exec -- make js-test
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..6b6c028
--- /dev/null
@@ -0,0 +1,6 @@
+.*.swp
+_build/
+*.install
+.merlin
+_opam/
+node_modules
diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/.ocamlformat b/.ocamlformat
new file mode 100644 (file)
index 0000000..f14ecd8
--- /dev/null
@@ -0,0 +1,4 @@
+version = 0.20.0
+profile = conventional
+break-infix = fit-or-vertical
+parse-docstrings = true
index 987cff4e10e7585f8b263ea6d9f936b394bb01d2..1985d9119eb93d637b1267df7dcea10dffe9473f 100644 (file)
@@ -1,31 +1,79 @@
+## v5.0.0 (2021-12-15)
+
+- Cohttp.Header: new implementation (lyrm #747)
+
+  + New implementation of Header modules using an associative list instead of a map, with one major semantic change (function ```get```, see below), and some new functions (```clean_dup```, ```get_multi_concat```)
+  + More Alcotest tests as well as fuzzing tests for this particular module.
+
+  ### Purpose
+
+  The new header implementation uses an associative list instead of a map to represent headers and is focused on predictability and intuitivity: except for some specific and documented functions, the headers are always kept in transmission order, which makes debugging easier and is also important for [RFC7230§3.2.2](https://tools.ietf.org/html/rfc7230#section-3.2.2) that states that multiple values of a header must be kept in order.
+
+  Also, to get an intuitive function behaviour, no extra work to enforce RFCs is done by the basic functions. For example, RFC7230§3.2.2 requires that a sender does not send multiple values for a non list-value header. This particular rule could require the ```Header.add``` function to remove previous values of non-list-value headers, which means some changes of the headers would be out of control of the user. With the current implementation, an user has to actively call dedicated functions to enforce such RFCs (here ```Header.clean_dup```).
+
+  ### Semantic changes
+  Two functions have a semantic change : ```get``` and ```update```.
+
+  #### get
+  ```get``` was previously doing more than just returns the value associated to a key; it was also checking if the searched header could have multiple values: if not, the last value associated to the header was returned; otherwise, all the associated values were concatenated and returned. This semantics does not match the global idea behind the new header implementation, and would also be very inefficient.
+
+  + The new ```get``` function only returns the last value associated to the searched header.
+  + ```get_multi_concat``` function has been added to get a result similar to the previous ```get``` function.
+
+  #### update
+  ```update``` is a pretty new function (#703) and changes are minor and related to ```get``` semantic changes.
+
+  + ```update h k f``` is now modifying only the last occurrences of the header ```k``` instead of all its occurrences.
+  + a new function ```update_all``` function has been added and work on all the occurrences of the updated header.
+
+  ### New functions :
+
+  + ```clean_dup```  enables the user to clean headers that follows the {{:https://tools.ietf.org/html/rfc7230#section-3.2.2} RFC7230§3.2.2} (no duplicate, except ```set-cookie```)
+  + ```get_multi_concat``` has been added to get a result similar to the previous ```get``` function.
+
+- Cohttp.Header: performance improvement (mseri, anuragsoni #778) 
+  **Breaking** the headers are no-longer lowercased when parsed, the headers key comparison is case insensitive instead.
+
+- cohttp-lwt-unix: Adopt ocaml-conduit 5.0.0 (smorimoto #787) 
+  **Breaking** `Conduit_lwt_unix.connect`'s `ctx` param type chaged from `ctx` to  `ctx Lazy.t`
+
+- cohttp-mirage: fix deprecated fmt usage (tmcgilchrist #783)
+- lwt_jsoo: Use logs for the warnings and document it (mseri #776)
+- lwt: Use logs to warn users about leaked bodies and document it (mseri #771)
+- lwt, lwt_unix: Improve use of logs and the documentation, fix bug in the Debug.enable_debug function (mseri #772)
+- lwt_jsoo: Fix exception on connection errors in chrome (mefyl #761)
+- lwt_jsoo: Fix `Lwt.wakeup_exn` `Invalid_arg` exception when a js
+  stack overflow happens in the XHR completion handler (mefyl #762).
+- lwt_jsoo: Add test suite (mefyl #764).
+
 ## v4.0.0 (2021-03-24)
 
-- cohttp.response: fix malformed status header for custom status codes (@mseri @aalekseyev #752)
-- Remove dependency to base (@samoht #745)
-- fix opam files and dependencies
-- add GitHub Actions workflow (@smorimoto #739)
-- lwt_jsoo: Forward exceptions to caller when response is null (@mefyl #738)
-- Remove wrapped false (@rgrinberg #734)
-- Use implicit executable dependency for generate.exe (@TheLortex #735)
-- cohttp: update HTTP codes (@emillon #711)
-- cohttp: add Uti.t to uri scheme (@brendanlong #707)
-- cohttp: fix chunked encoding of empty body (@mefyl #715)
-- cohttp-async: fix body not being uploaded with unchunked Async.Pipe (@mefyl #706)
-- cohttp-{async, lwt}: fix suprising behaviours of Body.is_empty (@anuragsoni #714 #712 #713)
-- cohttp-lwt-jsoo: rename Cohttp_lwt_xhr to Cohttp_lwt_jsoo for consistency (@mseri #717)
-- refactoring of tests (@mseri #709, @dinosaure #692)
-- update documentation (@dinosaure #716, @mseri #720)
-- cohttp: fix transfer-encoding ordering in headers (@mseri #721)
-- lower-level support for long-running cohttp-async connections (@brendanlong #704)
-- fix deadlock in logging (@dinosaure #722)
-- add of_form and to_form functions to body (@seliopou #440, @mseri #723)
-- cohttp-lwt: partly inline read_response, fix body stream leak (@madroach @dinosaure #696)
-- improve media type parsing (@seliopou #542, @dinosaure #725)
-- add comparison functions for Request.t and Response.t via ppx_compare (@msaffer-js @dinosaure #686)
-- [reverted] breaking changes to client and server API to use conduit
-  3.0.0 (@dinosaure #692). However, as the design discussion did not reach
-  consensus, these changes were reverted to preserve better compatibility with
-  existing cohttp users. (#741,  @samoht)
+- cohttp.response: fix malformed status header for custom status codes (mseri aalekseyev #752)
+- remove dependency to base (samoht #745)
+- add GitHub Actions workflow (smorimoto #739)
+- `cohttp-lwt-jsoo`: Forward exceptions to caller when response is null (mefyl #738)
+- Use implicit executable dependency for generate.exe (TheLortex #735)
+- cohttp: fix chunked encoding of empty body (mefyl #715)
+- cohttp-async: fix body not being uploaded with unchunked Async.Pipe (mefyl #706)
+- cohttp-{async, lwt}: fix suprising behaviours of Body.is_empty (anuragsoni #714 #712 #713)
+- refactoring of tests (mseri #709, dinosaure #692)
+- update documentation (dinosaure #716, mseri #720)
+- fix deadlock in logging (dinosaure #722)
+- improve media type parsing (seliopou #542, dinosaure #725)
+- [reverted] breaking changes to client and server API to use conduit 3.0.0 (dinosaure #692). However, as the design discussion did not reach consensus, these changes were reverted to preserve better compatibility with existing cohttp users. (samoht #741)
+
+**Potentially breaking changes**
+
+- remove `wrapped false` from the codebase (rgrinberg #734)
+- cohttp: add Uri.scheme to Request.t (brendanlong #707)
+- cohttp: update HTTP codes (emillon #711)
+- cohttp-lwt-jsoo: rename Cohttp_lwt_xhr to Cohttp_lwt_jsoo for consistency (mseri #717)
+- cohttp: fix transfer-encoding ordering in headers (mseri #721)
+- lower-level support for long-running cohttp-async connections (brendanlong #704)
+- add of_form and to_form functions to body (seliopou #440, mseri #723)
+- cohttp-lwt: partly inline read_response, fix body stream leak (madroach dinosaure #696).
+  Note: there is a new warning that may show up in your logs when bodies are leaked, see also [#730](https://github.com/mirage/ocaml-cohttp/issues/730).
+- add comparison functions for Request.t and Response.t via ppx_compare (msaffer-js dinosaure #686)
 
 ## v3.0.0 - aborted
 
   have been preserved for compatibility, but will be marked as deprecated in the next release. This
   changes the behavior of `Cohttp_lwt_unix.resolve_file`: it now percent-decodes the paths and blocks
   escaping from the docroot correctly. This also fixes and tests the corner cases in these methods
-  when the docroot is empty. (@ewanmellor #755)
+  when the docroot is empty. (ewanmellor #755)
 
 ## v2.5.4 (2020-07-21)
 
 - cohttp: a change in #694 modified the semantics of Header.replace.
   The semantics change is reverted, and a new Header.update function
-  is introduced, following the semantics of Map.update. (#702 @mseri)
+  is introduced, following the semantics of Map.update. (#702 mseri)
 - cohttp: reimplement update to support compilers that are older than
-  OCaml 4.06 (#703 @mseri)
+  OCaml 4.06 (#703 mseri)
 
 ## v2.5.3 (2020-06-27)
 
-- cohttp-async: adapt to async >= v0.14 (#699 @copy)
+- cohttp-async: adapt to async >= v0.14 (#699 copy)
 
 ## v2.5.2 (2020-06-27)
 
 - cohttp, cohttp-async: correctly set host header for unix domain sockets,
-  implement Unix domain socket support for cohttp-async (#698 @Leonidas-from-XIV)
+  implement Unix domain socket support for cohttp-async (#698 Leonidas-from-XIV)
 - cohttp: better body encoding management when creating request and
-  response, and correction of Header.replace function (#694 @lyrm)
+  response, and correction of Header.replace function (#694 lyrm)
 
 ## v2.5.1 (2020-02-18)
 
-- cohttp-lwt: pass ctx through HEAD client requests (#689 @hannesm)
+- cohttp-lwt: pass ctx through HEAD client requests (#689 hannesm)
 
 ## v2.5.0 (2019-12-17)
 
-- cohttp-async: support async v0.13.0 (#680 @copy)
-- cohttp-lwt-jsoo: support js_of_ocaml 3.5.0 and higher (@avsm)
+- cohttp-async: support async v0.13.0 (#680 copy)
+- cohttp-lwt-jsoo: support js_of_ocaml 3.5.0 and higher (avsm)
 
 ## v2.4.0 (2019-11-08)
 
 - mirage: adapt to new mirage interfaces: mirage-flow 2.0.0,
-  mirage-channel 4.0.0, mirage-kv 3.0.0 (#678 @hannesm)
+  mirage-channel 4.0.0, mirage-kv 3.0.0 (#678 hannesm)
 - async: use Pipe.singleton instead of Pipe.of_list as it is more efficient
-  (#677 @smuenzel-js)
+  (#677 smuenzel-js)
 
 ## v2.3.0 (2019-08-18)
 
 - use conduit-mirage instead of mirage-conduit, which was renamed
   upstream in conduit. The minimum OCaml version supported for
-  conduit-mirage is now OCaml 4.07 and higher. (#672 @avsm)
-- remove deprecation warnings in OCaml 4.08.0 using stdlib-shims (#672 @avsm)
-- async: do not read body if none is present (#671 @emillon)
+  conduit-mirage is now OCaml 4.07 and higher. (#672 avsm)
+- remove deprecation warnings in OCaml 4.08.0 using stdlib-shims (#672 avsm)
+- async: do not read body if none is present (#671 emillon)
 
 ## v2.2.0 (2019-07-20)
 
   handler). Now, cohttp will just log this at `info` level and
   continue. Exceptions produced by user code are logged as errors,
   while other exceptions generated by cohttp call back to the conduit
-  exception handler, as before. (#669 @talex5)
+  exception handler, as before. (#669 talex5)
 
 ## v2.1.3 (2019-07-12)
 
-- support uri.3.0.0 that has optional sexp support (#668 @avsm)
-- use re.1.9.0 api to remove deprecation warnings (#664 @vbmithr)
+- support uri.3.0.0 that has optional sexp support (#668 avsm)
+- use re.1.9.0 api to remove deprecation warnings (#664 vbmithr)
 
 ## v2.1.2 (2019-04-09)
 
 - cohttp: handle empty cookie components gracefully without raising
-  an exception (#663 @martinslota)
+  an exception (#663 martinslota)
 
 ## v2.1.1 (2019-04-05)
 
 - `cohttp-mirage`: remove dependency on the `result` module
-  (#662 @hannesm)
-- Support Async v0.12.0 and higher (#661 @copy)
+  (#662 hannesm)
+- Support Async v0.12.0 and higher (#661 copy)
 
 ## v2.1.0 (2019-03-01)
 
 
 Compatibility breaking interface changes:
 
-Async: Expert response action no longer writes empty HTTP body (#647 by @andreas)
+Async: Expert response action no longer writes empty HTTP body (#647 by andreas)
 
 In cohttp.0.99, a number of subpackages were turned into explicit
 opam packages to simplify dependency management.
@@ -130,51 +178,51 @@ them as follows:
 - `cohttp.top` -> `cohttp-top`
 
 Other changes and bugfixes:
-* Lwt, Mirage: Add log warnings for uncaught exceptions (#592 by @ansiwen)
-* Log invalid client input and do not catch out of memory exceptions (#652 @hannesm)
+* Lwt, Mirage: Add log warnings for uncaught exceptions (#592 by ansiwen)
+* Log invalid client input and do not catch out of memory exceptions (#652 hannesm)
 * Port opam files to opam2 and add local synopsis and descriptions.
-* Lwt: Add Expert response action for servers (#647 by @andreas)
-* Use the namespaced `js_of_ocaml` interfaces from 3.3.0 onwards (#654 @avsm)
-* Use Base64 3.1.0 interfaces (#655 @avsm)
-* Clean up redundant conflicts in the `opam` files (@avsm)
+* Lwt: Add Expert response action for servers (#647 by andreas)
+* Use the namespaced `js_of_ocaml` interfaces from 3.3.0 onwards (#654 avsm)
+* Use Base64 3.1.0 interfaces (#655 avsm)
+* Clean up redundant conflicts in the `opam` files (avsm)
 
 ## v1.2.0 (2018-10-19)
 
-* Support more than a single chunk extension for RFC7320 compliance (#618 by @djs55)
-* Lwt-unix: add a `?backlog` argument to the serve function (@samoht)
-* Use the uri.2.0.0 interfaces for sexpression generation of types (@avsm)
-* Switch to `sexplib0` for a more lightweight s-expression library (@mseri)
-* Minimum OCaml compiler version requirement is now 4.04.1 (@mseri)
-* Add an example of using custom resolvers to the README (@mseri)
+* Support more than a single chunk extension for RFC7320 compliance (#618 by djs55)
+* Lwt-unix: add a `?backlog` argument to the serve function (samoht)
+* Use the uri.2.0.0 interfaces for sexpression generation of types (avsm)
+* Switch to `sexplib0` for a more lightweight s-expression library (mseri)
+* Minimum OCaml compiler version requirement is now 4.04.1 (mseri)
+* Add an example of using custom resolvers to the README (mseri)
 
 ## v1.1.1 (2018-08-13)
 
-* Update to be compatible with new async/core (#607 by @rgrinberg)
-* Remove use of deprecated `Lwt_logs` (#609 by @raphael-proust)
-* Do not rely on locale for printing qvalues (#611 by @vbmithr)
-* Ppx dependencies aren't just build time dependencies (#625 by @rgrinberg)
+* Update to be compatible with new async/core (#607 by rgrinberg)
+* Remove use of deprecated `Lwt_logs` (#609 by raphael-proust)
+* Do not rely on locale for printing qvalues (#611 by vbmithr)
+* Ppx dependencies aren't just build time dependencies (#625 by rgrinberg)
 
 ## v1.1.0 (2018-03-28)
 
 * Add an "expert mode" to hand off raw responses to a custom handler,
-  which in turns makes protocols like Websockets easier (#488 by @msaffer).
-* Set the user-agent by default if one is not provided (#586 by @TheCBaH).
+  which in turns makes protocols like Websockets easier (#488 by msaffer).
+* Set the user-agent by default if one is not provided (#586 by TheCBaH).
 * Fix typo in the `cohttp.js` META file.
-* Refresh use of the Re library to the latest version (#602 by @rgrinberg).
-* Rearrange the ppx dependencies to be more specific (#596 by @yomimono).
-* Explicitly depend on sexplib in the Async backend (#605 by @kevinqiu).
+* Refresh use of the Re library to the latest version (#602 by rgrinberg).
+* Rearrange the ppx dependencies to be more specific (#596 by yomimono).
+* Explicitly depend on sexplib in the Async backend (#605 by kevinqiu).
 
 ## v1.0.2 (2018-01-06)
 
-* Support Async v0.10.0 and OCaml 4.06.0 (#588 via @vbmithr)
+* Support Async v0.10.0 and OCaml 4.06.0 (#588 via vbmithr)
 * Require `ppx_type_conv`>=v0.9.1` due to a bug with duplicate modules
   present in earlier versions.
 
 ## v1.0.1 (2018-01-03)
 
-* cohttp-mirage: expose the missing IO module (#594, @samoht)
+* cohttp-mirage: expose the missing IO module (#594, samoht)
 * cohttp-mirage: catch exceptions when closing channels in mirage client
-  (#589, @ansiwen)
+  (#589, ansiwen)
 
 ## v1.0.0 (2017-11-16)
 
@@ -221,8 +269,8 @@ alias was added to deprecate the direct use of `Cohttp_lwt_body`.
 This will let us unify the namespaces of all the packages to use
 a single top-level module for each package in the future.
 
-Most of the release and packaging work here was done by @rgrinberg
-and @avsm.
+Most of the release and packaging work here was done by rgrinberg
+and avsm.
 
 ## 0.22.0 (2017-03-09)
 
@@ -283,7 +331,7 @@ New features and bug fixes:
 * Do not use the `lwt` camlp4 extension. No observable external difference.
 * Do not return a code stacktrace in the default 500 handler.
 * Add `Cohttp.Header.compare` (#411)
-* Fix typos in CLI documentation (#413 via @moonlightdrive)
+* Fix typos in CLI documentation (#413 via moonlightdrive)
 * Use the Lwt 2.5.0 buffer API.
 * `Cohttp_lwt.read_response` now has a non-optional `closefn` parameter (#400).
 * Add a `Cohttp_lwt_s` module that contains all the Lwt module types
@@ -584,7 +632,7 @@ New features and bugfixes:
 * Install a `cohttp-server` binary that serves local directory contents via a web server (#54).
 * Add a `flush` function to the `IO` module type and implement in Lwt/Async/Mirage.
 * Add option `flush` support in the Async and Lwt responders (#52).
-* Autogenerate HTTP codes from @citricsquid's JSON representation of the HTTP RFCs.
+* Autogenerate HTTP codes from citricsquid's JSON representation of the HTTP RFCs.
 * Always set `TCP_NODELAY` for Lwt/Unix server sockets for low-latency responses (#58).
 * Added a Server-Side Events test-case from the HTML5 Doctor. See `lib_test/README.md`.
 * Async.Server response now takes an optional `body` rather than a mandatory `body option` (#62).
index 69b9b1f1ccd52ebd9e230a9ac7cda4d89849188f..286104546ce03556d33c90bb4e86866b8a4c3733 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: build clean test clean all-supported-ocaml-versions
+.PHONY: build clean test clean
 
 build:
        dune build
@@ -6,8 +6,8 @@ build:
 test:
        dune runtest
 
+js-test:
+       dune build @runjstest
+
 clean:
        dune clean
-
-all-supported-ocaml-versions:
-       dune build @install @runtest --workspace jbuild-workspace.dev
index ca335e48320fac36d9ce1236725d25ed538a3c46..5d7a606ecf34e2f0310767fea1aa7787fa511993 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-## ocaml-cohttp -- an OCaml library for HTTP clients and servers
+## ocaml-cohttp -- an OCaml library for HTTP clients and servers [![Main workflow](https://github.com/mirage/ocaml-cohttp/actions/workflows/workflow.yml/badge.svg)](https://github.com/mirage/ocaml-cohttp/actions/workflows/workflow.yml)
 
 Cohttp is an OCaml library for creating HTTP daemons. It has a portable
 HTTP parser, and implementations using various asynchronous programming
@@ -33,6 +33,7 @@ You can find help from cohttp users and maintainers at the
 - [Managing sessions](#managing-sessions)
 - [Multipart form data](#multipart-form-data)
 - [Creating custom resolver: a Docker Socket Client example](#creating-custom-resolver--a-docker-socket-client-example)
+- [Dealing with redirects](#dealing-with-redirects)
 - [Basic Server Tutorial](#basic-server-tutorial)
   * [Compile and execute with ocamlbuild](#compile-and-execute-with-ocamlbuild-1)
   * [Compile and execute with dune](#compile-and-execute-with-dune-1)
@@ -120,7 +121,7 @@ available on the [repository github pages](https://mirage.github.io/ocaml-cohttp
 Build and execute with:
 
 ```
-$ ocamlbuild -pkg cohttp-lwt-unix client_example.native
+$ ocamlbuild -use-ocamlfind -tag thread -pkg cohttp-lwt-unix client_example.native
 $ ./client_example.native
 ```
 
@@ -183,6 +184,21 @@ Fatal error: exception (Failure "Timeout expired")
 
 Similarly, in the case of `cohttp-async` you can directly use Async's
 [`with_timeout`](https://ocaml.janestreet.com/ocaml-core/latest/doc/async_unix/Async_unix/Clock/index.html#val-with_timeout) function.
+For example,
+
+```ocaml
+let get_body ~uri ~timeout =
+    let%bind _, body = Cohttp_async.Client.get ~interrupt:(after (sec timeout)) uri in
+    Body.to_string body    
+
+let body =
+  let uri = Uri.of_string "https://www.reddit.com/" in
+  let timeout = 0.1 in
+  Clock.with_timeout (sec timeout) (get_body ~uri ~timeout)
+  >>| function
+  | `Result body -> Log.debug logger "body: %s" body
+  | `Timeout  -> Log.debug logger "Timeout with url:%s" url
+```
 
 ## Managing sessions
 
@@ -193,8 +209,10 @@ which is compatible with `cohttp`.
 
 ## Multipart form data
 
-Multipart form data is not supported out of the box, but is provided by the external library
-[`multipart-form-data`](https://github.com/cryptosense/multipart-form-data).
+Multipart form data is not supported out of the box but is provided by external libraries:
+- [`multipart_form`](https://github.com/dinosaure/multipart_form) which has bounded memory consumption even when transferring large amount of data
+- [`multipart-form-data`](https://github.com/cryptosense/multipart-form-data)
+- [`http-multipart-formdata`](https://github.com/lemaetech/http-multipart-formdata) which however does not support streaming
 
 ## Creating custom resolver: a Docker Socket Client example
 
@@ -249,6 +267,95 @@ mentioning it to emphasize that we are creating a new Conduit resolver. Refer to
 [conduit's README](https://github.com/mirage/ocaml-conduit/) for examples of use and
 links to up-to-date conduit documentation.
 
+## Dealing with redirects
+
+This examples has been adapted from a script on the [ocaml.org](https://github.com/ocaml/ocaml.org/blob/master/script/http.ml) website, and shows an explicit way to deal with redirects in `cohttp-lwt-unix`.
+
+```ocaml
+let rec http_get_and_follow ~max_redirects uri =
+  let open Lwt.Syntax in
+  let* ans = Cohttp_lwt_unix.Client.get uri in
+  follow_redirect ~max_redirects uri ans
+
+and follow_redirect ~max_redirects request_uri (response, body) =
+  let open Lwt.Syntax in
+  let status = Cohttp.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 request_uri response
+  | `Found | `Temporary_redirect ->
+      handle_redirect ~permanent:false ~max_redirects request_uri response
+  | `Not_found | `Gone -> Lwt.fail_with "Not found"
+  | status ->
+      Lwt.fail_with
+        (Printf.sprintf "Unhandled status: %s"
+           (Cohttp.Code.string_of_status status))
+
+and handle_redirect ~permanent ~max_redirects request_uri response =
+  if max_redirects <= 0 then Lwt.fail_with "Too many redirects"
+  else
+    let headers = Cohttp.Response.headers response in
+    let location = Cohttp.Header.get headers "location" in
+    match location with
+    | None -> Lwt.fail_with "Redirection without Location header"
+    | Some url ->
+        let open Lwt.Syntax in
+        let uri = Uri.of_string url in
+        let* () =
+          if permanent then
+            Logs.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 uri ~max_redirects:(max_redirects - 1)
+```
+
+The following example, adapted from [blue-http](https://github.com/brendanlong/blue-http/blob/master/src/redirect.ml), does a similar thing with `cohttp-async` (and [ppx_let](https://github.com/janestreet/ppx_let)).
+
+```ocaml
+open Core_kernel
+open Async_kernel
+
+let with_redirects ~max_redirects uri f =
+  let seen_uris = Hash_set.create (module String) in
+  let rec loop ~max_redirects uri =
+    Hash_set.add seen_uris (Uri.to_string uri);
+    let%bind ((response, response_body) as res) = f uri in
+    let status_code =
+      Cohttp.(Response.status response |> Code.code_of_status)
+    in
+    if Cohttp.Code.is_redirection status_code then (
+      match Cohttp.(Response.headers response |> Header.get_location) with
+      | Some new_uri when Uri.to_string new_uri |> Hash_set.mem seen_uris ->
+          return res
+      | Some new_uri ->
+          if max_redirects > 0 then
+            (* Cohttp leaks connections if we don't drain the response body *)
+            Cohttp_async.Body.drain response_body >>= fun () ->
+            loop ~max_redirects:(max_redirects - 1) new_uri
+          else (
+            Log.Global.debug ~tags:[]
+              "Ignoring %d redirect from %s to %s: redirect limit exceeded"
+              status_code (Uri.to_string uri) (Uri.to_string new_uri);
+            return res)
+      | None ->
+          Log.Global.debug ~tags:[]
+            "Ignoring %d redirect from %s: there is no Location header"
+            status_code (Uri.to_string uri);
+          return res)
+    else return res
+  in
+  loop ~max_redirects uri
+```
+
+You can read a bit more on the rationale behind the absence of this functionality in the API [here](https://github.com/mirage/ocaml-cohttp/issues/76).
 
 ## Basic Server Tutorial
 
@@ -292,7 +399,7 @@ let server =
 
 Build and execute with:
 ```
-$ ocamlbuild -pkg cohttp-lwt-unix server_example.native
+$ ocamlbuild -use-ocamlfind -tag thread -pkg cohttp-lwt-unix server_example.native
 $ ./server_example.native
 ```
 
@@ -355,9 +462,19 @@ folder in the sources
 
 ## Debugging
 
-You can activate some runtime debugging for the servers by setting `COHTTP_DEBUG` to any value different from `0` or `false`, and it will set a default debug-level logger on stdout. Note: If you turn on the debugging on the `cohttp-lwt-server` example, you need to make sure you also pass the `-vvv` option, which forces the debug level of the logger.
+You can activate some runtime debugging for the servers by setting `COHTTP_DEBUG` to any value different from `0` or `false`, and it will set a default debug-level logger on stdout.
+
+Since both Cohttp and Conduit use `Logs` for debugging output, you can enable custom debugging in your code (if needed). For example, if you intend to make use of the `COHTTP_DEBUG` env variable, you could simply use
+
+```ocaml
+let () =
+  if not @@ Debug.debug_active () then (
+    Fmt_tty.setup_std_outputs ();
+    Logs.set_level ~all:true level;
+    Logs.set_reporter Debug.default_reporter);
+```
 
-Since both Cohttp and Conduit use `Logs` for debugging output, you can enable custom debugging in your code (if needed) by adding something like the following to your code (courtesy of @dinosaure)
+Of course you are free to completely override it and use your own reporters, for example by adding something like the following to your code (courtesy of @dinosaure).
 
 ```ocaml
 let reporter ppf =
@@ -374,9 +491,24 @@ let reporter ppf =
     msgf @@ fun ?header ?tags fmt -> with_metadata header tags k ppf fmt in
   { Logs.report }
 
-let () = Fmt_tty.setup_std_outputs ~style_renderer:`Ansi_tty ~utf_8:true ()
-let () = Logs.set_reporter (reporter Fmt.stderr)
-let () = Logs.set_level ~all:true (Some Logs.Debug)
+let () =
+  Fmt_tty.setup_std_outputs ~style_renderer:`Ansi_tty ~utf_8:true ();
+  Logs.set_reporter (reporter Fmt.stderr);
+  Logs.set_level ~all:true (Some Logs.Debug)
+```
+
+Note that you can selectively filter out the logs produced by `cohttp-lwt` and `cohttp-lwt-unix` internals as follows.
+
+```ocaml
+let () =
+  (* Set log level v for all loggers, this does also affect cohttp internal loggers *)
+  Logs.set_level ~all:true level;
+  (* Disable all cohttp-lwt and cohttp-lwt-unix logs *)
+  List.iter (fun src ->
+      match Logs.Src.name src with
+      | "cohttp.lwt.io" | "cohttp.lwt.server" -> Logs.Src.set_level src None
+      | _ -> ())
+  @@ Logs.Src.list ()
 ```
 
 ## Important Links
index 3bd7272b51836fab8ccf7a92285723dd1ba991be..594f6366f211a477c1e9479d4cf475f6e6c0d6ca 100644 (file)
@@ -1,4 +1,3 @@
-version: "4.0.0"
 opam-version: "2.0"
 maintainer: "anil@recoil.org"
 authors: [
@@ -25,14 +24,15 @@ doc: "https://mirage.github.io/ocaml-cohttp/"
 bug-reports: "https://github.com/mirage/ocaml-cohttp/issues"
 depends: [
   "ocaml" {>= "4.08"}
-  "dune" {>= "1.1.0"}
+  "dune" {>= "2.0"}
   "async_kernel" {>= "v0.14.0"}
   "async_unix" {>= "v0.14.0"}
   "async" {>= "v0.14.0"}
   "base" {>= "v0.11.0"}
   "core" {with-test}
-  "cohttp" {=version}
-  "conduit-async" {>="1.2.0" & <"3.0.0"}
+  "core_unix" {>= "v0.14.0"}
+  "cohttp" {= version}
+  "conduit-async" {>= "1.2.0"}
   "magic-mime"
   "mirage-crypto" {with-test}
   "logs"
@@ -45,8 +45,9 @@ depends: [
   "ipaddr"
 ]
 build: [
-  ["dune" "subst"] {pinned}
+  ["dune" "subst"] {dev}
   ["dune" "build" "-p" name "-j" jobs]
   ["dune" "runtest" "-p" name "-j" jobs] {with-test}
 ]
-dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
\ No newline at end of file
+available: arch != "s390x"
+dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
index 044e38d3169667d374ac9db97cf3f47f4b19bd49..32dc641854ef43f672bfe75a0855ffeaba946ae0 100644 (file)
@@ -19,10 +19,7 @@ open Async_kernel
 open Cohttp_async
 
 let show_headers h =
-  Cohttp.Header.iter
-    (fun k v ->
-      List.iter v ~f:(fun v_i -> Logs.info (fun m -> m "%s: %s%!" k v_i)))
-    h
+  Cohttp.Header.iter (fun k v -> Logs.info (fun m -> m "%s: %s%!" k v)) h
 
 let make_net_req uri meth' body () =
   let meth = Cohttp.Code.method_of_string meth' in
@@ -52,4 +49,4 @@ let _ =
            (optional_with_default "" string)
            ~doc:" Data to send when using POST")
     make_net_req
-  |> run
+  |> Command_unix.run
index d17605dfcc888f16492a7ca1c27e5aa5c9e56095..10b3133e19329f12988d27ba1767ec4b25f30fa8 100644 (file)
@@ -126,7 +126,7 @@ let start_server docroot port index cert_file key_file verbose () =
 
 let () =
   let open Async_command in
-  run
+  Command_unix.run
   @@ async_spec ~summary:"Serve the local directory contents via HTTP or HTTPS"
        Spec.(
          empty
index 80e12e9ae3050f88ed840d5f6be5188662a97c5f..47dc0935afea357d6b1efc7115b9c47a01e0e306 100644 (file)
@@ -3,4 +3,4 @@
  (package cohttp-async)
  (public_names cohttp-curl-async cohttp-server-async)
  (libraries cohttp-async async_kernel async.async_command async_unix base
-   cohttp cohttp_server fmt.tty))
+   cohttp cohttp_server fmt.tty core_unix.command_unix))
index 2ff403b7744d8b452e19803c7989c758778c58ea..24c7fbd200548e39b79d094f199b3c2757e66e45 100644 (file)
@@ -1,4 +1,3 @@
-version: "4.0.0"
 opam-version: "2.0"
 maintainer: "anil@recoil.org"
 authors: [
@@ -24,17 +23,20 @@ doc: "https://mirage.github.io/ocaml-cohttp/"
 bug-reports: "https://github.com/mirage/ocaml-cohttp/issues"
 depends: [
   "ocaml" {>= "4.08"}
-  "dune" {>= "1.1.0"}
-  "cohttp" {=version}
-  "cohttp-lwt" {=version}
+  "dune" {>= "2.0"}
+  "cohttp" {= version}
+  "cohttp-lwt" {= version}
+  "logs"
   "lwt" {>= "3.0.0"}
+  "lwt_ppx" {with-test}
+  "conf-npm" {with-test}
   "js_of_ocaml" {>= "3.3.0"}
   "js_of_ocaml-ppx" {>= "3.3.0"}
   "js_of_ocaml-lwt" {>= "3.5.0"}
 ]
 build: [
-  ["dune" "subst"] {pinned}
+  ["dune" "subst"] {dev}
   ["dune" "build" "-p" name "-j" jobs]
   ["dune" "runtest" "-p" name "-j" jobs] {with-test}
 ]
-dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
\ No newline at end of file
+dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
index 7566f39f9b97363e832a92eedb2cc154ac8515d1..a638de23f9a5f35578e9d6becb134a7ab807b1de 100644 (file)
@@ -46,7 +46,15 @@ let binary_string str =
 let string_of_uint8array u8a offset len =
   String.init len (fun i -> Char.chr (Typed_array.unsafe_get u8a (offset + i)))
 
+module String_io = Cohttp__String_io
+module IO = Cohttp_lwt__String_io
+module Header_io = Cohttp__Header_io.Make (IO)
+
 module Body_builder (P : Params) = struct
+  let src = Logs.Src.create "cohttp.lwt.jsoo" ~doc:"Cohttp Lwt JSOO module"
+
+  module Log = (val Logs.src_log src : Logs.LOG)
+
   (* perform the body transfer in chunks from string. *)
   let chunked_body_str text =
     let body_len = text##.length in
@@ -94,6 +102,30 @@ module Body_builder (P : Params) = struct
         else
           let u8a = new%js Typed_array.uint8Array_fromBuffer ab in
           CLB.of_string (string_of_uint8array u8a 0 ab##.byteLength)
+
+  let construct_body xml =
+    (* construct body *)
+    let b =
+      let respText () =
+        Js.Opt.case xml##.responseText
+          (fun () -> `String (Js.string ""))
+          (fun s -> `String s)
+      in
+      match xhr_response_supported with
+      | true when Js.Opt.return xml##.response == Js.null ->
+          Log.warn (fun m -> m "XHR Response is null; using empty string");
+          `String (Js.string "")
+      | true ->
+          Js.Opt.case
+            (File.CoerceTo.arrayBuffer xml##.response)
+            (fun () ->
+              Log.warn (fun m ->
+                  m "XHR Response is not an arrayBuffer; using responseText");
+              respText ())
+            (fun ab -> `ArrayBuffer ab)
+      | false -> respText ()
+    in
+    get b
 end
 
 module Make_api (X : sig
@@ -151,10 +183,6 @@ struct
   (* ??? *)
 end
 
-module String_io = Cohttp__String_io
-module IO = Cohttp_lwt__String_io
-module Header_io = Cohttp__Header_io.Make (IO)
-
 module Make_client_async (P : Params) = Make_api (struct
   module Response = Cohttp.Response
   module Request = Cohttp.Request
@@ -182,9 +210,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? *)
-              List.iter
-                (fun v -> xml ## (setRequestHeader (Js.string k) (Js.string v)))
-                v)
+              xml ## (setRequestHeader (Js.string k) (Js.string v)))
             headers
     in
 
@@ -193,45 +219,37 @@ module Make_client_async (P : Params) = Make_api (struct
           match xml##.readyState with
           | XmlHttpRequest.DONE -> (
               try
-                (* construct body *)
-                let body =
-                  let b =
-                    let respText () =
-                      Js.Opt.case xml##.responseText
-                        (fun () -> `String (Js.string ""))
-                        (fun s -> `String s)
-                    in
-                    if xhr_response_supported then
-                      Js.Opt.case
-                        (File.CoerceTo.arrayBuffer xml##.response)
-                        (fun () ->
-                          Firebug.console##log
-                            (Js.string
-                               "XHR Response is not an arrayBuffer; using \
-                                responseText");
-                          respText ())
-                        (fun ab -> `ArrayBuffer ab)
-                    else respText ()
-                  in
-                  Bb.get b
-                in
+                let body = Bb.construct_body xml in
+                (* Note; a type checker subversion seems to be possible here (4.01.0).
+                 * Remove the type constraint on Lwt.task above and return any old
+                 * guff here.  It'll compile and crash in the browser! *)
                 (* (re-)construct the response *)
+                let resp_headers = Js.to_string xml##getAllResponseHeaders in
+                let channel = String_io.open_in resp_headers in
                 let response =
-                  let resp_headers = Js.to_string xml##getAllResponseHeaders in
-                  let channel = String_io.open_in resp_headers in
                   Lwt.(
                     Header_io.parse channel >|= fun resp_headers ->
-                    Response.make ~version:`HTTP_1_1
+                    Cohttp.Response.make ~version:`HTTP_1_1
                       ~status:(C.Code.status_of_code xml##.status)
                       ~flush:false (* ??? *)
                       ~encoding:(CLB.transfer_encoding body)
                       ~headers:resp_headers ())
                 in
-                (* Note; a type checker subversion seems to be possible here (4.01.0).
-                 * Remove the type constraint on Lwt.task above and return any old
-                 * guff here.  It'll compile and crash in the browser! *)
                 Lwt.wakeup wake (response, body)
-              with e -> Lwt.wakeup_exn wake e)
+              with
+              | e
+              (* If we exhaust the stack, it is possible that
+                 Lwt.wakeup just aboves marks the promise as
+                 completed, but raises Stack_overflow while
+                 running the promise callbacks. In this case
+                 waking calling wakeup_exn on the already
+                 completed promise would raise an Invalid_arg
+                 exception, so although the promise is in a
+                 really bad state we may as well let the actual
+                 Stack_overflow exception go through. *)
+              when Lwt.state res = Lwt.Sleep
+              ->
+                Lwt.wakeup_exn wake e)
           | _ -> ());
 
     (* perform call *)
@@ -247,7 +265,6 @@ module Make_client_async (P : Params) = Make_api (struct
         Lwt.return (xml##send (Js.Opt.return (Obj.magic bs))))
     >>= fun () ->
     Lwt.on_cancel res (fun () -> xml##abort);
-
     (* unwrap the response *)
     Lwt.(
       res >>= fun (r, b) ->
@@ -278,12 +295,9 @@ module Make_client_sync (P : Params) = Make_api (struct
       | Some headers ->
           C.Header.iter
             (fun k v ->
-              List.iter
-                (* some headers lead to errors in the javascript console, should
-                   we filter then out here? *)
-                  (fun v ->
-                  xml ## (setRequestHeader (Js.string k) (Js.string v)))
-                v)
+              (* some headers lead to errors in the javascript console, should
+                 we filter then out here? *)
+              xml ## (setRequestHeader (Js.string k) (Js.string v)))
             headers
     in
     (* perform call *)
@@ -294,29 +308,7 @@ module Make_client_sync (P : Params) = Make_api (struct
         let bs = binary_string body in
         xml ## (send (Js.Opt.return (Obj.magic bs))))
     >>= fun _body ->
-    (* TODO: FIXME: looks like an indenting or cut-and-pasto here. Check this - avsm *)
-    (* construct body *)
-    let body =
-      let b =
-        let respText () =
-          Js.Opt.case xml##.responseText
-            (fun () -> `String (Js.string ""))
-            (fun s -> `String s)
-        in
-        if xhr_response_supported then
-          Js.Opt.case
-            (File.CoerceTo.arrayBuffer xml##.response)
-            (fun () ->
-              Firebug.console##log
-                (Js.string
-                   "XHR Response is not an arrayBuffer; using responseText");
-              respText ())
-            (fun ab -> `ArrayBuffer ab)
-        else respText ()
-      in
-      Bb.get b
-    in
-
+    let body = Bb.construct_body xml in
     (* (re-)construct the response *)
     let resp_headers = Js.to_string xml##getAllResponseHeaders in
     Header_io.parse (String_io.open_in resp_headers) >>= fun resp_headers ->
index 71b6382c40475bbcfdf0d0060857e5acb28e48d0..e1b330e7f15bb21491fddaa1f666fc4b31ce8f14 100644 (file)
  *
   }}}*)
 
-(** HTTP client for JavaScript using XMLHttpRequest. *)
+(** {1 HTTP client for JavaScript using XMLHttpRequest.}
+
+    The {!Logs} source name for this module's logger is ["cohttp.lwt.jsoo"]. To
+    log the current warnings using the browser's console log, you can write a
+    custom reporter or use:
+
+    {[
+      let reporter = Logs_browser.console_reporter () in
+      Logs.set_reporter reporter
+    ]} *)
 
 (** Configuration parameters for the XmlHttpRequest engines *)
 module type Params = sig
index 9b1cc0a4a7e57e1c641a07406966c3221981d624..18a372c53a2ce4ab02e4b203cfc92a20ff75b033 100644 (file)
@@ -4,4 +4,4 @@
  (synopsis "XHR/Lwt based http client")
  (preprocess
   (pps js_of_ocaml-ppx))
- (libraries js_of_ocaml cohttp-lwt))
+ (libraries js_of_ocaml cohttp-lwt logs))
index a45d8c827f7cf2d35fb0a09392e25e6fed009966..9bf319cabadc955bb5748182090c315000cc7f1c 100644 (file)
@@ -1,4 +1,3 @@
-version: "4.0.0"
 opam-version: "2.0"
 maintainer: "anil@recoil.org"
 authors: [
@@ -27,22 +26,22 @@ doc: "https://mirage.github.io/ocaml-cohttp/"
 bug-reports: "https://github.com/mirage/ocaml-cohttp/issues"
 depends: [
   "ocaml" {>= "4.08"}
-  "dune" {>= "1.1.0"}
-  "conduit-lwt" {>= "1.0.3" & < "3.0.0"}
-  "conduit-lwt-unix" {>= "1.0.3" & < "3.0.0"}
+  "dune" {>= "2.0"}
+  "conduit-lwt" {>= "5.0.0"}
+  "conduit-lwt-unix" {>= "5.0.0"}
   "cmdliner"
   "magic-mime"
   "logs"
   "fmt" {>= "0.8.2"}
-  "cohttp-lwt" {=version}
+  "cohttp-lwt" {= version}
   "ppx_sexp_conv" {>= "v0.13.0"}
   "lwt" {>= "3.0.0"}
   "base-unix"
   "ounit" {with-test}
 ]
 build: [
-  ["dune" "subst"] {pinned}
+  ["dune" "subst"] {dev}
   ["dune" "build" "-p" name "-j" jobs]
   ["dune" "runtest" "-p" name "-j" jobs] {with-test}
 ]
-dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
\ No newline at end of file
+dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
index 2ca0785f4230a0202ab96a777f519b3c1075ecfa..c31dafebdd6a8023696c2fb54d5ee444dbc401d2 100644 (file)
@@ -48,15 +48,12 @@ let client uri ofile meth' =
       | None -> output_body Lwt_io.stdout
       | Some fname -> Lwt_io.with_file ~mode:Lwt_io.output fname output_body)
 
-let run_client verbose ofile uri meth =
-  Lwt_main.run
-    ( (if verbose then (
-       (* activate debug sets the reporter *)
-       Cohttp_lwt_unix.Debug.activate_debug ();
-       Log.debug (fun d -> d ">>> Debug active");
-       return ())
-      else return ())
-    >>= fun () -> client uri ofile meth )
+let run_client level ofile uri meth =
+  if not @@ Debug.debug_active () then (
+    Fmt_tty.setup_std_outputs ();
+    Logs.set_level ~all:true level;
+    Logs.set_reporter Debug.default_reporter);
+  Lwt_main.run (client uri ofile meth)
 
 open Cmdliner
 
@@ -77,9 +74,7 @@ let meth =
   let doc = "Set http method" in
   Arg.(value & opt string "GET" & info [ "X"; "request" ] ~doc)
 
-let verb =
-  let doc = "Display additional debugging to standard error." in
-  Arg.(value & flag & info [ "v"; "verbose" ] ~doc)
+let verb = Logs_cli.level ()
 
 let ofile =
   let doc = "Output filename to store the URI into." in
index 923b4936f2f723f3a1dc6d97e5797706362e9e6e..9fddad8639aaf92380554610a09e729a66d7be37 100644 (file)
@@ -73,8 +73,12 @@ let start_proxy port host verbose cert key () =
   in
   Server.create ~mode config
 
-let lwt_start_proxy port host verbose cert key =
-  Lwt_main.run (start_proxy port host verbose cert key ())
+let lwt_start_proxy port host level cert key =
+  if not @@ Debug.debug_active () then (
+    Fmt_tty.setup_std_outputs ();
+    Logs.set_level ~all:true level;
+    Logs.set_reporter Debug.default_reporter);
+  Lwt_main.run (start_proxy port host (level <> None) cert key ())
 
 open Cmdliner
 
@@ -86,9 +90,7 @@ let port =
   let doc = "TCP port to listen on." in
   Arg.(value & opt int 8080 & info [ "p" ] ~docv:"PORT" ~doc)
 
-let verb =
-  let doc = "Logging output to console." in
-  Arg.(value & flag & info [ "v"; "verbose" ] ~doc)
+let verb = Logs_cli.level ()
 
 let ssl_cert =
   let doc = "SSL certificate file." in
index 944b90c9637ac648e69088d8cfa7470024c52c37..b4c2b63b8ae45676f366b2651091e166472ac63f 100644 (file)
@@ -128,11 +128,11 @@ let start_server docroot port host index tls () =
   let ctx = Cohttp_lwt_unix.Net.init ~ctx () in
   Server.create ~ctx ~mode config
 
-let lwt_start_server docroot port host index verbose tls =
-  if verbose <> None then (
-    (* activate_debug sets the reporter *)
-    Cohttp_lwt_unix.Debug.activate_debug ();
-    Logs.set_level verbose);
+let lwt_start_server docroot port host index level tls =
+  if not @@ Debug.debug_active () then (
+    Fmt_tty.setup_std_outputs ();
+    Logs.set_level ~all:true level;
+    Logs.set_reporter Debug.default_reporter);
   Lwt_main.run (start_server docroot port host index tls ())
 
 open Cmdliner
index a76a0c4c00ff5e79e96962b48f351754d790c61c..63e95e9032098be3f87c3b40a217d66944f169ca 100644 (file)
@@ -1,6 +1,6 @@
 (executables
  (names cohttp_curl_lwt cohttp_proxy_lwt cohttp_server_lwt)
  (libraries cohttp-lwt-unix cohttp_server logs logs.lwt logs.fmt logs.cli
-   cmdliner conduit-lwt)
+   cmdliner conduit-lwt fmt.tty)
  (package cohttp-lwt-unix)
  (public_names cohttp-curl-lwt cohttp-proxy-lwt cohttp-server-lwt))
index 090f21e3592fcef45b23855d9247f28ce2c1a256..2d550017ab7c4d83f45de3906ea769032b8a5ac5 100644 (file)
@@ -19,7 +19,7 @@ let debug_active () = !_debug_active
 
 open Lwt.Infix
 
-let default_reporter (file_descr, ppf) =
+let reporter file_descr ppf =
   let ppf, flush =
     let buf = Buffer.create 0x100 in
     ( Fmt.with_buffer ~like:ppf buf,
@@ -62,18 +62,21 @@ let default_reporter (file_descr, ppf) =
   in
   { Logs.report }
 
-let set_log =
+let default_reporter = reporter Lwt_unix.stderr Fmt.stderr
+
+let set_logger =
   lazy
-    ((* If no reporter has been set by the application, set default one
+    (if
+     (* If no reporter has been set by the application, set default one
         that prints to stderr *)
-     if Logs.reporter () == Logs.nop_reporter then
-       Logs.set_level @@ Some Logs.Debug;
-     Logs.set_reporter (default_reporter (Lwt_unix.stderr, Fmt.stderr)))
+     Logs.reporter () == Logs.nop_reporter
+    then Logs.set_reporter default_reporter)
 
 let activate_debug () =
-  Lazy.force set_log;
   if not !_debug_active then (
     _debug_active := true;
+    Lazy.force set_logger;
+    Logs.set_level ~all:true (Some Logs.Debug);
     Logs.debug (fun f -> f "Cohttp debugging output is active"))
 
 let () =
index 1261d2537e4b272960aef6d77ed5dcd909bf844d..d14e7124d37f6a839cfe335dbd305426cd4f1567 100644 (file)
 
 (** Debugging output for Cohttp Unix *)
 
+val default_reporter : Logs.reporter
+(** [default_reporter] provides a simple reporter that sends the logging output
+    to stderr. For example, the code below enables logging at level [level] to
+    stderr, using coloured output if possible.
+
+    {[
+      Fmt_tty.setup_std_outputs ();
+      Logs.set_level ~all:true (Some level);
+      Logs.set_reporter Debug.default_reporter
+    ]} *)
+
 val activate_debug : unit -> unit
 (** [activate_debug] enables debugging output that will be sent to standard
     error. *)
@@ -23,3 +34,19 @@ val activate_debug : unit -> unit
 val debug_active : unit -> bool
 (** [debug_active] returns true if [activate_debug] has been called and false
     otherwise *)
+
+(** {2 Selectively disable cohttp logging} *)
+
+(** It is possible to selectively disable cohttp internal logginb by filtering
+    over the various modules logs names as follows.
+
+    {[
+      (* Set log level v for all loggers, this does also affect cohttp internal loggers *)
+      Logs.set_level ~all:true level;
+      (* Disable all cohttp-lwt and cohttp-lwt-unix logs *)
+      List.iter (fun src ->
+          match Logs.Src.name src with
+          | "cohttp.lwt.io" | "cohttp.lwt.server" -> Logs.Src.set_level src None
+          | _ -> ())
+      @@ Logs.Src.list ()
+    ]} *)
index 9b088417e7ea207b7a0f7ec0865fb2a8e9b7c6ef..2862ac1743920b16ff0d6b7e1f862356b5f8ac0b 100644 (file)
  *
   }}}*)
 
+(** The [Io] module contains the IO implementation for [cohttp-lwt-unix].
+
+    The {!Logs} source name for this module logger is ["cohttp.lwt.io"]. Refer
+    to the {!Debug} module for further details.*)
+
 include
   Cohttp_lwt.S.IO
     with type ic = Lwt_io.input_channel
index 146d249f83086775179dd231a35fbeec6a9ebdf2..e32ca95c36484c779f526e74b8cc78f0089f1bd9 100644 (file)
@@ -23,12 +23,15 @@ module IO = Io
 type ctx = { ctx : Conduit_lwt_unix.ctx; resolver : Resolver_lwt.t }
 [@@deriving sexp_of]
 
-let init ?(ctx = Conduit_lwt_unix.default_ctx)
+let init ?(ctx = Lazy.force Conduit_lwt_unix.default_ctx)
     ?(resolver = Resolver_lwt_unix.system) () =
   { ctx; resolver }
 
 let default_ctx =
-  { resolver = Resolver_lwt_unix.system; ctx = Conduit_lwt_unix.default_ctx }
+  {
+    resolver = Resolver_lwt_unix.system;
+    ctx = Lazy.force Conduit_lwt_unix.default_ctx;
+  }
 
 let connect_uri ~ctx:{ ctx; resolver } uri =
   Resolver_lwt.resolve_uri ~uri resolver >>= fun endp ->
index ca94d33b622e10c6b2fef687568fbed6c7360dea..e33280e0faec43714d1057ad1c59250969c0e193 100644 (file)
@@ -2,7 +2,8 @@ module Server_core = Cohttp_lwt.Make_server (Io)
 include Server_core
 open Lwt.Infix
 
-let src = Logs.Src.create "cohttp.lwt.server" ~doc:"Cohttp Lwt server module"
+let src =
+  Logs.Src.create "cohttp.lwt.server" ~doc:"Cohttp Lwt Unix server module"
 
 module Log = (val Logs.src_log src : Logs.LOG)
 
index 7fe1641728fc8aef5c0f54753572d2a066e7aa65..37d75150e696260c6bb7ae850c82b5be01305b46 100644 (file)
@@ -1,5 +1,8 @@
 (** The [Server] module implements the full UNIX HTTP server interface,
-    including the UNIX-specific functions defined in {!S}. *)
+    including the UNIX-specific functions defined in {!S}.
+
+    The {!Logs} source name for this module logger is ["cohttp.lwt.server"].
+    Refer to the {!Debug} module for further details. *)
 
 include Cohttp_lwt.S.Server with module IO = Io
 
index 000d1ea56b62bfc0d7dd408e7e46bb981836a98b..3bf2692f785fcfd8f6855b8a00b9303af66d47bd 100644 (file)
@@ -24,6 +24,8 @@ let basic_res =
    Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)\r\n\
    Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT\r\n\
    Etag: \"3f80f-1b6-3e1cb03b\"\r\n\
+   Accept: text/*\r\n\
+   Accept: application/xml\r\n\
    Accept-Ranges:  none\r\n\
    Content-Length: 0\r\n\
    Connection: close\r\n\
@@ -244,13 +246,9 @@ let make_simple_req () =
   let open Cohttp in
   let open Cohttp_lwt_unix in
   let expected =
-    "POST /foo/bar HTTP/1.1\r\n\
-     foo: bar\r\n\
-     host: localhost\r\n\
-     transfer-encoding: chunked\r\n\
-     user-agent: "
+    "POST /foo/bar HTTP/1.1\r\nFoo: bar\r\nhost: localhost\r\nuser-agent: "
     ^ user_agent
-    ^ "\r\n\r\n6\r\nfoobar\r\n0\r\n\r\n"
+    ^ "\r\ntransfer-encoding: chunked\r\n\r\n6\r\nfoobar\r\n0\r\n\r\n"
   in
   let req =
     Request.make ~encoding:Transfer.Chunked ~meth:`POST
@@ -263,13 +261,9 @@ let mutate_simple_req () =
   let open Cohttp in
   let open Cohttp_lwt_unix in
   let expected =
-    "POST /foo/bar HTTP/1.1\r\n\
-     foo: bar\r\n\
-     host: localhost\r\n\
-     transfer-encoding: chunked\r\n\
-     user-agent: "
+    "POST /foo/bar HTTP/1.1\r\nfoo: bar\r\nhost: localhost\r\nuser-agent: "
     ^ user_agent
-    ^ "\r\n\r\n6\r\nfoobar\r\n0\r\n\r\n"
+    ^ "\r\ntransfer-encoding: chunked\r\n\r\n6\r\nfoobar\r\n0\r\n\r\n"
   in
   let req =
     Request.make ~encoding:Transfer.Chunked
index e1ffd5c60401dec1891e390288c85b3c8f7418d7..bd1bd54dab89221e0c384aa86ef8034df584883b 100644 (file)
@@ -1,4 +1,3 @@
-version: "4.0.0"
 opam-version: "2.0"
 maintainer: "anil@recoil.org"
 authors: [
@@ -27,8 +26,8 @@ doc: "https://mirage.github.io/ocaml-cohttp/"
 bug-reports: "https://github.com/mirage/ocaml-cohttp/issues"
 depends: [
   "ocaml" {>= "4.08"}
-  "dune" {>= "1.1.0"}
-  "cohttp" {=version}
+  "dune" {>= "2.0"}
+  "cohttp" {= version}
   "lwt" {>= "2.5.0"}
   "sexplib0"
   "ppx_sexp_conv" {>= "v0.13.0"}
@@ -36,8 +35,8 @@ depends: [
   "uri" {>= "2.0.0"}
 ]
 build: [
-  ["dune" "subst"] {pinned}
+  ["dune" "subst"] {dev}
   ["dune" "build" "-p" name "-j" jobs]
   ["dune" "runtest" "-p" name "-j" jobs] {with-test}
 ]
-dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
\ No newline at end of file
+dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
index 106fbe149c7e38dbb7582c84b3e06b123c83c9eb..533da1df07796ebb4810bb346d6dd8563d5f4894 100644 (file)
@@ -6,6 +6,10 @@ module Make (IO : S.IO) (Net : S.Net with module IO = IO) = struct
   module Response = Make.Response (IO)
   module Request = Make.Request (IO)
 
+  let src = Logs.Src.create "cohttp.lwt.client" ~doc:"Cohttp Lwt client"
+
+  module Log = (val Logs.src_log src : Logs.LOG)
+
   type ctx = Net.ctx
 
   let read_body ~closefn ic res =
@@ -25,7 +29,11 @@ module Make (IO : S.IO) (Net : S.Net with module IO = IO) = struct
         Gc.finalise_last
           (fun () ->
             if not !closed then
-              prerr_endline "Cohttp_lwt: body not consumed - leaking stream!")
+              Log.warn (fun m ->
+                  m
+                    "Body not consumed, leaking stream! Refer to \
+                     https://github.com/mirage/ocaml-cohttp/issues/730 for \
+                     additional details"))
           stream;
         body
     | `No ->
index 4846193b25714d7ded255ac8a80f4d11032c90eb..3e27432888e749259ca346bf5f3e5703c68691e8 100644 (file)
@@ -1,6 +1,10 @@
 (** The [Make] functor glues together a {!Cohttp.S.IO} implementation to send
     requests down a connection that is established by the {!Net} module. The
-    resulting module satisfies the {!Client} module type. *)
+    resulting module satisfies the {!Client} module type.
+
+    The {!Logs} source name for this module's logger is ["cohttp.lwt.client"].
+    When logging is enabled (at least {b warning} level), eventual body leaks
+    will be logged and easier to track. *)
 
 module Make (IO : S.IO) (Net : S.Net with module IO = IO) :
   S.Client with type ctx = Net.ctx
index 757d0e619fce3527449efdc170b5e6d05aa7682f..fe5b33d2cb60cd465094a1085ebc88ab55492190 100644 (file)
@@ -57,6 +57,17 @@ module type Client = sig
       interface rather than invoke this function directly. See {!head}, {!get}
       and {!post} for some examples.
 
+      To avoid leaks, the body needs to be consumed, using the functions
+      provided in the {!Body} module and, if not necessary, should be explicitly
+      drained calling {!Body.drain_body}. Leaks are logged as debug messages by
+      the client, 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)
+      ]}
+
       Depending on [ctx], the library is able to send a simple HTTP request or
       an encrypted one with a secured protocol (such as TLS). Depending on how
       conduit is configured, [ctx] might initiate a secured connection with TLS
index bbc46a07ff53c45f17d60062f0bd6b18ab61cd8b..fb0dccc1d63ebe71be00d45262b318037cb2b08e 100644 (file)
@@ -1,4 +1,7 @@
 (** The [Make] functor glues together a {!Cohttp.S.IO} implementation to send
     requests down a connection that is established by the user. The resulting
-    module satisfies the {!Server} module type. *)
+    module satisfies the {!Server} module type.
+
+    The {!Logs} source name for this module's logger is ["cohttp.lwt.server"].*)
+
 module Make (IO : S.IO) : S.Server with module IO = IO
index e017ab8e493a0cbadcf47afccd31ec984d430e2a..fffebe0eb6b77c55b5aaa2b96c99dd2096a287c6 100644 (file)
@@ -1,4 +1,3 @@
-version: "4.0.0"
 opam-version: "2.0"
 maintainer: "anil@recoil.org"
 authors: ["Anil Madhavapeddy" "Thomas Gazagnaire"]
@@ -20,22 +19,23 @@ Please see <https://mirage.io> for a self-hosted explanation
 and instructions on how to use this library."""
 depends: [
   "ocaml" {>= "4.08"}
-  "dune" {>= "1.1.0"}
+  "dune" {>= "2.0"}
   "mirage-flow" {>= "2.0.0"}
   "mirage-channel" {>= "4.0.0"}
-  "conduit" {>= "2.0.2" & <"3.0.0"}
-  "conduit-mirage" {>= "2.0.2" & <"3.0.0"}
+  "conduit" {>= "2.0.2"}
+  "conduit-mirage" {>= "2.0.2"}
   "mirage-kv" {>= "3.0.0"}
   "lwt" {>= "2.4.3"}
-  "cohttp" {=version}
-  "cohttp-lwt" {=version}
+  "cohttp" {= version}
+  "cohttp-lwt" {= version}
+  "fmt" {>= "0.8.7"}
   "astring"
   "magic-mime"
   "ppx_sexp_conv" {>= "v0.13.0"}
 ]
 build: [
-  ["dune" "subst"] {pinned}
+  ["dune" "subst"] {dev}
   ["dune" "build" "-p" name "-j" jobs]
   ["dune" "runtest" "-p" name "-j" jobs] {with-test}
 ]
-dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
\ No newline at end of file
+dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
index a3d5399ed5965c208a1c28e8a0be692911098849..bd65b06b13426f3f0433bac883d6eddb1562af8b 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 v4.0.0
+ * %%NAME%% %%VERSION%%
  *)
 
 open Lwt.Infix
index 9bb300004390560c16661bb3432f117fea2287c9..fe2f9d7668f5d25a93cd132efe3b8b0b03f5ebb1 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 v4.0.0
+ * %%NAME%% %%VERSION%%
  *)
 
 open Lwt.Infix
index db05f2077aa85709703d12f245850cd8046bcbba..23e9792f90fdc0107d8eb79a5acec2599fac6b02 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 v4.0.0
+ * %%NAME%% %%VERSION%%
  *)
 
 (** Cohttp IO implementation using Mirage channels. *)
index 9b269125d3b3ca7beeaa9ddb5ce2e17da874241a..6ca5b56fd7db593ab704e6eb5ab3324781382ad6 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 v4.0.0
+ * %%NAME%% %%VERSION%%
  *)
 
 module Key = Mirage_kv.Key
@@ -23,7 +23,7 @@ module HTTP (FS : Mirage_kv.RO) (S : Cohttp_lwt.S.Server) = struct
   open Lwt.Infix
   open Astring
 
-  let failf fmt = Fmt.kstrf Lwt.fail_with fmt
+  let failf fmt = Fmt.kstr Lwt.fail_with fmt
 
   let read_fs t name =
     FS.get t (Key.v name) >>= function
@@ -59,7 +59,7 @@ module HTTP (FS : Mirage_kv.RO) (S : Cohttp_lwt.S.Server) = struct
               in
               S.respond_string ~status:`OK ~body ~headers ())
             (fun _exn ->
-              let with_index = Fmt.strf "%s/index.html" path in
+              let with_index = Fmt.str "%s/index.html" path in
               exists fs with_index >>= function
               | true -> fn fs (Uri.with_path uri with_index)
               | false -> S.respond_not_found ())
index 06138573af664a511a42d056fbeb41208bf72bce..8f73c992c373bbfaab2e270f4b89fd3c25b3c55c 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 v4.0.0
+ * %%NAME%% %%VERSION%%
  *)
 
 (** Serve static HTTP sites from a Mirage key-value store. *)
index 51b52b86ab497953637d924ad765f8f498928261..9b21332b863b388e8f8a1cf90f50deb537e2bd3f 100644 (file)
@@ -1,4 +1,3 @@
-version: "4.0.0"
 opam-version: "2.0"
 maintainer: "anil@recoil.org"
 authors: [
@@ -23,12 +22,12 @@ doc: "https://mirage.github.io/ocaml-cohttp/"
 bug-reports: "https://github.com/mirage/ocaml-cohttp/issues"
 depends: [
   "ocaml" {>= "4.08"}
-  "dune" {>= "1.1.0"}
-  "cohttp" {=version}
+  "dune" {>= "2.0"}
+  "cohttp" {= version}
 ]
 build: [
-  ["dune" "subst"] {pinned}
+  ["dune" "subst"] {dev}
   ["dune" "build" "-p" name "-j" jobs]
   ["dune" "runtest" "-p" name "-j" jobs] {with-test}
 ]
-dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
\ No newline at end of file
+dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
index 32fabb8b8c686fbcc4338dae4b35367927157b8a..d89e7f97671429983bafd9cef260947dc686b853 100644 (file)
@@ -1,4 +1,3 @@
-version: "4.0.0"
 opam-version: "2.0"
 maintainer: "anil@recoil.org"
 authors: [
@@ -34,7 +33,7 @@ doc: "https://mirage.github.io/ocaml-cohttp/"
 bug-reports: "https://github.com/mirage/ocaml-cohttp/issues"
 depends: [
   "ocaml" {>= "4.08"}
-  "dune" {>= "2.0.0"}
+  "dune" {>= "2.0"}
   "re" {>= "1.9.0"}
   "uri" {>= "2.0.0"}
   "uri-sexp"
@@ -45,10 +44,11 @@ depends: [
   "fmt" {with-test}
   "jsonm" {build}
   "alcotest" {with-test}
+  "crowbar" {with-test}
 ]
 build: [
-  ["dune" "subst"] {pinned}
+  ["dune" "subst"] {dev}
   ["dune" "build" "-p" name "-j" jobs]
   ["dune" "runtest" "-p" name "-j" jobs] {with-test}
 ]
-dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
\ No newline at end of file
+dev-repo: "git+https://github.com/mirage/ocaml-cohttp.git"
diff --git a/cohttp/fuzz/dune b/cohttp/fuzz/dune
new file mode 100644 (file)
index 0000000..88906b3
--- /dev/null
@@ -0,0 +1,26 @@
+(executable
+ (name fuzz_header)
+ (libraries crowbar cohttp))
+
+(rule
+ (alias runtest)
+ (package cohttp)
+ (action
+  (run ./fuzz_header.exe)))
+
+(rule
+ (alias fuzz)
+ (deps
+  (:exe fuzz_header.exe)
+  (source_tree inputs))
+ (action
+  (run afl-fuzz -i inputs -o findings -- ./%{exe} @@)))
+
+(rule
+ (alias bun-fuzz)
+ (locks %{project_root}/bun)
+ (deps
+  (:exe fuzz_me.exe)
+  (source_tree input))
+ (action
+  (run bun --input inputs --output findings -- ./%{exe})))
diff --git a/cohttp/fuzz/fuzz_header.ml b/cohttp/fuzz/fuzz_header.ml
new file mode 100644 (file)
index 0000000..3602e99
--- /dev/null
@@ -0,0 +1,567 @@
+(*{{{ Copyright (c) 2021 Carine Morel <carine@tarides.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *}}}*)
+
+module H = Cohttp.Header
+
+(** Here, we test the Header module with fuzzing. Some of these tests may be
+    redundant with Alcotest tests.
+
+    The tests are launched with [dune runtest] but can also be run with [afl]
+    with the command line : [dune build @cohttp/fuzz/fuzz --no-buffer].
+
+    The tests below reflects the semantics we want for each function, however in
+    some cases, it may actually be specific to the current implementation and
+    does not necessary need to be enforced in future implementations. To make it
+    clear, tests are annoted by their categories:
+
+    - FS (Functions semantics): tests the semantics described in the
+      documentation.
+
+    - SI (Specific to current Implementation): these tests are here to check the
+      implementation is doing what we think it is doing but may change
+      accordingly to implementation changes. *)
+
+(* Generators *)
+let list_value_headers =
+  [|
+    "accept";
+    "accept-charset";
+    "accept-encoding";
+    "accept-language";
+    "accept-ranges";
+    "allow";
+    "cache-control";
+    "connection";
+    "content-encoding";
+    "content-language";
+    "expect";
+    "if-match";
+    "if-none-match";
+    "link";
+    "pragma";
+    "proxy-authenticate";
+    "te";
+    "trailer";
+    "transfer-encoding";
+    "upgrade";
+    "vary";
+    "via";
+    "warning";
+    "www-authenticate";
+  |]
+
+(** Pick a random list-value header name from a predefined array of values. *)
+let list_value_header_gen =
+  let open Crowbar in
+  let gen =
+    map
+      [ range (Array.length list_value_headers) ]
+      (fun i -> list_value_headers.(i))
+  in
+  let printer fmt str = pp fmt "%s" str in
+  with_printer printer gen
+
+(** Generate a tchar following
+    {{:https://tools.ietf.org/html/rfc7230#appendix-B} RFC 7230}.
+
+    tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" /
+    "_" / "`" / "|" / "~" / DIGIT / ALPHA *)
+let tchar_gen =
+  let tchar_code_gen =
+    let uppercased_letter = Crowbar.range ~min:65 26 in
+    let lowercased_letter = Crowbar.range ~min:97 26 in
+    let others =
+      List.map
+        (fun i -> Crowbar.const i)
+        [
+          33 (* ! *);
+          35 (* # *);
+          36 (* $ *);
+          37 (* % *);
+          38 (* & *);
+          42 (* * *);
+          43 (* + *);
+          45 (* - *);
+          46 (* . *);
+          94 (* ^ *);
+          95 (* _ *);
+          96 (* ` *);
+          124 (* | *);
+          126 (* ~ *);
+        ]
+      |> Crowbar.choose
+    in
+    let digit_and_others = Crowbar.(choose [ others; range ~min:48 10 ]) in
+    Crowbar.(choose [ lowercased_letter; uppercased_letter; digit_and_others ])
+  in
+  Crowbar.(map [ tchar_code_gen ] (fun i -> Char.escaped (Char.chr i)))
+
+(** Generate a non-empty word of arbitrary length (composed of tchar only). *)
+let word_gen =
+  let open Crowbar in
+  let gen =
+    fix (fun word_gen ->
+        choose
+          [
+            (* one letter word *)
+            tchar_gen;
+            (* two letters word *)
+            map [ tchar_gen; tchar_gen ] (fun l1 l2 -> l1 ^ l2);
+            (* add one letter *)
+            map [ tchar_gen; word_gen ] (fun l w -> l ^ w);
+          ])
+  in
+  let printer = pp_string in
+  with_printer printer gen
+
+(** Generate an header name: either a predefined list-value header or a random
+    word *)
+let header_name_gen =
+  let open Crowbar in
+  let gen = choose [ list_value_header_gen; word_gen ] in
+  let printer = pp_string in
+  with_printer printer gen
+
+let header_printer fmt (k, v) = Crowbar.pp fmt "%s, %s" k v
+
+(** Generate a header key/value pair *)
+let header_gen : (string * string) Crowbar.gen =
+  let open Crowbar in
+  let gen_setcookie = pair (const "Set-cookie") word_gen in
+  let gen_otherheader = pair header_name_gen word_gen in
+  let gen =
+    (* one in ten generated header is a "set-cookie" header *)
+    choose (gen_setcookie :: List.init 9 (fun _ -> gen_otherheader))
+  in
+  with_printer header_printer gen
+
+(** Generate a list of headers *)
+let header_list_gen : (string * string) list Crowbar.gen =
+  let open Crowbar in
+  let gen = list header_gen in
+  let printer = pp_list header_printer in
+  with_printer printer gen
+
+(** Generate a [Cohttp.Header.t] headers. *)
+let headers_gen : H.t Crowbar.gen =
+  let open Crowbar in
+  let gen =
+    fix (fun headers_gen ->
+        choose
+          [
+            (* empty header *)
+            const (H.init ());
+            (* add one pair (k, v) *)
+            map [ header_gen; headers_gen ] (fun (k, v) h -> H.add h k v);
+            (* add a list of headers *)
+            map [ headers_gen; header_list_gen ] (fun h l -> H.add_list h l);
+          ])
+  in
+  let printer fmt h = Crowbar.pp fmt "\n%s@." (H.to_string h) in
+  with_printer printer gen
+
+(* Tests *)
+(* Important note : keys must be lowercased before comparison *)
+let eqssl l1 l2 =
+  List.map (fun (k, v) -> (String.lowercase_ascii k, v)) l1
+  = List.map (fun (k, v) -> (String.lowercase_ascii k, v)) l2
+
+let is_empty_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall h, k, v. is_empty (add h k v) = false) *)
+    add_test ~name:"[is_empty] returns false on a non empty header"
+      [ headers_gen; header_name_gen; word_gen ] (fun h k v ->
+        check_eq false H.(is_empty (add h k v))))
+
+let init_with_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall k v. to_list (init_with k v) = [k, v] *)
+    add_test ~name:"[init_list k v] is [k, v]" [ header_name_gen; word_gen ]
+      (fun k v -> check_eq H.(to_list (init_with k v)) [ (k, v) ]))
+
+let mem_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall k. mem (init ()) k = false *)
+    add_test ~name:"[mem h k] on an empty header is always false"
+      [ header_name_gen ] (fun k -> check_eq false H.(mem (init ()) k));
+    (* SI *)
+    (* forall h, k. H.mem h k = List.(mem_assoc (String.lowercase_ascii x) (List.map (fun (k, v) -> String.lowercase_ascii k, v) (H.to_list h))) *)
+    add_test ~name:"Header.mem has the same behavior than List.mem_assoc"
+      [ headers_gen; header_name_gen ] (fun h k ->
+        check_eq
+          H.(mem h k)
+          List.(
+            mem_assoc (String.lowercase_ascii k)
+              (List.map
+                 (fun (k, v) -> (String.lowercase_ascii k, v))
+                 (H.to_list h)))))
+
+let add_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall k, v, h. mem (add h k v) k = true *)
+    add_test ~name:"mem (add h k v) k  = true"
+      [ headers_gen; header_name_gen; word_gen ] (fun h k v ->
+        check_eq true H.(mem (add h k v) k));
+    add_test
+    (* FS *)
+    (* forall h, k, v. to_list (add h k v) = to_list h @ [lowercase k, v] *)
+      ~name:"[add] adds a value at the header end"
+      [ headers_gen; header_name_gen; word_gen ] (fun h k v ->
+        check_eq (H.to_list h @ [ (k, v) ]) H.(to_list (add h k v))))
+
+let to_list_of_list_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall h. to_list (of_list h) = h (with lowercase key comparison) *)
+    add_test ~name:"to_list (of_list h) = h" [ header_list_gen ] (fun h ->
+        check_eq ~eq:eqssl H.(to_list (of_list h)) h);
+
+    (* FS and RFC *)
+    (* forall h, k1, v1, k2, v2. to_list (add (add h k1 v1) k2 v2) = to_list \
+       h @ [k1, v1; k2, v2] *)
+    add_test ~name:"checking [to_list] order after multiple [add] calls"
+      [ headers_gen; header_name_gen; word_gen; header_name_gen; word_gen ]
+      (fun h k1 v1 k2 v2 ->
+        check_eq ~eq:eqssl
+          H.(to_list (add (add h k1 v1) k2 v2))
+          H.(to_list h @ [ (k1, v1); (k2, v2) ])))
+
+let add_opt_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall hopt, k, v.
+              add_opt hopt k v = | add h k v     if hopt = Some h
+                                 | init_with k v if hopt = None *)
+    add_test ~name:"add_opt (Some h) = add and add_opt None = init_with"
+      [ option headers_gen; header_name_gen; word_gen ]
+      (fun hopt k v ->
+        check_eq
+          H.(match hopt with None -> init_with k v | Some h -> add h k v)
+          H.(add_opt hopt k v)))
+
+let add_unless_exists_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall h, k, v. if mem h k = true then add_unless_exists h k v = h *)
+    add_test ~name:"[add_unless_exists h k v] does nothing if k exists"
+      [ headers_gen; header_list_gen; header_name_gen; word_gen; word_gen ]
+      (fun h l k v1 v2 ->
+        (* A random header such as mem h k = true *)
+        let h = H.(add_list (add h k v1) l) in
+        check_eq H.(add_unless_exists h k v2) h);
+    (* FS *)
+    (* forall h, k, v. if mem h k = false then add_unless_exists h k v = add \
+         h k v *)
+    add_test ~name:"add_unless_exists = add if key does not exist"
+      [ headers_gen; header_name_gen; word_gen ] (fun h k v ->
+        (* Making sure as mem h k = false *)
+        guard (not (H.mem h k));
+        check_eq H.(add_unless_exists h k v) H.(add h k v)))
+
+let add_list () =
+  Crowbar.(
+    (* FS *)
+    (* forall h, l. to_list (add_list h l) = to_list h @ l *)
+    add_test
+      ~name:"[add_list h l] adds all headers in [l] in order at the end of [h]"
+      [ headers_gen; header_list_gen ] (fun h l ->
+        check_eq ~eq:eqssl H.(to_list (add_list h l)) H.(to_list h @ l)))
+
+let add_multi () =
+  Crowbar.(
+    (* FS *)
+    (* forall h, k, vs. add_multi h k vs = add_list h (List.map (fun v -> k, v) vs) *)
+    add_test ~name:"[add_list] and [add_multi] have compatible semantics"
+      [ headers_gen; header_name_gen; list word_gen ]
+      (fun h k vs ->
+        check_eq
+          H.(add_multi h k vs)
+          H.(add_list h (List.map (fun v -> (k, v)) vs)));
+    (* FS *)
+    (* forall h, k, l. get_multi (add_multi h k l) k = get_multi h k @ l *)
+    add_test ~name:"get_multi (add_multi h k l) k = get_multi h k @ l"
+      [ headers_gen; header_name_gen; Crowbar.list word_gen ]
+      (fun h k l ->
+        check_eq H.(get_multi (add_multi h k l) k) H.(get_multi h k @ l)))
+
+let get_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall h k, if mem h k = false then get h k = None *)
+    add_test ~name:"[get h k] returns None if k does not exists in h"
+      [ headers_gen; header_name_gen ] (fun h k ->
+        guard H.(not (mem h k));
+        check_eq H.(get h k) None);
+    (* FS *)
+    (* forall h k, get (add h k v) = Some v *)
+    add_test ~name:"get (add h k v) = Some v"
+      [ headers_gen; header_name_gen; word_gen ] (fun h k v ->
+        check_eq H.(get (add h k v) k) (Some v)))
+
+let get_multi_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall h k, if mem h k = false then get_multi h k = [] *)
+    add_test ~name:"[get_multi h k] returns [] if k does not exists in h"
+      [ headers_gen; header_name_gen ] (fun h k ->
+        guard H.(not (mem h k));
+        check_eq H.(get_multi h k) []);
+    (* FS *)
+    (* forall l1, l2, k, v.
+          get_multi (of_list (l1 @ [ (k, v) ] @ l2)) k =
+          get_multi (of_list l1) k @ [ v ] @ get_multi (of_list l2) k *)
+    add_test ~name:"[get_multi] returns values in transmission order"
+      [ header_list_gen; header_list_gen; header_name_gen; word_gen ]
+      (fun l1 l2 k v ->
+        check_eq
+          H.(get_multi (of_list (l1 @ [ (k, v) ] @ l2)) k)
+          H.(get_multi (of_list l1) k @ [ v ] @ get_multi (of_list l2) k));
+    (* FS and RFC7230§3.2.2 *)
+    (* forall h, v1, v2, forall k in list values headers.
+            get_multi (add (add h k v1) k v2)) k = get_multi h k @ [v1; v2] *)
+    add_test ~name:"headers order is preserved"
+      [ headers_gen; list_value_header_gen; word_gen; word_gen ]
+      (fun h k v1 v2 ->
+        check_eq
+          H.(get_multi (add (add h k v1) k v2) k)
+          (H.(get_multi h k) @ [ v1; v2 ])))
+
+let remove_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall h, k. mem (remove h k) k = false *)
+    add_test ~name:"[remove] removes all values associated to a key"
+      [ headers_gen; header_name_gen ] (fun h k ->
+        check_eq false H.(mem (remove h k) k));
+    (* FS *)
+    (* forall h, k. remove (remove h k) k = remove h k*)
+    add_test ~name:"(fun x -> remove x k) is idempotent"
+      [ headers_gen; header_name_gen ] (fun h k ->
+        check_eq H.(remove (remove h k) k) H.(remove h k)))
+
+let replace_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall h, k, v. get_multi (replace h k v) = [ v ] *)
+    add_test ~name:"[replace] replaces the last value and remove the others"
+      [ headers_gen; header_list_gen; header_name_gen; word_gen; word_gen ]
+      (fun h l k v1 v2 ->
+        check_eq H.(get_multi (replace h k v1) k) [ v1 ];
+        (* This second check is to make sure the case where mem h k = true is tested *)
+        let h =
+          H.(add_list (add h k v1) l)
+          (* h is built such as mem h k = true *)
+        in
+        check_eq H.(get_multi (replace h k v2) k) [ v2 ]);
+    (* FS *)
+    (* forall h, k, v. if mem h k = false then replace h k v = add h k v) *)
+    add_test ~name:"replace h k v = add h k v if k does not exists in h"
+      [ headers_gen; header_name_gen; word_gen ] (fun h k v ->
+        guard H.(mem h k = false);
+        check_eq H.(replace h k v) H.(add h k v));
+    (* SI *)
+    (* forall h, l, k, v1, v2.
+           if mem (of_list l) k = false then
+              replace (add_list h ([ k, v1 ] @ l)) k v2 =
+             add_list (add (remove h k) k v2) l k) *)
+    add_test ~name:"[replace] does not change headers order"
+      [ headers_gen; header_list_gen; header_name_gen; word_gen; word_gen ]
+      (fun h l k v1 v2 ->
+        guard H.(not (mem (of_list l) k));
+        (* A random headers such as mem h k = true *)
+        let h1 = H.(add_list h ([ (k, v1) ] @ l)) in
+        let h2 = H.(add_list (remove h k) ([ (k, v2) ] @ l)) in
+        check_eq ~eq:eqssl H.(to_list (replace h1 k v2)) H.(to_list h2)))
+
+let update_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall h k, update h k id = h  *)
+    add_test ~name:"[update h k id] does nothing"
+      [ headers_gen; header_name_gen ] (fun h k ->
+        check_eq H.(update h k (fun x -> x)) h);
+    (*FS*)
+    (* forall h k f, remove (update h k f) k = remove h k *)
+    add_test ~name:"[update h k _] only changes k "
+      [ headers_gen; header_name_gen; word_gen ] (fun h k w ->
+        check_eq H.(remove (update h k (fun _ -> None)) k) H.(remove h k);
+        check_eq H.(remove (update h k (fun _ -> Some w)) k) H.(remove h k));
+    (*FS*)
+    add_test ~name:"[update h k (fun _ -> None)] removes last occurence of k."
+      [ headers_gen; header_name_gen ] (fun h k ->
+        let h1 = H.update h k (fun _ -> None) in
+        let r1 = H.get_multi h1 k in
+        let r2 =
+          match List.rev (H.get_multi h k) with
+          | [] -> []
+          | _ :: xs -> List.rev xs
+        in
+        check_eq r1 r2);
+    (*FS*)
+    add_test
+      ~name:
+        "[update h k (function Some _ -> Some w)] replaces last occurence of k."
+      [ headers_gen; header_name_gen; word_gen ] (fun h k w ->
+        let h1 = H.update h k (fun _ -> Some w) in
+        let r1 = H.get_multi h1 k in
+        let r2 =
+          match List.rev (H.get_multi h k) with
+          | [] -> [ w ]
+          | _ :: xs -> List.rev (w :: xs)
+        in
+        check_eq r1 r2))
+
+let update_all_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall h k, update_all h k id = h  *)
+    add_test ~name:"[update_all h k id] does nothing"
+      [ headers_gen; header_name_gen ] (fun h k ->
+        check_eq H.(update_all h k (fun x -> x)) h);
+    (*FS*)
+    (* forall h k f, remove (update_all h k f) k = remove h k *)
+    add_test ~name:"[update_all h k _] only changes k "
+      [ headers_gen; header_name_gen; word_gen ] (fun h k w ->
+        check_eq H.(remove (update_all h k (fun _ -> [])) k) H.(remove h k);
+        check_eq H.(remove (update_all h k (fun _ -> [ w ])) k) H.(remove h k));
+    (*FS*)
+    add_test ~name:"[update_all h k (fun _ -> [])] removes all occurences of k."
+      [ headers_gen; header_name_gen ] (fun h k ->
+        let h1 = H.update_all h k (fun _ -> []) in
+        check_eq H.(get_multi h1 k) []);
+    (*FS*)
+    add_test
+      ~name:
+        "[update_all h k (function _ -> [w])] removes all occurences of k and \
+         adds w." [ headers_gen; header_name_gen; word_gen ] (fun h k w ->
+        let h1 = H.update_all h k (fun _ -> [ w ]) in
+        let r1 = H.get_multi h1 k in
+        let r2 = [ w ] in
+        check_eq r1 r2))
+
+let get_multi_concat_test () =
+  Crowbar.(
+    (* FS *)
+    (* forall h, k. if mem h k = false then get_multi_concat h k = None *)
+    add_test
+      ~name:"[get_multi_concat h k] returns \"\" if k does not exists in h"
+      [ headers_gen; header_name_gen ] (fun h k ->
+        guard H.(not (mem h k));
+        check_eq H.(get_multi_concat h k) None);
+    (* FS *)
+    (* forall h, k. get_multi_concat ~list_value_only:true h k = get h k
+       if k is not a list value header *)
+    add_test ~name:"[get_multi_concat] optional argument works properly"
+      [ headers_gen; word_gen ] (fun h k ->
+        guard (not (Array.mem (String.lowercase_ascii k) list_value_headers));
+        check_eq H.(get_multi_concat ~list_value_only:true h k) H.(get h k));
+    (* FS - Very important for RFC 7230.3.2.2 *)
+    add_test ~name:"[get_multi_concat] returns values in transmission order"
+      [ header_list_gen; header_list_gen; header_name_gen; word_gen ]
+      (fun l1 l2 k v ->
+        let str_opt ?(bfr = false) ?(aft = false) s =
+          match s with
+          | None -> ""
+          | Some v -> if bfr then "," ^ v else if aft then v ^ "," else v
+        in
+        check_eq
+          H.(str_opt (get_multi_concat (of_list (l1 @ [ (k, v) ] @ l2)) k))
+          H.(
+            str_opt ~aft:true (get_multi_concat (of_list l1) k)
+            ^ v
+            ^ str_opt ~bfr:true (get_multi_concat (of_list l2) k))))
+
+(* Note : clean_dup does nothing to already concatenated headers. For
+   example, ["a", "v1,v2"] will be not be cleaned. *)
+let clean_dup_test () =
+  Crowbar.(
+    (* FS *)
+    (* Check that there is no more duplicates (except set-cookie). *)
+    add_test
+      ~name:
+        "All headers name in [h] appears strictly once in [clean_dup h] except \
+         for [set-cookie]" [ headers_gen ] (fun h ->
+        let h = H.remove h "set-cookie" in
+        let h = H.(to_list (clean_dup h)) in
+        let compare_key (k, _) (k', _) = compare k k' in
+        check_eq (List.sort_uniq compare_key h) (List.sort compare_key h));
+    (* FS *)
+    (* forall h, k in list_value_headers.
+       String.concat "," (get_multi_concat h k) = get (clean_dup h) k *)
+    add_test ~name:"[clean_dup] concatenates properly list-value headers"
+      [ headers_gen; list_value_header_gen ] (fun h k ->
+        check_eq H.(get_multi_concat h k) H.(get (clean_dup h) k));
+    (* FS *)
+    (* forall h. clean_dup (clean_dup h) = clean_dup h *)
+    add_test ~name:"[clean_dup] is idempotent" [ headers_gen ] (fun h ->
+        check_eq H.(clean_dup (clean_dup h)) H.(clean_dup h));
+    (* FS *)
+    (* forall h. get_multi (clean_dup h) "set-cookie" = get_multi h "set-cookie"*)
+    add_test ~name:"[clean_dup] does nothing to [set-cookie] headers"
+      [ headers_gen ] (fun h ->
+        check_eq
+          H.(get_multi h "set-cookie")
+          H.(get_multi (clean_dup h) "set-cookie"));
+    (* FS *)
+    (* As the generated header values are only composed of tchar (it
+       does not generate concatenated values like "gzip,chunked"), the
+       only cases where there are commas in a value is if [clean_dup]
+       concatenated multiple values.
+
+       This test checks that only one value is kept for non-list-value
+       headers and that this value is the last one. *)
+    add_test
+      ~name:"Only list-value headers can have multiple concatenated values "
+      [ headers_gen ] (fun h ->
+        (* As it is an exception, [set-cookie] is removed. *)
+        let h = H.remove h "set-cookie" in
+        let h' = H.(clean_dup h) in
+        let has_multiple_values v =
+          match String.split_on_char ',' v with
+          | [] | [ _ ] -> false
+          | _ -> true
+        in
+        check_eq true
+          H.(
+            fold
+              (fun k v b ->
+                if Array.mem k list_value_headers then b
+                else if has_multiple_values v then false
+                else b && get h k = Some v)
+              h' true)))
+
+let () =
+  init_with_test ();
+  is_empty_test ();
+  mem_test ();
+  add_test ();
+  to_list_of_list_test ();
+  add_opt_test ();
+  add_unless_exists_test ();
+  add_list ();
+  add_multi ();
+  get_test ();
+  get_multi_test ();
+  get_multi_concat_test ();
+  remove_test ();
+  replace_test ();
+  update_test ();
+  update_all_test ();
+  clean_dup_test ();
+  ()
diff --git a/cohttp/fuzz/inputs/input b/cohttp/fuzz/inputs/input
new file mode 100644 (file)
index 0000000..a459bc2
--- /dev/null
@@ -0,0 +1 @@
+something
\ No newline at end of file
index 26823b154dc893972b3629f56b66f409a922a83c..f4995c07ed55fe4ac03a17ee50a51cefe044c71d 100644 (file)
  *
   }}}*)
 
-module LString : sig
-  type t
-
-  val of_string : string -> t
-  val to_string : t -> string
-  val compare : t -> t -> int
-end = struct
-  type t = string
-
-  let of_string x = String.lowercase_ascii x
-  let to_string x = x
-  let compare a b = String.compare a b
-end
-
-module StringMap = Map.Make (LString)
-
-type t = string list StringMap.t
-
-let user_agent = Printf.sprintf "ocaml-cohttp/%s" Conf.version
-let compare = StringMap.compare Stdlib.compare
-
-let headers_with_list_values =
-  Array.map LString.of_string
-    [|
-      "accept";
-      "accept-charset";
-      "accept-encoding";
-      "accept-language";
-      "accept-ranges";
-      "allow";
-      "cache-control";
-      "connection";
-      "content-encoding";
-      "content-language";
-      "expect";
-      "if-match";
-      "if-none-match";
-      "link";
-      "pragma";
-      "proxy-authenticate";
-      "te";
-      "trailer";
-      "transfer-encoding";
-      "upgrade";
-      "vary";
-      "via";
-      "warning";
-      "www-authenticate";
-    |]
-
-let is_transfer_encoding =
-  let k = LString.of_string "transfer-encoding" in
-  fun k' -> LString.compare k k' = 0
-
-let is_header_with_list_value =
-  let tbl = Hashtbl.create (Array.length headers_with_list_values) in
-  headers_with_list_values |> Array.iter (fun h -> Hashtbl.add tbl h ());
-  fun h -> Hashtbl.mem tbl h
-
-let init () = StringMap.empty
-let is_empty x = StringMap.is_empty x
-let init_with k v = StringMap.singleton (LString.of_string k) [ v ]
-
-let add h k v =
-  let k = LString.of_string k in
-  try
-    if is_transfer_encoding k then
-      StringMap.add k (StringMap.find k h @ [ v ]) h
-    else StringMap.add k (v :: StringMap.find k h) h
-  with Not_found -> StringMap.add k [ v ] h
-
+let caseless_equal a b =
+  if a == b then true
+  else
+    let len = String.length a in
+    len = String.length b
+    &&
+    let stop = ref false in
+    let idx = ref 0 in
+    while (not !stop) && !idx < len do
+      let c1 = String.unsafe_get a !idx in
+      let c2 = String.unsafe_get b !idx in
+      if Char.lowercase_ascii c1 <> Char.lowercase_ascii c2 then stop := true;
+      incr idx
+    done;
+    not !stop
+
+type t = (string * string) list
+
+let compare = Stdlib.compare
+let init () = []
+let is_empty = function [] -> true | _ -> false
+let init_with k v = [ (k, v) ]
+
+let mem h k =
+  let rec loop = function
+    | [] -> false
+    | (k', _) :: h' -> if caseless_equal k k' then true else loop h'
+  in
+  loop h
+
+let add h k v : t = (k, v) :: h
 let add_list h l = List.fold_left (fun h (k, v) -> add h k v) h l
 let add_multi h k l = List.fold_left (fun h v -> add h k v) h l
-let add_opt h k v = match h with None -> init_with k v | Some h -> add h k v
 
-let remove h k =
-  let k = LString.of_string k in
-  StringMap.remove k h
+let add_opt h_opt k v =
+  match h_opt with None -> init_with k v | Some h -> add h k v
 
-let replace h k v =
-  let k = LString.of_string k in
-  StringMap.add k [ v ] h
+let add_unless_exists h k v = if mem h k then h else add h k v
+
+let add_opt_unless_exists h k v =
+  match h with None -> init_with k v | Some h -> add_unless_exists h k v
 
 let get h k =
-  let k = LString.of_string k in
-  try
-    let v = StringMap.find k h in
-    if is_header_with_list_value k then Some (String.concat "," v)
-    else Some (List.hd v)
-  with Not_found | Failure _ -> None
+  let rec loop h =
+    match h with
+    | [] -> None
+    | (k', v) :: h' -> if caseless_equal k k' then Some v else loop h'
+  in
+  loop h
+
+let get_multi (h : t) (k : string) =
+  let rec loop h acc =
+    match h with
+    | [] -> acc
+    | (k', v) :: h' ->
+        if caseless_equal k k' then loop h' (v :: acc) else loop h' acc
+  in
+  loop h []
+
+let remove h k =
+  let rec loop seen = function
+    | [] -> if seen then [] else raise Not_found
+    | (k', _) :: h when caseless_equal k k' -> loop true h
+    | x :: h -> x :: loop seen h
+  in
+  try loop false h with Not_found -> h
+
+let remove_last h k =
+  let rec loop seen = function
+    | [] -> raise Not_found
+    | (k', _) :: h when caseless_equal k k' -> h
+    | x :: h -> x :: loop seen h
+  in
+  try loop false h with Not_found -> h
+
+let replace_ last h k v =
+  let rec loop seen = function
+    | [] -> if seen then [] else raise Not_found
+    | (k'', _) :: h when caseless_equal k k'' ->
+        if last then (k'', v) :: h
+        else if not seen then (k, v) :: loop true h
+        else loop seen h
+    | x :: h -> x :: loop seen h
+  in
+  try loop false h with Not_found -> add h k v
+
+let replace = replace_ false
 
 let update h k f =
   let vorig = get h k in
-  let k = LString.of_string k in
   match (f vorig, vorig) with
-  | None, _ -> StringMap.remove k h
+  | None, None -> h
+  | None, _ -> remove_last h k
   | Some s, Some s' when s == s' -> h
-  | Some s, _ ->
-      let v' =
-        if is_header_with_list_value k then String.split_on_char ',' s
-        else [ s ]
-      in
-      StringMap.add k v' h
+  | Some s, _ -> replace_ true h k s
 
-let mem h k = StringMap.mem (LString.of_string k) h
-let add_unless_exists h k v = if mem h k then h else add h k v
+let update_all h k f =
+  let vorig = get_multi h k in
+  match (f vorig, vorig) with
+  | [], [] -> h
+  | [], _ -> remove h k
+  | xs, xs' when xs = xs' -> h
+  | xs, _ ->
+      let h = remove h k in
+      add_multi h k xs
 
-let add_opt_unless_exists h k v =
-  match h with None -> init_with k v | Some h -> add_unless_exists h k v
+let map (f : string -> string -> string) (h : t) : t =
+  List.map
+    (fun (k, v) ->
+      let vs' = f k v in
+      (k, vs'))
+    h
 
-let get_multi h k =
-  let k = LString.of_string k in
-  try StringMap.find k h with Not_found -> []
+let iter (f : string -> string -> unit) (h : t) : unit =
+  List.iter (fun (k, v) -> f k v) h
 
-let map fn h = StringMap.mapi (fun k v -> fn (LString.to_string k) v) h
-let iter fn h = ignore (map fn h)
+let fold (f : string -> string -> 'a -> 'a) (h : t) (init : 'a) : 'a =
+  List.fold_left (fun acc (k, v) -> f k v acc) init h
 
-let fold fn h acc =
-  StringMap.fold
-    (fun k v acc ->
-      List.fold_left (fun acc v -> fn (LString.to_string k) v acc) acc v)
-    h acc
+let of_list h = List.rev h
+let to_list h = List.rev h
 
-let of_list l = List.fold_left (fun h (k, v) -> add h k v) (init ()) l
-let to_list h = List.rev (fold (fun k v acc -> (k, v) :: acc) h [])
-let header_line k v = Printf.sprintf "%s: %s\r\n" k v
-let to_lines h = List.rev (fold (fun k v acc -> header_line k v :: acc) h [])
+let to_lines (h : t) =
+  let header_line k v = Printf.sprintf "%s: %s\r\n" k v in
+  List.fold_left (fun acc (k, v) -> header_line k v :: acc) [] h
 
-let to_frames =
-  let to_frame k v acc = Printf.sprintf "%s: %s" k v :: acc in
-  fun h -> List.rev (fold to_frame h [])
+let to_frames =
+  let to_frame k v = Printf.sprintf "%s: %s" k v in
+  List.fold_left (fun acc (k, v) -> to_frame k v :: acc) [] h
 
 let to_string h =
   let b = Buffer.create 128 in
-  h
-  |> iter (fun k v ->
-         v
-         |> List.iter (fun v ->
-                Buffer.add_string b k;
-                Buffer.add_string b ": ";
-                Buffer.add_string b v;
-                Buffer.add_string b "\r\n"));
+  to_list h
+  |> List.iter (fun (k, v) ->
+         Buffer.add_string b k;
+         Buffer.add_string b ": ";
+         Buffer.add_string b v;
+         Buffer.add_string b "\r\n");
   Buffer.add_string b "\r\n";
   Buffer.contents b
 
+let headers_with_list_values =
+  [|
+    "accept";
+    "accept-charset";
+    "accept-encoding";
+    "accept-language";
+    "accept-ranges";
+    "allow";
+    "cache-control";
+    "connection";
+    "content-encoding";
+    "content-language";
+    "expect";
+    "if-match";
+    "if-none-match";
+    "link";
+    "pragma";
+    "proxy-authenticate";
+    "te";
+    "trailer";
+    "transfer-encoding";
+    "upgrade";
+    "vary";
+    "via";
+    "warning";
+    "www-authenticate";
+  |]
+
+let is_header_with_list_value =
+  let tbl = Hashtbl.create (Array.length headers_with_list_values) in
+  headers_with_list_values |> Array.iter (fun h -> Hashtbl.add tbl h ());
+  fun h -> Hashtbl.mem tbl h
+
+let is_set_cookie k = caseless_equal k "set-cookie"
+
+(* set-cookie is an exception according to
+   {{:https://tools.ietf.org/html/rfc7230#section-3.2.2}
+    RFC7230§3.2.2} and can appear multiple times in a response message.
+*)
+let clean_dup (h : t) : t =
+  let add h k v =
+    if is_set_cookie k then (k, v) :: h
+    else
+      let to_add = ref false in
+      let rec loop = function
+        | [] ->
+            to_add := true;
+            []
+        | (k', v') :: hs ->
+            if caseless_equal k k' then
+              if is_header_with_list_value k then (k, v' ^ "," ^ v) :: hs
+              else (
+                to_add := true;
+                hs)
+            else (k', v') :: loop hs
+      in
+      let h = loop h in
+      if !to_add then (k, v) :: h else h
+  in
+  List.rev h |> List.fold_left (fun acc (k, v) -> add acc k v) []
+
+let get_multi_concat ?(list_value_only = false) h k : string option =
+  if (not list_value_only) || is_header_with_list_value k then
+    let vs = get_multi h k in
+    match vs with [] -> None | _ -> Some (String.concat "," vs)
+  else get h k
+
 let parse_content_range s =
   try
     let start, fini, total =
@@ -210,21 +273,25 @@ let get_media_type headers =
   | None -> None
 
 let get_acceptable_media_ranges headers =
-  Accept.media_ranges (get headers "accept")
+  Accept.media_ranges (get_multi_concat ~list_value_only:true headers "accept")
 
 let get_acceptable_charsets headers =
-  Accept.charsets (get headers "accept-charset")
+  Accept.charsets
+    (get_multi_concat ~list_value_only:true headers "accept-charset")
 
 let get_acceptable_encodings headers =
-  Accept.encodings (get headers "accept-encoding")
+  Accept.encodings
+    (get_multi_concat ~list_value_only:true headers "accept-encoding")
 
 let get_acceptable_languages headers =
-  Accept.languages (get headers "accept-language")
+  Accept.languages
+    (get_multi_concat ~list_value_only:true headers "accept-language")
 
 (* Parse the transfer-encoding and content-length headers to
  * determine how to decode a body *)
 let get_transfer_encoding headers =
-  match get headers "transfer-encoding" with
+  (* It should actually be [get] as the interresting value is actually the last.*)
+  match get_multi_concat ~list_value_only:true headers "transfer-encoding" with
   | Some "chunked" -> Transfer.Chunked
   | Some _ | None -> (
       match get_content_range headers with
@@ -270,6 +337,8 @@ let get_links headers =
 let add_links headers links =
   add_multi headers "link" (List.map Link.to_string links)
 
+let user_agent = Printf.sprintf "ocaml-cohttp/%s" Conf.version
+
 let prepend_user_agent headers user_agent =
   let k = "user-agent" in
   match get headers k with
index 6914b6964e2a26ce3ef0b9b15b951c83c3bd3eaa..38d6823c991faa9fd7ba58e5a64ab5ddb91154c4 100644 (file)
  *
   }}}*)
 
-(** Map of HTTP header key and value(s) associated with them. Since HTTP headers
-    can contain duplicate keys, this structure can return a list of values
-    associated with a single key. *)
-
+(** Associative list representing HTTP headers. Order of transmission is
+    preserved, which implies that headers with same name are neither removed or
+    concataned by default (see [clean_dup] to do that). *)
 type t [@@deriving sexp]
 (** The type for HTTP headers. *)
 
 val init : unit -> t
-(** Construct a fresh, empty map of HTTP headers. *)
+(** [init ()] constructs a fresh, empty list of HTTP headers. *)
 
 val is_empty : t -> bool
-(** Test whether HTTP headers are empty or not. *)
+(** [is_empty h] tests whether HTTP headers [h] are empty or not. *)
+
+val of_list : (string * string) list -> t
+(** [of_list l] construct a fresh headers from the content of [l] and in same
+    order. [to_list] and [of_list] are defined such as [to_list (of_list l) = l]
+    is true with case insensitive comparison. *)
+
+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] *)
 
 val init_with : string -> string -> t
-(** Construct a fresh map of HTTP headers with a single key and value entry. *)
+(** [init_with k v] construct a fresh HTTP headers with a single header with
+    name [k] and value [v]. *)
 
 val add : t -> string -> string -> t
-(** Add a key and value to an existing header map. *)
+(** [add h k v] adds the header name [k] and it associated value [v] at the end
+    of header list [h]. *)
 
 val add_list : t -> (string * string) list -> t
-(** Add multiple key and value pairs to an existing header map. *)
+(** [add_list h l] adds in order all header pairs contained in [l] to the header
+    list [h].
+
+    {e Invariant (with case insensitive comparison):}
+    [to_list (add_list h l) = to_list h @ l] *)
 
 val add_multi : t -> string -> string list -> t
-(** Add multiple values to a key in an existing header map. *)
+(** [add_multi h k vs] add multiple header pairs with same name [h] and values
+    contained in [vs] in [h]. The new headers are in the same order that in
+    [vs].
+
+    {e Invariant:} [get_multi (add_multi h k vs) k = (get_multi h k) @ vs] *)
 
 val add_opt : t option -> string -> string -> t
-(** Given an optional header, either update the existing one with a key and
-    value, or construct a fresh header with those values if the header is
+(** [add_opt hopt k v] adds the header [(k, v)] to [h] if [hopt] is [Some h], or
+    constructs a fresh header list containing this single header if [hopt] is
     [None]. *)
 
 val add_unless_exists : t -> string -> string -> t
-(** Given a header, update it with the key and value unless the key is already
-    present in the header. *)
+(** [add_unless_exists h k v] adds [(k, v)] to [h] unless the header name [k] is
+    already present in the header. *)
 
 val add_opt_unless_exists : t option -> string -> string -> t
-(** [add_opt_unless_exists h k v] updates [h] with the key [k] and value [v]
-    unless the key is already present in the header. If [h] is [None] then a
-    fresh header is allocated containing the key [k] and the value [v]. *)
+(** [add_opt_unless_exists h k v] adds [(k, v)] to [h] if [hopt] is [Some h]
+    unless the header name [k] is already present in the headers. If [h] is
+    [None] then a fresh header list is constructed containing the header
+    [(k, v)]. *)
 
 val remove : t -> string -> t
-(** Remove a key from the header map and return a fresh header set. The original
-    header parameter is not modified. *)
+(** [remove h k] removes every values associated to the header name [k] from
+    [h]. *)
 
 val replace : t -> string -> string -> t
-(** Replace the value of a key from the header map if it exists, otherwise it
-    adds it to the header map. The original header parameter is not modified. *)
+(** [replace h k v] replaces the last added value of [k] from [h] and removed
+    all other occurences of [k] if it exists. Otherwise it adds [(k, v)] to [h].
 
-val update : t -> string -> (string option -> string option) -> t
-(** [update h k f] returns a map containing the same headers as [h], except for
-    the header [k]. Depending on the value of [v] where [v] is [f (get h k)],
-    the header [k] is added, removed or updated. If [v] is [None], the header is
-    removed if it exists; otherwise, if [v] is [Some z] then [k] is associated
-    to [z] in the resulting headers. If [k] was already associated in [h] to a
-    value that is physically equal to [z], [h] is returned unchanged. Similarly
-    as for [get], if the header is one of the set of headers defined to have
-    list values, then all of the values are concatenated into a single string
-    separated by commas and passed to [f], while the return value of [f] is
-    split on commas and associated to [k]. If it is a singleton header, then the
-    first value is passed to [f] and no concatenation is performed, similarly
-    for the return value. The original header parameters are not modified. *)
+    {e Invariant:} [forall h, k, v. get_multi (replace h k v) = \[ v \]] *)
 
 val mem : t -> string -> bool
-(** Check if a key exists in the header. *)
+(** [mem h k] returns [true] if the header name [k] appears in [h] and [false]
+    otherwise. *)
 
 val compare : t -> t -> int
-(** Structural comparison of two [Header] values. *)
+(** [compare h h'] is the structural comparison of two [Header] values. *)
 
 val get : t -> string -> string option
-(** Retrieve a key from a header. If the header is one of the set of headers
-    defined to have list values, then all of the values are concatenated into a
-    single string separated by commas and returned. If it is a singleton header,
-    then the first value is selected and no concatenation is performed. *)
+(** [get h k] returns [Some v] where [v] is the last added value associated with
+    [k] in [h] if it exists and [None] otherwise *)
 
 val get_multi : t -> string -> string list
-(** Retrieve all of the values associated with a key *)
+(** [get_multi h k] returns a list of all values associated with [k] in [h] in
+    order they appear in it. *)
+
+val get_multi_concat : ?list_value_only:bool -> t -> string -> string option
+(** [get_multi_concat h k] returns [Some v] if there is at least one value
+    associated with [k] in [h] and [None] otherwise. [v] is the concatenation of
+    all values paired with [k] in [h], separated by a comma and in order they
+    appear in [h].
+
+    The optional argument [?list_value_only] is [false] by default. If it is
+    [true] and there is at least one value associated to [k], the returned value
+    is the concatenated values only if [k] is a header that can have multiple
+    values (like transfer-encoding or accept). Otherwise, the 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] *)
+
+val update : t -> string -> (string option -> string option) -> t
+(** [update h k f] returns an header list containing the same headers as [h],
+    except for the header name [k]. Depending on the value of [v] where [v] is
+    [f (get h k)], the header pair [(k, v)] is added, removed or updated.
+
+    - If [v] is [None], the last occurence of [k] in [h] is removed;
+
+    - If [v] is [Some w] then the last value paired with [k] in [h] is replaced
+      by [w] if it exists. Otherwise, the pair [(k, w)] is added;
 
-val iter : (string -> string list -> unit) -> t -> unit
-val map : (string -> string list -> string list) -> t -> t
+    - If [k] was already associated last in [h] to a value that is physically
+      equal to [w], [h] is returned unchanged. *)
+
+val update_all : t -> string -> (string list -> string list) -> t
+(** [update_all h k f] returns an header list containing the same headers as
+    [h], except for the header [k]. Depending on the list of values [vs] where
+    [vs] is [f (get_multi h k)], the values associated to the header [k] are
+    added, removed or updated.
+
+    - If [vs] is an empty list, every occurences of the header [k] in [h] are
+      removed;
+
+    - If [vs] is a non-empty list, all values previously associated to [k] are
+      removed and all values in [vs] are added with [add_multi];
+
+    - If [k] was already associated in [h] to a list that is equal to [vs], [h]
+      is returned unchanged. *)
+
+val iter : (string -> string -> unit) -> t -> unit
+val map : (string -> string -> string) -> t -> t
 val fold : (string -> string -> 'a -> 'a) -> t -> 'a -> 'a
-val of_list : (string * string) list -> t
-val to_list : t -> (string * string) list
 
 val to_lines : t -> string list
-(** Return header fieds as a list of lines. Beware that each line ends with
-    "\r\n" characters. *)
+(** [to_lines h] returns header fieds as a list of lines. Beware that each line
+    ends with "\r\n" characters. *)
 
 val to_frames : t -> string list
-(** Same as {!to_lines} but lines do not end with "\r\n" characters. *)
+(** [to_frames h] returns the same as {!to_lines} but lines do not end with
+    "\r\n" characters. *)
 
 val to_string : t -> string
+
+val clean_dup : t -> t
+(** [clean_dup h] cleans duplicates in [h] following
+    {{:https://tools.ietf.org/html/rfc7230#section-3.2.2} RFC7230§3.2.2}; if a
+    duplicated header can not have multiple values, only the last value is kept
+    in place. Otherwise, the values are concatenated and place at the first
+    position the header is encountered in [h].
+
+    Already concatenated values (like [anhost.com, anotherhost.com] in the
+    example below) are not affected by [clean_dup]. For example,
+
+    {v
+    transfer-encoding: gzip
+    host: afirsthost.com
+    connection: keep-alive
+    host: anhost.com, anotherhost.com
+    transfer-encoding: chunked
+    v}
+
+    becomes
+
+    {v
+    transfer-encoding: gzip, chunked
+    connection: keep-alive
+    host: anhost.com, anotherhost.com
+    v}
+
+    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]. *)
+
 val get_content_range : t -> Int64.t option
 val get_media_type : t -> string option
 val get_connection_close : t -> bool
index a4fc17619cf43cda828615fbd6a3238153080fb5..eb12ae962c0122827252ef618aceb649a372aa35 100644 (file)
@@ -25,18 +25,14 @@ module Make (IO : S.IO) = struct
   open IO
   module Transfer_IO = Transfer_io.Make (IO)
 
-  let rev _k v = List.rev v
-
   let parse ic =
     (* consume also trailing "^\r\n$" line *)
     let rec parse_headers' headers =
       read_line ic >>= function
-      | Some "" | None -> return (Header.map rev headers)
+      | Some "" | None -> return headers
       | Some line -> (
           match split_header line with
-          | [ hd; tl ] ->
-              let header = String.lowercase_ascii hd in
-              parse_headers' (Header.add headers header tl)
+          | [ hd; tl ] -> parse_headers' (Header.add headers hd tl)
           | _ -> return headers)
     in
     parse_headers' (Header.init ())
index b1e3309b8e11b171621c07cbdc9966875750094c..dde8072f7b9b4562141080b7e4f096cad6ce6732 100644 (file)
@@ -12,7 +12,7 @@
 
 (executable
  (name test_header)
- (modules test_header)
+ (modules unitary_test_header test_header)
  (forbidden_libraries base)
  (libraries cohttp alcotest fmt))
 
index 7c83edc776ec368b007191ce70eeef042676022a..3ffee01589e417f33f1a6946eb93fcc1a15bede1 100644 (file)
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *}}}*)
 
-open Printf
 module String_io = Cohttp__String_io
 module StringResponse = Cohttp.Response.Make (String_io.M)
-module HIO = Cohttp__Header_io.Make (String_io.M)
 module H = Cohttp.Header
 
 let aes = Alcotest.check Alcotest.string
@@ -102,106 +100,6 @@ let get_media_type () =
     "media type" (Some "foo/bar")
     (Cohttp.Header.get_media_type header)
 
-let list_valued_header () =
-  let h = H.init () in
-  let h = H.add h "accept" "foo" in
-  let h = H.add h "accept" "bar" in
-  aeso "list valued header" (H.get h "accept") (Some "bar,foo")
-
-let t_header =
-  Alcotest.testable
-    (fun fmt h ->
-      let sexp = Cohttp.Header.sexp_of_t h in
-      Sexplib0.Sexp.pp_hum fmt sexp)
-    (fun x y -> Cohttp.Header.compare x y = 0)
-
-let large_header () =
-  let sz = 1024 * 1024 * 100 in
-  let h = H.init () in
-  let v1 = String.make sz 'a' in
-  let h = H.add h "x-large" v1 in
-  let h = H.add h v1 "foo" in
-  aeso "x-large" (H.get h "x-large") (Some v1);
-  let obuf = Buffer.create (sz + 1024) in
-  HIO.write h obuf;
-  let ibuf = Buffer.contents obuf in
-  let sbuf = String_io.open_in ibuf in
-  Alcotest.check t_header "large_header" (HIO.parse sbuf) h
-
-let many_headers () =
-  let size = 1000000 in
-  let rec add_header num h =
-    match num with
-    | 0 -> h
-    | n ->
-        let k = sprintf "h%d" n in
-        let v = sprintf "v%d" n in
-        let h = H.add h k v in
-        add_header (num - 1) h
-  in
-  let h = add_header size (H.init ()) in
-  Alcotest.(check int) "many_headers" (List.length (H.to_list h)) size
-
-module Updates = struct
-  let h =
-    H.init () |> fun h ->
-    H.add h "first" "1" |> fun h ->
-    H.add h "second" "2" |> fun h ->
-    H.add h "accept" "foo" |> fun h -> H.add h "accept" "bar"
-
-  let replace_headers_if_exists () =
-    let h = H.replace h "second" "2a" in
-    Alcotest.(check (option string))
-      "replace_existing_header" (Some "2a") (H.get h "second")
-
-  let replace_headers_if_absent () =
-    let h = H.replace h "third" "3" in
-    Alcotest.(check (option string))
-      "replace_new_header" (Some "3") (H.get h "third")
-
-  let update_headers_if_exists () =
-    let h1 =
-      H.update h "second" (function Some _ -> Some "2a" | None -> None)
-    in
-    let h2 = H.replace h "second" "2a" in
-    Alcotest.(check t_header) "update_existing_header" h1 h2
-
-  let update_headers_if_exists_rm () =
-    let h1 =
-      H.update h "second" (function Some _ -> None | None -> Some "3")
-    in
-    let h2 = H.remove h "second" in
-    Alcotest.(check t_header) "update_remove_header" h1 h2
-
-  let update_headers_if_absent_add () =
-    let h = H.update h "third" (function Some _ -> None | None -> Some "3") in
-    Alcotest.(check (option string))
-      "update_add_new_header" (Some "3") (H.get h "third")
-
-  let update_headers_if_absent_rm () =
-    let h1 = H.update h "third" (function _ -> None) in
-    Alcotest.(check t_header) "update_remove_absent_header" h h1
-
-  let update_headers_if_exists_multi () =
-    let h1 =
-      H.update h "accept" (function
-        | Some v -> Some ("baz," ^ v)
-        | None -> None)
-    in
-    let h2 = H.add h "accept" "baz" in
-    Alcotest.(check (option string))
-      "update_existing_header_multivalued" (H.get h1 "accept")
-      (H.get h2 "accept")
-
-  let update_headers_if_absent () =
-    let h1 =
-      H.update h "third" (function Some _ -> Some "3" | None -> None)
-    in
-    Alcotest.(check t_header) "update_new_header: unchanged" h h1;
-    Alcotest.(check (option string))
-      "update_new_header: map unchanged" None (H.get h "third")
-end
-
 module Content_range = struct
   let h1 = H.of_list [ ("Content-Length", "123") ]
   let h2 = H.of_list [ ("Content-Range", "bytes 200-300/1000") ]
@@ -551,18 +449,7 @@ let test_cachecontrol_concat () =
   in
   let h = headers_of_response "concat Cache-Control" resp in
   aeso "test_cachecontrol_concat" (Some "public,max-age:86400")
-    (H.get h "Cache-Control")
-
-let transfer_encoding () =
-  let h =
-    H.of_list
-      [ ("transfer-encoding", "gzip"); ("transfer-encoding", "chunked") ]
-  in
-  let sh = H.to_string h in
-  aes "transfer_encoding_string_is_ordered" sh
-    "transfer-encoding: gzip\r\ntransfer-encoding: chunked\r\n\r\n";
-  let sh = H.get h "transfer-encoding" in
-  aeso "transfer_encoding_get_is_ordered" (Some "gzip,chunked") sh
+    (H.get_multi_concat h "Cache-Control")
 
 let () = Printexc.record_backtrace true
 
@@ -603,24 +490,5 @@ let () =
           ("content-range", `Quick, Content_range.content_range);
         ] );
       ("Cache Control", [ ("concat", `Quick, test_cachecontrol_concat) ]);
-      ( "Header",
-        [
-          ("get list valued", `Quick, list_valued_header);
-          ("trim whitespace", `Quick, trim_ws);
-          ("replace existing", `Quick, Updates.replace_headers_if_exists);
-          ("replace absent", `Quick, Updates.replace_headers_if_absent);
-          ("update existing", `Quick, Updates.update_headers_if_exists);
-          ( "update existing list",
-            `Quick,
-            Updates.update_headers_if_exists_multi );
-          ("update add absent", `Quick, Updates.update_headers_if_absent_add);
-          ("update rm existing", `Quick, Updates.update_headers_if_exists_rm);
-          ("update rm absent", `Quick, Updates.update_headers_if_absent_rm);
-          ("update absent", `Quick, Updates.update_headers_if_absent);
-          ("many headers", `Slow, many_headers);
-          ("transfer encoding is in correct order", `Quick, transfer_encoding);
-        ]
-        @
-        if Sys.word_size = 64 then [ ("large header", `Slow, large_header) ]
-        else [] );
+      Unitary_test_header.tests;
     ]
diff --git a/cohttp/test/unitary_test_header.ml b/cohttp/test/unitary_test_header.ml
new file mode 100644 (file)
index 0000000..4eca861
--- /dev/null
@@ -0,0 +1,401 @@
+(*{{{ Copyright (c) 2021 Carine Morel <carine@tarides.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *}}}*)
+
+module H = Cohttp.Header
+(** These tests try as much as possible to tests each header functions
+    independently. *)
+
+let aei = Alcotest.check Alcotest.int
+let aes = Alcotest.check Alcotest.string
+let aeso = Alcotest.check Alcotest.(option string)
+let aesl = Alcotest.check Alcotest.(list string)
+let aessl = Alcotest.check Alcotest.(list (pair string string))
+let aeb = Alcotest.check Alcotest.bool
+
+let t_header =
+  Alcotest.testable
+    (fun fmt h ->
+      let sexp = Cohttp.Header.sexp_of_t h in
+      Sexplib0.Sexp.pp_hum fmt sexp)
+    (fun x y -> Cohttp.Header.compare x y = 0)
+
+let aeh = Alcotest.check t_header
+
+let hstr =
+  [
+    ("accept", "application/xml");
+    ("transfer-encoding", "chunked");
+    ("accept", "text/html");
+    ("content-length", "100");
+  ]
+
+let prebuilt = H.of_list hstr
+let to_list_rev h = List.rev (H.to_list h)
+
+let to_list_tests () =
+  aessl "to_list (init ())" [] H.(to_list (init ()));
+  aessl "to_list (add (init ()) k v"
+    [ ("a", "a1") ]
+    H.(to_list (add (init ()) "a" "a1"));
+  aessl "to_list (of_list h) = h" hstr H.(to_list prebuilt)
+
+let is_empty_tests () =
+  aeb "is_empty (init ())" true H.(is_empty (init ()));
+  aeb "is_empty (add (init ()) k v" false H.(is_empty (add (init ()) "a" "a1"));
+  aeb "is_empty (remove (add (init ()) k v) k)" true
+    H.(is_empty (remove (add (init ()) "a" "a1") "a"))
+
+let init_with_tests () =
+  aessl "init_with k v"
+    [ ("traNsfer-eNcoding", "chunked") ]
+    H.(to_list (init_with "traNsfer-eNcoding" "chunked"))
+
+let mem_tests () =
+  aeb "mem (init ()) k = false" false H.(mem (init ()) "a");
+  aeb "mem h k" true H.(mem prebuilt "accept");
+  aeb "mem h k" true H.(mem prebuilt "content-length");
+  aeb "mem h k" false H.(mem prebuilt "a")
+
+let add_tests () =
+  aessl "add h k v" (hstr @ [ ("a", "a1") ]) H.(to_list (add prebuilt "a" "a1"));
+  aessl "add (add h k v) k v"
+    (hstr @ [ ("a", "a1"); ("a", "a1") ])
+    H.(to_list (add (add prebuilt "a" "a1") "a" "a1"));
+  aessl "add (add h k' v') k v"
+    (hstr @ [ ("a", "a1"); ("b", "b1") ])
+    H.(to_list (add (add prebuilt "a" "a1") "b" "b1"))
+
+let get_tests () =
+  aeso "get (add (init () k v) k" (Some "a1")
+    H.(get (add (init ()) "a" "a1") "a");
+  aeso "get (add h k v) k when mem h k = false" (Some "a1")
+    H.(get (add prebuilt "a" "a1") "a");
+  aeso "get (add h k v) k when mem h k = true" (Some "text/html")
+    H.(get (add prebuilt "a" "a1") "accept");
+  aeso "get (add (add h k v') k v) k = v" (Some "a2")
+    H.(get (add (add prebuilt "a" "a1") "a" "a2") "a")
+
+let add_list_tests () =
+  let l = [ ("a", "a1"); ("b", "b1") ] in
+  aessl "add_list (init ()) []" [] H.(to_list (add_list (init ()) []));
+  aessl "add_list (init ()) l" l H.(to_list (add_list (init ()) l));
+  aessl "add_list h []" hstr H.(to_list (add_list prebuilt []));
+  aessl "add_list h [k, v]"
+    (hstr @ [ ("a", "a1") ])
+    H.(to_list (add_list prebuilt [ ("a", "a1") ]));
+  aessl "add_list h l" (hstr @ l) H.(to_list (add_list prebuilt l))
+
+let add_multi_tests () =
+  let k, vals = ("a", [ "a1"; "a2"; "a3" ]) in
+  let l = List.map (fun v -> ("a", v)) vals in
+  aessl "add_multi (init ()) k []" [] H.(to_list (add_multi (init ()) k []));
+  aessl "add_multi (init ()) k vals" l H.(to_list (add_multi (init ()) k vals));
+  aessl "add_multi h k []" hstr H.(to_list (add_multi prebuilt k []));
+  aessl "add_multi h k vals" (hstr @ l) H.(to_list (add_multi prebuilt k vals))
+
+let add_unless_exists_tests () =
+  let k, v = ("a", "a1") in
+  let k', v' = ("transfer-encoding", "chunked") in
+  let k'', v'' = ("accept", "text/*") in
+  aessl "add_unless_exists (init ()) k v"
+    [ (k, v) ]
+    H.(to_list (add_unless_exists (init ()) k v));
+  aessl "add_unless_exists h k v when mem h k = false"
+    (hstr @ [ (k, v) ])
+    H.(to_list (add_unless_exists prebuilt k v));
+  aessl "add_unless_exists h k v when mem h k = true)" hstr
+    H.(to_list (add_unless_exists prebuilt k' v'));
+  aessl "add_unless_exists h k v when mem h k = true)" hstr
+    H.(to_list (add_unless_exists prebuilt k'' v''))
+
+let remove_tests () =
+  aessl "remove (init ()) k" [] H.(to_list (remove (init ()) "accept"));
+  aessl "remove (add (add (init ()) k v) k v) k" []
+    H.(to_list (remove (add (add (init ()) "k" "v") "k" "v") "k"));
+  aessl "remove h k when mem h k = false" hstr H.(to_list (remove prebuilt "a"));
+  aessl "remove h k when mem h k = true"
+    [
+      ("accept", "application/xml");
+      ("accept", "text/html");
+      ("content-length", "100");
+    ]
+    H.(to_list (remove prebuilt "transfer-encoding"));
+  aessl "remove h k when mem h k = true"
+    [ ("transfer-encoding", "chunked"); ("content-length", "100") ]
+    H.(to_list (remove prebuilt "accept"))
+
+let replace_tests () =
+  let k, v, v' = ("a", "a1", "a2") in
+  aessl "replace (init ()) k v" [ (k, v) ] H.(to_list (replace (init ()) k v));
+  aessl "replace (add (init ()) k v) k v"
+    [ (k, v) ]
+    H.(to_list (replace (add (init ()) k v) k v));
+  aessl "replace (add (init ()) k v) k v'"
+    [ (k, v') ]
+    H.(to_list (replace (add (init ()) k v) k v'));
+  aessl "replace h k v when mem h k = false"
+    (hstr @ [ (k, v) ])
+    H.(to_list (replace prebuilt k v));
+  aessl "replace h k v when mem h k = true"
+    [
+      ("accept", "application/xml");
+      ("transfer-encoding", "gzip");
+      ("accept", "text/html");
+      ("content-length", "100");
+    ]
+    H.(to_list (replace prebuilt "transfer-encoding" "gzip"));
+  aessl "replace h k v when mem h = true"
+    [
+      ("transfer-encoding", "chunked");
+      ("accept", "text/*");
+      ("content-length", "100");
+    ]
+    H.(to_list (replace prebuilt "accept" "text/*"))
+
+let h =
+  H.init () |> fun h ->
+  H.add h "first" "1" |> fun h ->
+  H.add h "second" "2" |> fun h ->
+  H.add h "accept" "foo" |> fun h -> H.add h "accept" "bar"
+
+let update_tests () =
+  let h1 =
+    H.update h "second" (function Some _ -> Some "2a" | None -> None)
+  in
+  let h2 = H.replace h "second" "2a" in
+  aeh "update existing header" h1 h2;
+  let h1 = H.update h "second" (function Some _ -> None | None -> Some "3") in
+  let h2 = H.remove h "second" in
+  aeh "update remove header" h1 h2;
+  let h1 =
+    H.update h "accept" (function Some _ -> Some "baz" | None -> None)
+  in
+  aesl "update existing header with multiple values"
+    H.(get_multi h1 "accept")
+    [ "foo"; "baz" ];
+  let h' = H.update h "third" (function Some _ -> None | None -> Some "3") in
+  aesl "update add new header" (H.get_multi h' "third") [ "3" ];
+  let h1 = H.update h "third" (function _ -> None) in
+  aeh "update_remove_absent_header" h h1;
+  let h1 = H.update h "third" (function Some _ -> Some "3" | None -> None) in
+  aeh "update_new_header: unchanged" h h1;
+  let h1 = H.update h "accept" (function Some _ -> None | None -> None) in
+  aeso "update_existing_header_remove_multivalue: remove last" (Some "foo")
+    (H.get h1 "accept")
+
+let update_all_tests () =
+  let h1 = H.update_all h "second" (function [] -> [] | _ -> [ "2a" ]) in
+  let h2 = H.(add (remove h "second") "second" "2a") in
+  aeh "update_all existing header" h1 h2;
+  let h1 = H.update_all h "second" (function [] -> [ "3" ] | _ -> []) in
+  let h2 = H.remove h "second" in
+  aeh "update_all remove header" h1 h2;
+  let h1 = H.update_all h "accept" (function [] -> [] | _ -> [ "baz" ]) in
+  aesl "update_all existing header with multiple values"
+    H.(get_multi h1 "accept")
+    [ "baz" ];
+  let h1 =
+    H.update_all h "accept" (function [] -> [] | xs -> xs @ [ "baz" ])
+  in
+  let h2 = H.add h "accept" "baz" in
+  aeso "update_all_existing_header_multivalued"
+    (H.get_multi_concat h1 "accept")
+    (H.get_multi_concat h2 "accept");
+  let h1 = H.update_all h "accept" (function _ -> []) in
+  aeh "update_all_existing_header_multivalue : remove all" (H.remove h "accept")
+    h1;
+  let h1 = H.update_all h "third" (function [] -> [ "3"; "33" ] | _ -> []) in
+  let h2 = H.add_multi h "third" [ "3"; "33" ] in
+  aeh "update add new header" h1 h2;
+  let h1 = H.update_all h "third" (function _ -> []) in
+  aeh "update_remove_absent_header" h h1;
+  let h1 = H.update_all h "third" (function [] -> [] | _ -> [ "3" ]) in
+  aeh "update_new_header: unchanged" h h1
+
+let get_multi_tests () =
+  aesl "get_multi (init ()) k" [] H.(get_multi (init ()) "a");
+  aesl "get_multi h k when mem h k = false" [] H.(get_multi prebuilt "a");
+  aesl "get_multi h k when mem h k = true" [ "chunked" ]
+    H.(get_multi prebuilt "transfer-encoding");
+  aesl "get_multi h k when mem h k = true"
+    [ "application/xml"; "text/html" ]
+    H.(get_multi prebuilt "accept")
+
+let hstr =
+  [
+    ("accept", "application/xml");
+    ("transfer-encoding", "chunked");
+    ("accept", "text/html");
+    ("content-length", "100");
+  ]
+
+let get_multi_concat_tests () =
+  let h1 = H.(add (add prebuilt "a" "a1") "a" "a2") in
+  aeso "get_multi_concat (init ()) k" None H.(get_multi_concat (init ()) "a");
+  aeso "get_multi_concat h k when mem h k = false" None
+    H.(get_multi_concat prebuilt "a");
+  aeso "get_multi_concat h k when mem h k = true"
+    (Some "application/xml,text/html")
+    H.(get_multi_concat prebuilt "accept");
+  aeso "get_multi_concat ~list_value_only:false h k when mem h k = true"
+    (Some "a1,a2")
+    H.(get_multi_concat h1 "a");
+  aeso "get_multi_concat ~list_value_only:true h k when mem h k = true"
+    (Some "a2")
+    H.(get_multi_concat ~list_value_only:true h1 "a")
+
+let map_tests () =
+  let a = ", a" in
+  aessl "map (fun _ v -> v) (init ())" []
+    H.(to_list (map (fun _k v -> v) (init ())));
+  aessl "map (fun _ v -> v) (init ())" (H.to_list prebuilt)
+    H.(to_list (map (fun _k v -> v) prebuilt));
+  aessl "map (fun _ v -> v ^ a ) (init ())"
+    [
+      ("accept", "application/xml, a");
+      ("transfer-encoding", "chunked, a");
+      ("accept", "text/html, a");
+      ("content-length", "100, a");
+    ]
+    H.(to_list (map (fun _k v -> v ^ a) prebuilt))
+
+let fold_tests () =
+  let rev k v acc = H.(add acc k v) in
+  let h1 = H.(fold rev prebuilt (init ())) in
+  aessl
+    "[fold (fun k v acc -> H.(add acc k v)) h (init ())] reverses the header"
+    (List.rev H.(to_list h1))
+    H.(to_list prebuilt);
+  let h1 = H.(fold rev (fold rev prebuilt (init ())) (init ())) in
+  aeh "[fold rev (fold rev h (init ())) (init ()) = h] " h1 prebuilt;
+  let count _ _ acc = acc + 1 in
+  aei "[fold (fun _ _ acc -> acc+1) h 0] returns the length of h"
+    (List.length H.(to_list prebuilt))
+    H.(fold count prebuilt 0)
+
+let iter_tests () =
+  let h = ref H.(init ()) in
+  let rev k v = h := H.(add !h k v) in
+  H.(iter rev prebuilt);
+  aessl "[iter (fun k v -> href := H.(add !href k v)) h] reverses the header"
+    (List.rev H.(to_list !h))
+    H.(to_list prebuilt);
+  let c = ref 0 in
+  let count _ _ = c := !c + 1 in
+  aei "[iter (fun _ _ -> count := !count+1) h] works fine"
+    (List.length H.(to_list prebuilt))
+    (H.(iter count prebuilt);
+     !c)
+
+let to_lines_tests () =
+  aesl "to_lines h"
+    [
+      "accept: application/xml\r\n";
+      "transfer-encoding: chunked\r\n";
+      "accept: text/html\r\n";
+      "content-length: 100\r\n";
+    ]
+    H.(to_lines prebuilt)
+
+let to_frames_tests () =
+  aesl "to_frames h"
+    [
+      "accept: application/xml";
+      "transfer-encoding: chunked";
+      "accept: text/html";
+      "content-length: 100";
+    ]
+    H.(to_frames prebuilt)
+
+let to_string_tests () =
+  aes "to_string h"
+    "accept: application/xml\r\n\
+     transfer-encoding: chunked\r\n\
+     accept: text/html\r\n\
+     content-length: 100\r\n\
+     \r\n"
+    H.(to_string prebuilt)
+
+let many_headers () =
+  let size = 1000000 in
+  let rec add_header num h =
+    match num with
+    | 0 -> h
+    | n ->
+        let k = Printf.sprintf "h%d" n in
+        let v = Printf.sprintf "v%d" n in
+        let h = H.add h k v in
+        add_header (num - 1) h
+  in
+  let h = add_header size (H.init ()) in
+  Alcotest.(check int) "many_headers" (List.length (H.to_list h)) size
+
+let transfer_encoding_tests () =
+  let h =
+    H.of_list
+      [ ("transfer-encoding", "gzip"); ("transfer-encoding", "chunked") ]
+  in
+  let sh = H.to_string h in
+  aes "transfer_encoding_string_is_ordered" sh
+    "transfer-encoding: gzip\r\ntransfer-encoding: chunked\r\n\r\n";
+  let sh = H.get_multi_concat h "transfer-encoding" in
+  aeso "transfer_encoding_get_is_ordered" (Some "gzip,chunked") sh
+
+module String_io = Cohttp__String_io
+module HIO = Cohttp__Header_io.Make (String_io.M)
+
+let large_header () =
+  let sz = 1024 * 1024 * 100 in
+  let h = H.init () in
+  let v1 = String.make sz 'a' in
+  let h = H.add h "x-large" v1 in
+  let h = H.add h v1 "foo" in
+  aeso "x-large" (H.get h "x-large") (Some v1);
+  let obuf = Buffer.create (sz + 1024) in
+  HIO.write h obuf;
+  let ibuf = Buffer.contents obuf in
+  let sbuf = String_io.open_in ibuf in
+  Alcotest.check t_header "large_header" (HIO.parse sbuf) h
+
+let tests =
+  ( "Unitary Header tests",
+    [
+      ("Header.to_list", `Quick, to_list_tests);
+      ("Header.is_empty", `Quick, is_empty_tests);
+      ("Header.init_with", `Quick, init_with_tests);
+      ("Header.mem", `Quick, mem_tests);
+      ("Header.add", `Quick, add_tests);
+      ("Header.get", `Quick, get_tests);
+      ("Header.add_list", `Quick, add_list_tests);
+      ("Header.add_multi", `Quick, add_multi_tests);
+      ("Header.add_unless_exists", `Quick, add_unless_exists_tests);
+      ("Header.remove", `Quick, remove_tests);
+      ("Header.replace", `Quick, replace_tests);
+      ("Header.get_multi", `Quick, get_multi_tests);
+      ("Header.get_multi_concat", `Quick, get_multi_concat_tests);
+      ("Header.to_lines", `Quick, to_lines_tests);
+      ("Header.to_frames", `Quick, to_frames_tests);
+      ("Header.to_string", `Quick, to_string_tests);
+      ("Header.map", `Quick, map_tests);
+      ("Header.fold", `Quick, fold_tests);
+      ("Header.iter", `Quick, iter_tests);
+      ("Header.update", `Quick, update_tests);
+      ("Header.update_all", `Quick, update_all_tests);
+      ("many headers", `Slow, many_headers);
+      ("transfer encoding is in correct order", `Quick, transfer_encoding_tests);
+    ]
+    @
+    if Sys.word_size = 64 then [ ("large header", `Slow, large_header) ] else []
+  )
diff --git a/cohttp_lwt_jsoo_test/package.json b/cohttp_lwt_jsoo_test/package.json
new file mode 100644 (file)
index 0000000..9f034ad
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "ocaml-cohttp",
+  "version": "4.0.0",
+  "description": "An OCaml library for HTTP clients and servers",
+  "repository": "https://github.com/mirage/ocaml-cohttp",
+  "license": "ISC",
+  "dependencies": {
+    "xmlhttprequest": "^1.8.0"
+  }
+}
diff --git a/cohttp_lwt_jsoo_test/src/cohttp_lwt_jsoo_test.ml b/cohttp_lwt_jsoo_test/src/cohttp_lwt_jsoo_test.ml
new file mode 100644 (file)
index 0000000..ed57301
--- /dev/null
@@ -0,0 +1,34 @@
+module Client = Cohttp_lwt_jsoo.Client
+module Js = Js_of_ocaml.Js
+
+let _Promise = Js.Unsafe.global##._Promise
+let ( let* ) = Lwt.( >>= )
+let ( let+ ) = Lwt.( >|= )
+
+let promise_of_lwt lwt =
+  new%js _Promise
+    (Js.wrap_callback (fun resolve reject ->
+         try%lwt
+           let+ res = lwt () in
+           Js.Unsafe.fun_call resolve [| Js.Unsafe.inject res |]
+         with e ->
+           let msg = Printexc.to_string e in
+           Js.Unsafe.fun_call reject
+             [| Js.Unsafe.inject (new%js Js.error_constr (Js.string msg)) |]))
+
+let () =
+  Js.export_all
+    (object%js
+       method request uri =
+         let f () =
+           let uri = Uri.of_string (Js.to_string uri) in
+           let* response, body = Client.get uri in
+           let+ body = Cohttp_lwt.Body.to_string body in
+           let status =
+             Cohttp.Response.status response |> Cohttp.Code.code_of_status
+           in
+           Js.array
+             [| Js.Unsafe.inject status; Js.Unsafe.inject @@ Js.string body |]
+         in
+         promise_of_lwt f
+    end)
diff --git a/cohttp_lwt_jsoo_test/src/dune b/cohttp_lwt_jsoo_test/src/dune
new file mode 100644 (file)
index 0000000..d627919
--- /dev/null
@@ -0,0 +1,15 @@
+(executable
+ (name cohttp_lwt_jsoo_test)
+ (modes js)
+ (libraries cohttp cohttp-lwt cohttp-lwt-jsoo lwt js_of_ocaml-lwt)
+ (preprocess
+  (pps lwt_ppx js_of_ocaml-ppx)))
+
+(rule
+ (alias runjstest)
+ (deps test.js cohttp_lwt_jsoo_test.bc.js)
+ (action
+  (setenv
+   NODE_PATH
+   "%{project_root}/cohttp_lwt_jsoo_test/node_modules"
+   (run ./test.js))))
diff --git a/cohttp_lwt_jsoo_test/src/test.js b/cohttp_lwt_jsoo_test/src/test.js
new file mode 100755 (executable)
index 0000000..49e744c
--- /dev/null
@@ -0,0 +1,19 @@
+#!/usr/bin/env node
+
+const assert = require('assert');
+
+global.XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
+const tests = require('./cohttp_lwt_jsoo_test.bc.js')
+
+async function main() {
+  {
+    const [status, body] = await tests.request("https://mirage.io");
+    assert(status == 200);
+  }
+  {
+    const [status, body] = await tests.request("https://this.domain.does.not.exist");
+    assert(status == 0);
+  }
+}
+
+main()
diff --git a/cohttp_lwt_jsoo_test/yarn.lock b/cohttp_lwt_jsoo_test/yarn.lock
new file mode 100644 (file)
index 0000000..e80db41
--- /dev/null
@@ -0,0 +1,8 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+xmlhttprequest@^1.8.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
+  integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=
index 6a3dcc77e57d60454a1368eb0ed9a3834e1ee084..ad62607fd3a32b5183920b06713657a13799b12e 100644 (file)
@@ -1,3 +1,2 @@
 (lang dune 2.0)
 (name cohttp)
-(version v4.0.0)
index 22bc87fc271663c5f2cfa1ad0ac5227bb85319da..65022d323630d454b46d757b5d7bce5279e5b11a 100644 (file)
@@ -236,7 +236,7 @@ module Auth = struct
     let hashed_req = digest canonical_request in
     Printf.sprintf "AWS4-HMAC-SHA256\n%s\n%s\n%s" time_str scope_str hashed_req
 
-  let make_signing_key ?date ~region ~service ~secret_access_key =
+  let make_signing_key ?date ~region ~service ~secret_access_key () =
     let mac k v =
       Mirage_crypto.Hash.(mac `SHA256 ~key:k (Cstruct.of_string v))
     in
@@ -267,6 +267,7 @@ module Auth = struct
     in
     let signing_key =
       make_signing_key ~date ~region ~service ~secret_access_key:aws_secret_key
+        ()
     in
     let creds =
       Printf.sprintf "%s/%s/%s/%s/aws4_request" aws_access_key
diff --git a/jbuild-workspace.dev b/jbuild-workspace.dev
deleted file mode 100644 (file)
index 67a92d3..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-;; This file is used by `make all-supported-ocaml-versions`
-(context ((switch 4.03.0)))
-(context ((switch 4.04.2)))
-(context ((switch 4.05.0)))
-(context ((switch 4.06.0)))