mirror of
https://github.com/c-cube/ezcurl.git
synced 2025-12-16 23:56:54 -05:00
Compare commits
43 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0a5b989b7 | ||
|
|
27b3ca76c8 | ||
|
|
4b7d9ec769 | ||
|
|
ebf1e1fc12 | ||
|
|
742e51df97 | ||
|
|
0faaf35969 | ||
|
|
012016f9b4 | ||
|
|
49b265ce56 | ||
|
|
fbd71baa19 | ||
|
|
d50a53ebda | ||
|
|
e97003644a | ||
|
|
48d18bd912 | ||
|
|
71a37788ac | ||
|
|
a8ad44d39e | ||
|
|
5b4b71ee15 | ||
|
|
7c684f1869 | ||
|
|
ddf181fe83 | ||
|
|
d259d85eb7 | ||
|
|
d41eb2ceb4 | ||
|
|
46dc012356 | ||
|
|
648ca8f77f | ||
|
|
7939bfa9e3 | ||
|
|
9e0b84ecfd | ||
|
|
be03f25cc0 | ||
|
|
73899acdf2 | ||
|
|
c6141c8b3d | ||
|
|
90ad38de20 | ||
|
|
52c35dc2df | ||
|
|
5bb480235a | ||
|
|
849194728a | ||
|
|
9e7d9be5c3 | ||
|
|
91cfbc94ad | ||
|
|
63392ea36b | ||
|
|
ac24129ed6 | ||
|
|
bb69dade38 | ||
|
|
2665eda083 | ||
|
|
f70f25d951 | ||
|
|
5ba64ee30a | ||
|
|
916f72fb39 | ||
|
|
953db1f130 | ||
|
|
cfcd23d16a | ||
|
|
db9b18fb44 | ||
|
|
c568f9e4e3 |
22 changed files with 1025 additions and 616 deletions
4
.github/workflows/gh-pages.yml
vendored
4
.github/workflows/gh-pages.yml
vendored
|
|
@ -18,9 +18,9 @@ jobs:
|
||||||
path: ~/.opam
|
path: ~/.opam
|
||||||
key: opam-ubuntu-latest-4.12.0
|
key: opam-ubuntu-latest-4.12.0
|
||||||
|
|
||||||
- uses: avsm/setup-ocaml@v1
|
- uses: ocaml/setup-ocaml@v3
|
||||||
with:
|
with:
|
||||||
ocaml-version: '4.12.0'
|
ocaml-compiler: '5.03'
|
||||||
|
|
||||||
- name: Pin
|
- name: Pin
|
||||||
run: opam pin -n .
|
run: opam pin -n .
|
||||||
|
|
|
||||||
18
.github/workflows/main.yml
vendored
18
.github/workflows/main.yml
vendored
|
|
@ -2,10 +2,10 @@ name: build
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
jobs:
|
jobs:
|
||||||
run:
|
run:
|
||||||
name: Build
|
name: Build
|
||||||
|
|
@ -13,20 +13,20 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
#- macos-latest
|
# - macos-latest # build issues with `ar` (!!!)
|
||||||
#- windows-latest
|
#- windows-latest # certificate problem
|
||||||
ocaml-compiler:
|
ocaml-compiler:
|
||||||
- 4.03.x
|
- '4.08'
|
||||||
- 4.12.x
|
- '4.14'
|
||||||
|
- '5.3'
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: ocaml/setup-ocaml@v2
|
- uses: ocaml/setup-ocaml@v3
|
||||||
with:
|
with:
|
||||||
ocaml-compiler: ${{ matrix.ocaml-compiler }}
|
ocaml-compiler: ${{ matrix.ocaml-compiler }}
|
||||||
- run: opam pin -n .
|
- run: opam pin -n .
|
||||||
- run: opam depext -yt ezcurl ezcurl-lwt
|
- run: opam depext -yt ezcurl ezcurl-lwt
|
||||||
- run: opam install -t . --deps-only
|
- run: opam install -t . --deps-only
|
||||||
- run: opam exec -- dune build
|
- run: opam exec -- dune build @install
|
||||||
- run: opam exec -- dune runtest
|
- run: opam exec -- dune runtest
|
||||||
if: ${{ matrix.os == 'ubuntu-latest'}}
|
|
||||||
|
|
|
||||||
15
.ocamlformat
Normal file
15
.ocamlformat
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
version = 0.26.2
|
||||||
|
profile=conventional
|
||||||
|
margin=80
|
||||||
|
if-then-else=k-r
|
||||||
|
parens-ite=true
|
||||||
|
parens-tuple=multi-line-only
|
||||||
|
sequence-style=terminator
|
||||||
|
type-decl=sparse
|
||||||
|
break-cases=toplevel
|
||||||
|
cases-exp-indent=2
|
||||||
|
field-space=tight-decl
|
||||||
|
leading-nested-match-parens=true
|
||||||
|
module-item-spacing=compact
|
||||||
|
quiet=true
|
||||||
|
ocaml-version=4.08.0
|
||||||
27
CHANGELOG.md
Normal file
27
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
## 0.2.4
|
||||||
|
|
||||||
|
- fix: global initialization logic is now hidden behind a mutex
|
||||||
|
* depend on `thread`
|
||||||
|
|
||||||
|
## 0.2.3
|
||||||
|
|
||||||
|
- fix: workaround servers which do not understand "Expect" header
|
||||||
|
- fix: correctly set size of payload for POST
|
||||||
|
- make sure to setup 'PUT" correctly
|
||||||
|
- allow POST with non-form data
|
||||||
|
|
||||||
|
## 0.2.2
|
||||||
|
|
||||||
|
- fix: do not reset client if passed as argument
|
||||||
|
|
||||||
|
## 0.2.1
|
||||||
|
|
||||||
|
- fix setting of headers
|
||||||
|
|
||||||
|
## 0.2
|
||||||
|
|
||||||
|
- add default user agent
|
||||||
|
|
||||||
|
## 0.1
|
||||||
|
|
||||||
|
- initial release
|
||||||
17
Makefile
17
Makefile
|
|
@ -2,18 +2,29 @@
|
||||||
all: build test
|
all: build test
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@dune build @all
|
@dune build @install
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@dune runtest --no-buffer --force
|
@dune runtest --no-buffer --force
|
||||||
|
|
||||||
|
test-autopromote:
|
||||||
|
@dune runtest --no-buffer --force --auto-promote
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@dune clean
|
@dune clean
|
||||||
|
|
||||||
doc:
|
doc:
|
||||||
@dune build @doc
|
@dune build @doc
|
||||||
|
|
||||||
|
WATCH?= @install @runtest
|
||||||
watch:
|
watch:
|
||||||
@dune build @all -w
|
@dune build $(WATCH )-w
|
||||||
|
|
||||||
.PHONY: all build test watch
|
VERSION=$(shell awk '/^version:/ {print $$2}' ezcurl.opam)
|
||||||
|
|
||||||
|
update_next_tag:
|
||||||
|
@echo "update version to $(VERSION)..."
|
||||||
|
sed -i "s/NEXT_VERSION/$(VERSION)/g" $(wildcard src/**/*.ml) $(wildcard src/**/*.mli)
|
||||||
|
sed -i "s/NEXT_RELEASE/$(VERSION)/g" $(wildcard src/**/*.ml) $(wildcard src/**/*.mli)
|
||||||
|
|
||||||
|
.PHONY: all build test watch update_next_tag
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# EZCurl [](https://github.com/c-cube/ezcurl/actions/workflows/main.yml)
|
# EZCurl [](https://github.com/c-cube/ezcurl/actions/workflows/main.yml)
|
||||||
|
|
||||||
A simple wrapper around OCurl, for easy tasks around http.
|
A simple wrapper around [OCurl](https://github.com/ygrek/ocurl/), for easy tasks around http.
|
||||||
|
|
||||||
**project goals**
|
**project goals**
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ A simple wrapper around OCurl, for easy tasks around http.
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
- for the synchronous library: `opam install ezcurl`
|
- for the synchronous library: `opam install ezcurl`
|
||||||
- for the lwt-baed library: `opam install ezcurl-lwt` (depends on `ezcurl`)
|
- for the lwt-based library: `opam install ezcurl-lwt` (depends on `ezcurl`)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ val url : string = "https://curl.haxx.se/"
|
||||||
# let res = Ezcurl.get ~url ();;
|
# let res = Ezcurl.get ~url ();;
|
||||||
...
|
...
|
||||||
# let content = match res with Ok c -> c | Error (_,s) -> failwith s;;
|
# let content = match res with Ok c -> c | Error (_,s) -> failwith s;;
|
||||||
val content : Ezcurl_core.response =
|
val content : string Ezcurl_core.response =
|
||||||
...
|
...
|
||||||
|
|
||||||
# content.Ezcurl.code;;
|
# content.Ezcurl.code;;
|
||||||
|
|
|
||||||
8
dune
8
dune
|
|
@ -1,10 +1,10 @@
|
||||||
|
|
||||||
(alias
|
(rule
|
||||||
(name runtest)
|
(alias runtest)
|
||||||
(package ezcurl-lwt)
|
(package ezcurl-lwt)
|
||||||
(deps README.md)
|
(deps (:file README.md))
|
||||||
(action
|
(action
|
||||||
(progn
|
(progn
|
||||||
(run ocaml-mdx test %{deps})
|
(run ocaml-mdx test %{deps})
|
||||||
(diff? %{deps} %{deps}.corrected))))
|
(diff? %{file} %{file}.corrected))))
|
||||||
|
|
||||||
|
|
|
||||||
42
dune-project
42
dune-project
|
|
@ -1 +1,41 @@
|
||||||
(lang dune 1.0)
|
(lang dune 3.0)
|
||||||
|
|
||||||
|
(name ezcurl)
|
||||||
|
|
||||||
|
(version 0.2.4)
|
||||||
|
|
||||||
|
(generate_opam_files true)
|
||||||
|
|
||||||
|
(license MIT)
|
||||||
|
|
||||||
|
(maintainers "simon.cruanes.2007@m4x.org")
|
||||||
|
|
||||||
|
(authors "Simon Cruanes")
|
||||||
|
(source (github c-cube/ezcurl))
|
||||||
|
(documentation https://c-cube.github.io/ezcurl/)
|
||||||
|
|
||||||
|
(package
|
||||||
|
(name ezcurl)
|
||||||
|
(synopsis "Friendly wrapper around OCurl")
|
||||||
|
(tags
|
||||||
|
("curl" "web" "http" "client"))
|
||||||
|
(depends
|
||||||
|
(ocurl
|
||||||
|
(>= 0.8))
|
||||||
|
(odoc :with-doc)
|
||||||
|
(ocaml
|
||||||
|
(>= 4.03))))
|
||||||
|
|
||||||
|
(package
|
||||||
|
(name ezcurl-lwt)
|
||||||
|
(synopsis "Friendly wrapper around OCurl, Lwt version")
|
||||||
|
(tags
|
||||||
|
("curl" "web" "http" "client" "lwt"))
|
||||||
|
(depends
|
||||||
|
(ezcurl
|
||||||
|
(= :version))
|
||||||
|
lwt
|
||||||
|
(mdx :with-test)
|
||||||
|
(odoc :with-doc)
|
||||||
|
(ocaml
|
||||||
|
(>= 4.03))))
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,34 @@
|
||||||
|
# This file is generated by dune, edit dune-project instead
|
||||||
opam-version: "2.0"
|
opam-version: "2.0"
|
||||||
name: "ezcurl-lwt"
|
version: "0.2.4"
|
||||||
version: "0.2"
|
|
||||||
authors: ["Simon Cruanes"]
|
|
||||||
maintainer: "simon.cruanes.2007@m4x.org"
|
|
||||||
license: "MIT"
|
|
||||||
synopsis: "Friendly wrapper around OCurl, Lwt version"
|
synopsis: "Friendly wrapper around OCurl, Lwt version"
|
||||||
build: [
|
maintainer: ["simon.cruanes.2007@m4x.org"]
|
||||||
["dune" "build" "@install" "-p" name "-j" jobs]
|
authors: ["Simon Cruanes"]
|
||||||
["dune" "build" "@doc" "-p" name] {with-doc}
|
license: "MIT"
|
||||||
#["dune" "runtest" "-p" name] {with-test}
|
tags: ["curl" "web" "http" "client" "lwt"]
|
||||||
]
|
homepage: "https://github.com/c-cube/ezcurl"
|
||||||
depends: [
|
doc: "https://c-cube.github.io/ezcurl/"
|
||||||
"ocurl" {>= "0.8.0"}
|
|
||||||
"ezcurl" { = version }
|
|
||||||
"lwt"
|
|
||||||
"dune" { >= "1.0" }
|
|
||||||
"odoc" {with-doc}
|
|
||||||
"mdx" {with-test}
|
|
||||||
"ocaml" { >= "4.03.0" }
|
|
||||||
]
|
|
||||||
tags: [ "curl" "web" "http" "client" "lwt" ]
|
|
||||||
homepage: "https://github.com/c-cube/ezcurl/"
|
|
||||||
doc: "https://c-cube.github.io/ezcurl/doc/1.2"
|
|
||||||
bug-reports: "https://github.com/c-cube/ezcurl/issues"
|
bug-reports: "https://github.com/c-cube/ezcurl/issues"
|
||||||
|
depends: [
|
||||||
|
"dune" {>= "3.0"}
|
||||||
|
"ezcurl" {= version}
|
||||||
|
"lwt"
|
||||||
|
"mdx" {with-test}
|
||||||
|
"odoc" {with-doc}
|
||||||
|
"ocaml" {>= "4.03"}
|
||||||
|
]
|
||||||
|
build: [
|
||||||
|
["dune" "subst"] {dev}
|
||||||
|
[
|
||||||
|
"dune"
|
||||||
|
"build"
|
||||||
|
"-p"
|
||||||
|
name
|
||||||
|
"-j"
|
||||||
|
jobs
|
||||||
|
"@install"
|
||||||
|
"@runtest" {with-test}
|
||||||
|
"@doc" {with-doc}
|
||||||
|
]
|
||||||
|
]
|
||||||
dev-repo: "git+https://github.com/c-cube/ezcurl.git"
|
dev-repo: "git+https://github.com/c-cube/ezcurl.git"
|
||||||
|
|
|
||||||
47
ezcurl.opam
47
ezcurl.opam
|
|
@ -1,23 +1,32 @@
|
||||||
|
# This file is generated by dune, edit dune-project instead
|
||||||
opam-version: "2.0"
|
opam-version: "2.0"
|
||||||
name: "ezcurl"
|
version: "0.2.4"
|
||||||
version: "0.2"
|
|
||||||
authors: ["Simon Cruanes"]
|
|
||||||
maintainer: "simon.cruanes.2007@m4x.org"
|
|
||||||
license: "MIT"
|
|
||||||
synopsis: "Friendly wrapper around OCurl"
|
synopsis: "Friendly wrapper around OCurl"
|
||||||
build: [
|
maintainer: ["simon.cruanes.2007@m4x.org"]
|
||||||
["dune" "build" "@install" "-p" name "-j" jobs]
|
authors: ["Simon Cruanes"]
|
||||||
["dune" "build" "@doc" "-p" name] {with-doc}
|
license: "MIT"
|
||||||
#["dune" "runtest" "-p" name] {with-test}
|
tags: ["curl" "web" "http" "client"]
|
||||||
]
|
homepage: "https://github.com/c-cube/ezcurl"
|
||||||
depends: [
|
doc: "https://c-cube.github.io/ezcurl/"
|
||||||
"ocurl" {>= "0.8.0"}
|
|
||||||
"dune" { >= "1.0" }
|
|
||||||
"odoc" {with-doc}
|
|
||||||
"ocaml" { >= "4.03.0" }
|
|
||||||
]
|
|
||||||
tags: [ "curl" "web" "http" "client" ]
|
|
||||||
homepage: "https://github.com/c-cube/ezcurl/"
|
|
||||||
doc: "https://c-cube.github.io/ezcurl/doc/1.2"
|
|
||||||
bug-reports: "https://github.com/c-cube/ezcurl/issues"
|
bug-reports: "https://github.com/c-cube/ezcurl/issues"
|
||||||
|
depends: [
|
||||||
|
"dune" {>= "3.0"}
|
||||||
|
"ocurl" {>= "0.8"}
|
||||||
|
"odoc" {with-doc}
|
||||||
|
"ocaml" {>= "4.03"}
|
||||||
|
]
|
||||||
|
build: [
|
||||||
|
["dune" "subst"] {dev}
|
||||||
|
[
|
||||||
|
"dune"
|
||||||
|
"build"
|
||||||
|
"-p"
|
||||||
|
name
|
||||||
|
"-j"
|
||||||
|
jobs
|
||||||
|
"@install"
|
||||||
|
"@runtest" {with-test}
|
||||||
|
"@doc" {with-doc}
|
||||||
|
]
|
||||||
|
]
|
||||||
dev-repo: "git+https://github.com/c-cube/ezcurl.git"
|
dev-repo: "git+https://github.com/c-cube/ezcurl.git"
|
||||||
|
|
|
||||||
|
|
@ -1,374 +0,0 @@
|
||||||
let opt_iter ~f = function None -> () | Some x -> f x
|
|
||||||
|
|
||||||
module Config = struct
|
|
||||||
type t = {
|
|
||||||
verbose: bool;
|
|
||||||
authmethod: Curl.curlAuth list option;
|
|
||||||
max_redirects: int;
|
|
||||||
follow_location: bool;
|
|
||||||
username: string option;
|
|
||||||
password: string option;
|
|
||||||
user_agent: string option;
|
|
||||||
}
|
|
||||||
|
|
||||||
let default : t = {
|
|
||||||
verbose=false;
|
|
||||||
max_redirects = 50;
|
|
||||||
follow_location=true;
|
|
||||||
authmethod=None;
|
|
||||||
username=None;
|
|
||||||
password=None;
|
|
||||||
user_agent=Some "curl";
|
|
||||||
}
|
|
||||||
|
|
||||||
let password x self = {self with password=Some x}
|
|
||||||
let username x self = {self with username=Some x}
|
|
||||||
let verbose x self = { self with verbose=x}
|
|
||||||
let follow_location x self = {self with follow_location=x}
|
|
||||||
let max_redirects x self = {self with max_redirects=max 1 x}
|
|
||||||
let authmethod x self = {self with authmethod=Some x}
|
|
||||||
|
|
||||||
let string_of_authmethod = function
|
|
||||||
| Curl.CURLAUTH_ANY -> "any"
|
|
||||||
| Curl.CURLAUTH_BASIC -> "basic"
|
|
||||||
| Curl.CURLAUTH_DIGEST -> "digest"
|
|
||||||
| Curl.CURLAUTH_GSSNEGOTIATE -> "gss_negotiate"
|
|
||||||
| Curl.CURLAUTH_NTLM -> "ntlm"
|
|
||||||
| Curl.CURLAUTH_ANYSAFE -> "any_safe"
|
|
||||||
|
|
||||||
let str_of_str_opt = function
|
|
||||||
| None -> "<none>"
|
|
||||||
| Some s -> s
|
|
||||||
|
|
||||||
let pp out (self:t) =
|
|
||||||
let {
|
|
||||||
verbose; authmethod; max_redirects; follow_location;
|
|
||||||
username; password; user_agent
|
|
||||||
} = self in
|
|
||||||
Format.fprintf out
|
|
||||||
"{@[verbose=%B;@ max_redirects=%d;@ follow_location=%B;@ \
|
|
||||||
username=%s;@ password=%s;@ authmethod=%s;@ user_agent=%s@]}"
|
|
||||||
verbose max_redirects follow_location
|
|
||||||
(str_of_str_opt username) (str_of_str_opt password)
|
|
||||||
(match authmethod with
|
|
||||||
| None -> "none"
|
|
||||||
| Some l -> List.map string_of_authmethod l |> String.concat ",") (str_of_str_opt user_agent)
|
|
||||||
|
|
||||||
let to_string s = Format.asprintf "%a" pp s
|
|
||||||
end
|
|
||||||
|
|
||||||
type t = Curl.t
|
|
||||||
|
|
||||||
let _init = lazy (
|
|
||||||
Curl.global_init Curl.CURLINIT_GLOBALALL;
|
|
||||||
at_exit Curl.global_cleanup;
|
|
||||||
)
|
|
||||||
|
|
||||||
let make ?(set_opts=fun _ -> ()) () : t =
|
|
||||||
Lazy.force _init;
|
|
||||||
let c = Curl.init () in
|
|
||||||
Gc.finalise Curl.cleanup c;
|
|
||||||
set_opts c;
|
|
||||||
c
|
|
||||||
|
|
||||||
let delete = Curl.cleanup
|
|
||||||
|
|
||||||
(* set options *)
|
|
||||||
let _apply_config (self:t) (config:Config.t) : unit =
|
|
||||||
let {
|
|
||||||
Config.verbose; max_redirects; follow_location; authmethod;
|
|
||||||
username; password; user_agent
|
|
||||||
} = config in
|
|
||||||
Curl.set_verbose self verbose;
|
|
||||||
Curl.set_maxredirs self max_redirects;
|
|
||||||
Curl.set_followlocation self follow_location;
|
|
||||||
opt_iter user_agent ~f:(fun user_agent -> Curl.set_useragent self user_agent);
|
|
||||||
opt_iter authmethod ~f:(Curl.set_httpauth self);
|
|
||||||
opt_iter username ~f:(Curl.set_username self);
|
|
||||||
opt_iter password ~f:(Curl.set_password self);
|
|
||||||
()
|
|
||||||
|
|
||||||
let _set_headers (self:t) (headers: _ list) : unit =
|
|
||||||
let headers =
|
|
||||||
List.map (fun (k,v) -> k ^ ": " ^ v ^ "\r\n") headers
|
|
||||||
in
|
|
||||||
Curl.set_httpheader self headers;
|
|
||||||
()
|
|
||||||
|
|
||||||
let with_client ?set_opts f =
|
|
||||||
let c = make ?set_opts () in
|
|
||||||
try
|
|
||||||
let x = f c in
|
|
||||||
delete c;
|
|
||||||
x
|
|
||||||
with e ->
|
|
||||||
delete c;
|
|
||||||
raise e
|
|
||||||
|
|
||||||
type response_info = {
|
|
||||||
ri_response_time: float;
|
|
||||||
ri_redirect_count: int;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pp_response_info out r =
|
|
||||||
let {ri_response_time; ri_redirect_count} = r in
|
|
||||||
Format.fprintf out "{@[response_time=%.3fs;@ redirect_count=%d@]}"
|
|
||||||
ri_response_time ri_redirect_count
|
|
||||||
|
|
||||||
let string_of_response_info s = Format.asprintf "%a" pp_response_info s
|
|
||||||
|
|
||||||
type response = {
|
|
||||||
code: int;
|
|
||||||
headers: (string * string) list;
|
|
||||||
body: string;
|
|
||||||
info: response_info;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pp_response out r =
|
|
||||||
let pp_header out (s1,s2) =
|
|
||||||
Format.fprintf out "@[<2>%s:@ %s@]" s1 s2
|
|
||||||
in
|
|
||||||
let pp_headers out l =
|
|
||||||
Format.fprintf out "@[<v>%a@]" (Format.pp_print_list pp_header) l
|
|
||||||
in
|
|
||||||
let {code; body; headers; info; } = r in
|
|
||||||
Format.fprintf out "{@[code=%d;@ headers=@[%a@];@ info=%a;@ body=@[%a@]@]}"
|
|
||||||
code pp_headers headers pp_response_info info
|
|
||||||
Format.pp_print_text body
|
|
||||||
|
|
||||||
|
|
||||||
let string_of_response s = Format.asprintf "%a" pp_response s
|
|
||||||
|
|
||||||
type meth =
|
|
||||||
| GET
|
|
||||||
| POST of Curl.curlHTTPPost list
|
|
||||||
| PUT
|
|
||||||
| DELETE
|
|
||||||
| HEAD
|
|
||||||
| CONNECT
|
|
||||||
| OPTIONS
|
|
||||||
| TRACE
|
|
||||||
| PATCH
|
|
||||||
|
|
||||||
let string_of_meth = function
|
|
||||||
| GET -> "GET"
|
|
||||||
| POST _ -> "POST"
|
|
||||||
| PUT -> "PUT"
|
|
||||||
| DELETE -> "DELETE"
|
|
||||||
| HEAD -> "HEAD"
|
|
||||||
| CONNECT -> "CONNECT"
|
|
||||||
| OPTIONS -> "OPTIONS"
|
|
||||||
| TRACE -> "TRACE"
|
|
||||||
| PATCH -> "PATCH"
|
|
||||||
|
|
||||||
let pp_meth out m = Format.pp_print_string out (string_of_meth m)
|
|
||||||
|
|
||||||
module type IO = sig
|
|
||||||
type 'a t
|
|
||||||
val return : 'a -> 'a t
|
|
||||||
val (>>=) : 'a t -> ('a -> 'b t) -> 'b t
|
|
||||||
val (>|=) : 'a t -> ('a -> 'b) -> 'b t
|
|
||||||
val fail : exn -> 'a t
|
|
||||||
val perform : Curl.t -> Curl.curlCode t
|
|
||||||
end
|
|
||||||
|
|
||||||
module type S = sig
|
|
||||||
type 'a io
|
|
||||||
|
|
||||||
val http :
|
|
||||||
?tries:int ->
|
|
||||||
?client:t ->
|
|
||||||
?config:Config.t ->
|
|
||||||
?range:string ->
|
|
||||||
?content:[`String of string | `Write of (bytes -> int -> int)] ->
|
|
||||||
?headers:(string*string) list ->
|
|
||||||
url:string ->
|
|
||||||
meth:meth ->
|
|
||||||
unit ->
|
|
||||||
(response, Curl.curlCode * string) result io
|
|
||||||
(** General purpose HTTP call via cURL.
|
|
||||||
@param url the URL to query
|
|
||||||
@param meth which method to use (see {!meth})
|
|
||||||
@param tries how many times to retry in case of [CURLE_AGAIN] code
|
|
||||||
@param client a client to reuse (instead of allocating a new one)
|
|
||||||
@param range an optional
|
|
||||||
{{: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests} byte range}
|
|
||||||
to fetch (either to get large pages
|
|
||||||
by chunks, or to resume an interrupted download).
|
|
||||||
@param config configuration to set
|
|
||||||
@param content the content to send as the query's body, either
|
|
||||||
a [`String s] to write a single string, or [`Write f]
|
|
||||||
where [f] is a callback that is called on a buffer [b] with len [n]
|
|
||||||
(as in [f b n]) and returns how many bytes it wrote in the buffer
|
|
||||||
[b] starting at index [0] (at most [n] bytes).
|
|
||||||
It must return [0] when the content is entirely written, and not
|
|
||||||
before.
|
|
||||||
@param headers headers of the query
|
|
||||||
*)
|
|
||||||
|
|
||||||
val get :
|
|
||||||
?tries:int ->
|
|
||||||
?client:t ->
|
|
||||||
?config:Config.t ->
|
|
||||||
?range:string ->
|
|
||||||
?headers:(string*string) list ->
|
|
||||||
url:string ->
|
|
||||||
unit ->
|
|
||||||
(response, Curl.curlCode * string) result io
|
|
||||||
(** Shortcut for [http ~meth:GET]
|
|
||||||
See {!http} for more info.
|
|
||||||
*)
|
|
||||||
|
|
||||||
val put :
|
|
||||||
?tries:int ->
|
|
||||||
?client:t ->
|
|
||||||
?config:Config.t ->
|
|
||||||
?headers:(string*string) list ->
|
|
||||||
url:string ->
|
|
||||||
content:[`String of string | `Write of (bytes -> int -> int)] ->
|
|
||||||
unit ->
|
|
||||||
(response, Curl.curlCode * string) result io
|
|
||||||
(** Shortcut for [http ~meth:PUT]
|
|
||||||
See {!http} for more info.
|
|
||||||
*)
|
|
||||||
|
|
||||||
val post :
|
|
||||||
?tries:int ->
|
|
||||||
?client:t ->
|
|
||||||
?config:Config.t ->
|
|
||||||
?headers:(string*string) list ->
|
|
||||||
?content:[`String of string | `Write of (bytes -> int -> int)] ->
|
|
||||||
params:Curl.curlHTTPPost list ->
|
|
||||||
url:string ->
|
|
||||||
unit ->
|
|
||||||
(response, Curl.curlCode * string) result io
|
|
||||||
(** Shortcut for [http ~meth:(POST params)]
|
|
||||||
See {!http} for more info.
|
|
||||||
*)
|
|
||||||
end
|
|
||||||
|
|
||||||
exception Parse_error of Curl.curlCode * string
|
|
||||||
|
|
||||||
let mk_res (self:t) headers body : (response,_) result =
|
|
||||||
let split_colon s =
|
|
||||||
match String.index s ':' with
|
|
||||||
| exception Not_found ->
|
|
||||||
raise (Parse_error (Curl.CURLE_CONV_FAILED, "header line without a ':': " ^ s))
|
|
||||||
| i ->
|
|
||||||
String.sub s 0 i,
|
|
||||||
String.trim (String.sub s (i+1) (String.length s-i-1))
|
|
||||||
in
|
|
||||||
try
|
|
||||||
let code = Curl.get_httpcode self in
|
|
||||||
let headers =
|
|
||||||
match headers with
|
|
||||||
| [] -> []
|
|
||||||
| _ :: tl -> List.map split_colon tl (* first one is "http1.1 NNN <descr>" *)
|
|
||||||
in
|
|
||||||
let info = {
|
|
||||||
ri_redirect_count=Curl.get_redirectcount self;
|
|
||||||
ri_response_time=Curl.get_totaltime self;
|
|
||||||
} in
|
|
||||||
Ok {headers; code; body; info}
|
|
||||||
with Parse_error (e, msg) ->
|
|
||||||
Error (e, Curl.strerror e ^ ": " ^ msg)
|
|
||||||
|
|
||||||
module Make(IO : IO)
|
|
||||||
: S with type 'a io = 'a IO.t
|
|
||||||
= struct
|
|
||||||
open IO
|
|
||||||
|
|
||||||
type 'a io = 'a IO.t
|
|
||||||
|
|
||||||
let content_read_fun_ content =
|
|
||||||
match content with
|
|
||||||
| `String s ->
|
|
||||||
let n = ref 0 in
|
|
||||||
(fun i ->
|
|
||||||
let len = min i (String.length s - !n) in
|
|
||||||
let r = String.sub s !n len in
|
|
||||||
n := !n + len;
|
|
||||||
r)
|
|
||||||
| `Write f ->
|
|
||||||
let buf = Bytes.create 1024 in
|
|
||||||
(fun i ->
|
|
||||||
let len = min i (Bytes.length buf) in
|
|
||||||
let n = f buf len in
|
|
||||||
Bytes.sub_string buf i n)
|
|
||||||
|
|
||||||
let http
|
|
||||||
?(tries=1) ?client ?(config=Config.default) ?range ?content ?(headers=[]) ~url ~meth ()
|
|
||||||
: _ result io =
|
|
||||||
let do_cleanup, self = match client with
|
|
||||||
| None -> true, make()
|
|
||||||
| Some c ->
|
|
||||||
Curl.reset c;
|
|
||||||
false, c
|
|
||||||
in
|
|
||||||
_apply_config self config;
|
|
||||||
opt_iter range ~f:(fun s -> Curl.set_range self s);
|
|
||||||
(* TODO: ability to make content a stream with a `read` function *)
|
|
||||||
opt_iter content
|
|
||||||
~f:(fun content ->
|
|
||||||
Curl.set_readfunction self (content_read_fun_ content));
|
|
||||||
(* local state *)
|
|
||||||
let tries = max tries 1 in (* at least one attempt *)
|
|
||||||
let body = Buffer.create 64 in
|
|
||||||
let resp_headers = ref [] in
|
|
||||||
let resp_headers_done = ref false in (* once we get "\r\n" header line *)
|
|
||||||
Curl.set_url self url;
|
|
||||||
begin match meth with
|
|
||||||
| POST l -> Curl.set_httppost self l;
|
|
||||||
| GET -> Curl.set_httpget self true;
|
|
||||||
| PUT -> Curl.set_put self true;
|
|
||||||
| DELETE -> Curl.set_customrequest self "DELETE";
|
|
||||||
| HEAD -> Curl.set_customrequest self "HEAD"
|
|
||||||
| CONNECT -> Curl.set_customrequest self "CONNECT"
|
|
||||||
| OPTIONS -> Curl.set_customrequest self "OPTIONS"
|
|
||||||
| TRACE -> Curl.set_customrequest self "TRACE"
|
|
||||||
| PATCH -> Curl.set_customrequest self "PATCH"
|
|
||||||
end;
|
|
||||||
_set_headers self headers;
|
|
||||||
Curl.set_headerfunction self
|
|
||||||
(fun s0 ->
|
|
||||||
let s = String.trim s0 in
|
|
||||||
(* Printf.printf "got header %S\n%!" s0; *)
|
|
||||||
if s0 = "\r\n" then (
|
|
||||||
resp_headers_done := true;
|
|
||||||
) else (
|
|
||||||
(* redirection: drop previous headers *)
|
|
||||||
if !resp_headers_done then (
|
|
||||||
resp_headers_done := false;
|
|
||||||
resp_headers := [];
|
|
||||||
);
|
|
||||||
|
|
||||||
resp_headers := s :: !resp_headers;
|
|
||||||
);
|
|
||||||
String.length s0);
|
|
||||||
Curl.set_writefunction self
|
|
||||||
(fun s ->
|
|
||||||
Buffer.add_string body s;
|
|
||||||
String.length s);
|
|
||||||
let rec loop i =
|
|
||||||
IO.perform self >>= function
|
|
||||||
| Curl.CURLE_OK ->
|
|
||||||
let r = mk_res self (List.rev !resp_headers) (Buffer.contents body) in
|
|
||||||
if do_cleanup then Curl.cleanup self;
|
|
||||||
return r
|
|
||||||
| Curl.CURLE_AGAIN when i > 1 ->
|
|
||||||
loop (i-1) (* try again *)
|
|
||||||
| c ->
|
|
||||||
if do_cleanup then Curl.cleanup self;
|
|
||||||
return (Error (c, Curl.strerror c))
|
|
||||||
in
|
|
||||||
loop tries
|
|
||||||
|
|
||||||
let get ?tries ?client ?config ?range ?headers ~url () : _ result io =
|
|
||||||
http ?tries ?client ?config ?range ?headers ~url ~meth:GET ()
|
|
||||||
|
|
||||||
let post ?tries ?client ?config ?headers ?content ~params ~url () : _ result io =
|
|
||||||
http ?tries ?client ?config ?headers ?content ~url ~meth:(POST params) ()
|
|
||||||
|
|
||||||
let put ?tries ?client ?config ?headers ~url ~content () : _ result io =
|
|
||||||
http ?tries ?client ?config ?headers ~url ~content ~meth:PUT ()
|
|
||||||
end
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
|
|
||||||
(** {1 Core signatures and implementation} *)
|
|
||||||
|
|
||||||
module Config : sig
|
|
||||||
type t
|
|
||||||
val default : t
|
|
||||||
val verbose : bool -> t -> t
|
|
||||||
val authmethod : Curl.curlAuth list -> t -> t
|
|
||||||
val max_redirects : int -> t -> t
|
|
||||||
val follow_location : bool -> t -> t
|
|
||||||
val username : string -> t -> t
|
|
||||||
val password : string -> t -> t
|
|
||||||
|
|
||||||
val pp : Format.formatter -> t -> unit
|
|
||||||
val to_string : t -> string
|
|
||||||
end
|
|
||||||
|
|
||||||
type t = Curl.t
|
|
||||||
|
|
||||||
val make :
|
|
||||||
?set_opts:(t -> unit) ->
|
|
||||||
unit -> t
|
|
||||||
|
|
||||||
val delete : t -> unit
|
|
||||||
|
|
||||||
val with_client :
|
|
||||||
?set_opts:(t -> unit) ->
|
|
||||||
(t -> 'a) -> 'a
|
|
||||||
(** Make a temporary client, call the function with it, then cleanup *)
|
|
||||||
|
|
||||||
(* TODO: duphandle is deprecated, how do we iterate on options?
|
|
||||||
val copy : t -> t
|
|
||||||
*)
|
|
||||||
|
|
||||||
type response_info = {
|
|
||||||
ri_response_time: float;
|
|
||||||
ri_redirect_count: int;
|
|
||||||
}
|
|
||||||
|
|
||||||
val pp_response_info : Format.formatter -> response_info -> unit
|
|
||||||
val string_of_response_info : response_info -> string
|
|
||||||
|
|
||||||
type response = {
|
|
||||||
code: int;
|
|
||||||
headers: (string * string) list;
|
|
||||||
body: string;
|
|
||||||
info: response_info;
|
|
||||||
}
|
|
||||||
|
|
||||||
val pp_response : Format.formatter -> response -> unit
|
|
||||||
val string_of_response : response -> string
|
|
||||||
|
|
||||||
(** The {{: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods} HTTP method}
|
|
||||||
to use *)
|
|
||||||
type meth =
|
|
||||||
| GET
|
|
||||||
| POST of Curl.curlHTTPPost list
|
|
||||||
| PUT
|
|
||||||
| DELETE
|
|
||||||
| HEAD
|
|
||||||
| CONNECT
|
|
||||||
| OPTIONS
|
|
||||||
| TRACE
|
|
||||||
| PATCH
|
|
||||||
|
|
||||||
val pp_meth : Format.formatter -> meth -> unit
|
|
||||||
val string_of_meth : meth -> string
|
|
||||||
|
|
||||||
(** {2 Underlying IO Monad} *)
|
|
||||||
module type IO = sig
|
|
||||||
type 'a t
|
|
||||||
val return : 'a -> 'a t
|
|
||||||
val (>>=) : 'a t -> ('a -> 'b t) -> 'b t
|
|
||||||
val (>|=) : 'a t -> ('a -> 'b) -> 'b t
|
|
||||||
val fail : exn -> 'a t
|
|
||||||
val perform : Curl.t -> Curl.curlCode t
|
|
||||||
end
|
|
||||||
|
|
||||||
(** {2 Main Signature} *)
|
|
||||||
module type S = sig
|
|
||||||
type 'a io
|
|
||||||
|
|
||||||
val http :
|
|
||||||
?tries:int ->
|
|
||||||
?client:t ->
|
|
||||||
?config:Config.t ->
|
|
||||||
?range:string ->
|
|
||||||
?content:[`String of string | `Write of (bytes -> int -> int)] ->
|
|
||||||
?headers:(string*string) list ->
|
|
||||||
url:string ->
|
|
||||||
meth:meth ->
|
|
||||||
unit ->
|
|
||||||
(response, Curl.curlCode * string) result io
|
|
||||||
(** General purpose HTTP call via cURL.
|
|
||||||
@param url the URL to query
|
|
||||||
@param meth which method to use (see {!meth})
|
|
||||||
@param tries how many times to retry in case of [CURLE_AGAIN] code
|
|
||||||
@param client a client to reuse (instead of allocating a new one)
|
|
||||||
@param range an optional
|
|
||||||
{{: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests} byte range}
|
|
||||||
to fetch (either to get large pages
|
|
||||||
by chunks, or to resume an interrupted download).
|
|
||||||
@param config configuration to set
|
|
||||||
@param content the content to send as the query's body, either
|
|
||||||
a [`String s] to write a single string, or [`Write f]
|
|
||||||
where [f] is a callback that is called on a buffer [b] with len [n]
|
|
||||||
(as in [f b n]) and returns how many bytes it wrote in the buffer
|
|
||||||
[b] starting at index [0] (at most [n] bytes).
|
|
||||||
It must return [0] when the content is entirely written, and not
|
|
||||||
before.
|
|
||||||
@param headers headers of the query
|
|
||||||
*)
|
|
||||||
|
|
||||||
val get :
|
|
||||||
?tries:int ->
|
|
||||||
?client:t ->
|
|
||||||
?config:Config.t ->
|
|
||||||
?range:string ->
|
|
||||||
?headers:(string*string) list ->
|
|
||||||
url:string ->
|
|
||||||
unit ->
|
|
||||||
(response, Curl.curlCode * string) result io
|
|
||||||
(** Shortcut for [http ~meth:GET]
|
|
||||||
See {!http} for more info.
|
|
||||||
*)
|
|
||||||
|
|
||||||
val put :
|
|
||||||
?tries:int ->
|
|
||||||
?client:t ->
|
|
||||||
?config:Config.t ->
|
|
||||||
?headers:(string*string) list ->
|
|
||||||
url:string ->
|
|
||||||
content:[`String of string | `Write of (bytes -> int -> int)] ->
|
|
||||||
unit ->
|
|
||||||
(response, Curl.curlCode * string) result io
|
|
||||||
(** Shortcut for [http ~meth:PUT]
|
|
||||||
See {!http} for more info.
|
|
||||||
*)
|
|
||||||
|
|
||||||
val post :
|
|
||||||
?tries:int ->
|
|
||||||
?client:t ->
|
|
||||||
?config:Config.t ->
|
|
||||||
?headers:(string*string) list ->
|
|
||||||
?content:[`String of string | `Write of (bytes -> int -> int)] ->
|
|
||||||
params:Curl.curlHTTPPost list ->
|
|
||||||
url:string ->
|
|
||||||
unit ->
|
|
||||||
(response, Curl.curlCode * string) result io
|
|
||||||
(** Shortcut for [http ~meth:(POST params)]
|
|
||||||
See {!http} for more info.
|
|
||||||
*)
|
|
||||||
end
|
|
||||||
|
|
||||||
module Make(IO : IO) : S with type 'a io = 'a IO.t
|
|
||||||
|
|
@ -3,4 +3,4 @@
|
||||||
(name ezcurl_core)
|
(name ezcurl_core)
|
||||||
(public_name ezcurl.core)
|
(public_name ezcurl.core)
|
||||||
(flags :standard -warn-error -32)
|
(flags :standard -warn-error -32)
|
||||||
(libraries curl))
|
(libraries curl threads))
|
||||||
|
|
|
||||||
524
src/core/ezcurl_core.ml
Normal file
524
src/core/ezcurl_core.ml
Normal file
|
|
@ -0,0 +1,524 @@
|
||||||
|
let opt_iter ~f = function
|
||||||
|
| None -> ()
|
||||||
|
| Some x -> f x
|
||||||
|
|
||||||
|
module Config = struct
|
||||||
|
type t = {
|
||||||
|
verbose: bool;
|
||||||
|
authmethod: Curl.curlAuth list option;
|
||||||
|
max_redirects: int;
|
||||||
|
follow_location: bool;
|
||||||
|
username: string option;
|
||||||
|
password: string option;
|
||||||
|
user_agent: string option;
|
||||||
|
}
|
||||||
|
|
||||||
|
let default : t =
|
||||||
|
{
|
||||||
|
verbose = false;
|
||||||
|
max_redirects = 50;
|
||||||
|
follow_location = true;
|
||||||
|
authmethod = None;
|
||||||
|
username = None;
|
||||||
|
password = None;
|
||||||
|
user_agent = Some "curl";
|
||||||
|
}
|
||||||
|
|
||||||
|
let password x self = { self with password = Some x }
|
||||||
|
let username x self = { self with username = Some x }
|
||||||
|
let verbose x self = { self with verbose = x }
|
||||||
|
let follow_location x self = { self with follow_location = x }
|
||||||
|
let max_redirects x self = { self with max_redirects = max 1 x }
|
||||||
|
let authmethod x self = { self with authmethod = Some x }
|
||||||
|
|
||||||
|
let string_of_authmethod = function
|
||||||
|
| Curl.CURLAUTH_ANY -> "any"
|
||||||
|
| Curl.CURLAUTH_BASIC -> "basic"
|
||||||
|
| Curl.CURLAUTH_DIGEST -> "digest"
|
||||||
|
| Curl.CURLAUTH_GSSNEGOTIATE -> "gss_negotiate"
|
||||||
|
| Curl.CURLAUTH_NTLM -> "ntlm"
|
||||||
|
| Curl.CURLAUTH_ANYSAFE -> "any_safe"
|
||||||
|
|
||||||
|
let str_of_str_opt = function
|
||||||
|
| None -> "<none>"
|
||||||
|
| Some s -> s
|
||||||
|
|
||||||
|
let pp out (self : t) =
|
||||||
|
let {
|
||||||
|
verbose;
|
||||||
|
authmethod;
|
||||||
|
max_redirects;
|
||||||
|
follow_location;
|
||||||
|
username;
|
||||||
|
password;
|
||||||
|
user_agent;
|
||||||
|
} =
|
||||||
|
self
|
||||||
|
in
|
||||||
|
Format.fprintf out
|
||||||
|
"{@[verbose=%B;@ max_redirects=%d;@ follow_location=%B;@ username=%s;@ \
|
||||||
|
password=%s;@ authmethod=%s;@ user_agent=%s@]}"
|
||||||
|
verbose max_redirects follow_location (str_of_str_opt username)
|
||||||
|
(str_of_str_opt password)
|
||||||
|
(match authmethod with
|
||||||
|
| None -> "none"
|
||||||
|
| Some l -> List.map string_of_authmethod l |> String.concat ",")
|
||||||
|
(str_of_str_opt user_agent)
|
||||||
|
|
||||||
|
let to_string s = Format.asprintf "%a" pp s
|
||||||
|
end
|
||||||
|
|
||||||
|
type t = { curl: Curl.t } [@@unboxed]
|
||||||
|
type client = t
|
||||||
|
|
||||||
|
let _top_mutex = Mutex.create ()
|
||||||
|
|
||||||
|
let _with_mutex f =
|
||||||
|
Mutex.lock _top_mutex;
|
||||||
|
match f () with
|
||||||
|
| res ->
|
||||||
|
Mutex.unlock _top_mutex;
|
||||||
|
res
|
||||||
|
| exception e ->
|
||||||
|
Mutex.unlock _top_mutex;
|
||||||
|
raise e
|
||||||
|
|
||||||
|
let _init =
|
||||||
|
let initialized = ref false in
|
||||||
|
fun () ->
|
||||||
|
_with_mutex @@ fun () ->
|
||||||
|
if not !initialized then (
|
||||||
|
initialized := true;
|
||||||
|
Curl.global_init Curl.CURLINIT_GLOBALALL;
|
||||||
|
at_exit Curl.global_cleanup
|
||||||
|
)
|
||||||
|
|
||||||
|
let make ?(set_opts = fun _ -> ()) ?cookiejar_file
|
||||||
|
?(enable_session_cookies = false) () : t =
|
||||||
|
_init ();
|
||||||
|
let curl = Curl.init () in
|
||||||
|
Gc.finalise Curl.cleanup curl;
|
||||||
|
opt_iter cookiejar_file ~f:(fun file ->
|
||||||
|
Curl.set_cookiejar curl file;
|
||||||
|
Curl.set_cookiefile curl file);
|
||||||
|
if enable_session_cookies then Curl.set_cookiefile curl "";
|
||||||
|
set_opts curl;
|
||||||
|
{ curl }
|
||||||
|
|
||||||
|
let delete (self : t) = Curl.cleanup self.curl
|
||||||
|
let _cfg_no_signal = ref false (* default: 0 *)
|
||||||
|
let _get_no_signal () : bool = _with_mutex @@ fun () -> !_cfg_no_signal
|
||||||
|
let set_no_signal v = _with_mutex @@ fun () -> _cfg_no_signal := v
|
||||||
|
|
||||||
|
module Cookies = struct
|
||||||
|
let reload_cookiejar (self : t) : unit =
|
||||||
|
Curl.set_cookielist self.curl "RELOAD"
|
||||||
|
|
||||||
|
let flush_cookiejar (self : t) : unit = Curl.set_cookielist self.curl "FLUSH"
|
||||||
|
let get_cookies self = Curl.get_cookielist self.curl
|
||||||
|
|
||||||
|
let set_cookies self (l : string list) =
|
||||||
|
List.iter (Curl.set_cookielist self.curl) l
|
||||||
|
|
||||||
|
let transfer c1 c2 = set_cookies c2 @@ get_cookies c1
|
||||||
|
end
|
||||||
|
|
||||||
|
(* set options *)
|
||||||
|
let _apply_config (self : t) (config : Config.t) : unit =
|
||||||
|
let {
|
||||||
|
Config.verbose;
|
||||||
|
max_redirects;
|
||||||
|
follow_location;
|
||||||
|
authmethod;
|
||||||
|
username;
|
||||||
|
password;
|
||||||
|
user_agent;
|
||||||
|
} =
|
||||||
|
config
|
||||||
|
in
|
||||||
|
Curl.set_verbose self.curl verbose;
|
||||||
|
Curl.set_maxredirs self.curl max_redirects;
|
||||||
|
Curl.set_followlocation self.curl follow_location;
|
||||||
|
opt_iter user_agent ~f:(fun user_agent ->
|
||||||
|
Curl.set_useragent self.curl user_agent);
|
||||||
|
opt_iter authmethod ~f:(Curl.set_httpauth self.curl);
|
||||||
|
opt_iter username ~f:(Curl.set_username self.curl);
|
||||||
|
opt_iter password ~f:(Curl.set_password self.curl);
|
||||||
|
Curl.set_nosignal self.curl (_get_no_signal ());
|
||||||
|
()
|
||||||
|
|
||||||
|
let _set_headers (self : t) (headers : _ list) : unit =
|
||||||
|
let headers = List.map (fun (k, v) -> k ^ ": " ^ v) headers in
|
||||||
|
Curl.set_httpheader self.curl headers;
|
||||||
|
()
|
||||||
|
|
||||||
|
let with_client ?set_opts f =
|
||||||
|
let c = make ?set_opts () in
|
||||||
|
try
|
||||||
|
let x = f c in
|
||||||
|
delete c;
|
||||||
|
x
|
||||||
|
with e ->
|
||||||
|
delete c;
|
||||||
|
raise e
|
||||||
|
|
||||||
|
type response_info = {
|
||||||
|
ri_response_time: float;
|
||||||
|
ri_redirect_count: int;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pp_response_info out r =
|
||||||
|
let { ri_response_time; ri_redirect_count } = r in
|
||||||
|
Format.fprintf out "{@[response_time=%.3fs;@ redirect_count=%d@]}"
|
||||||
|
ri_response_time ri_redirect_count
|
||||||
|
|
||||||
|
let string_of_response_info s = Format.asprintf "%a" pp_response_info s
|
||||||
|
|
||||||
|
type 'body response = {
|
||||||
|
code: int;
|
||||||
|
headers: (string * string) list;
|
||||||
|
body: 'body;
|
||||||
|
info: response_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pp_response_with ppbody out r =
|
||||||
|
let pp_header out (s1, s2) = Format.fprintf out "@[<2>%s:@ %s@]" s1 s2 in
|
||||||
|
let pp_headers out l =
|
||||||
|
Format.fprintf out "@[<v>%a@]" (Format.pp_print_list pp_header) l
|
||||||
|
in
|
||||||
|
let { code; body; headers; info } = r in
|
||||||
|
Format.fprintf out "{@[code=%d;@ headers=@[%a@];@ info=%a;@ body=@[%a@]@]}"
|
||||||
|
code pp_headers headers pp_response_info info ppbody body
|
||||||
|
|
||||||
|
let pp_response = pp_response_with Format.pp_print_text
|
||||||
|
let string_of_response s = Format.asprintf "%a" pp_response s
|
||||||
|
|
||||||
|
type meth =
|
||||||
|
| GET
|
||||||
|
| POST of Curl.curlHTTPPost list
|
||||||
|
| PUT
|
||||||
|
| DELETE
|
||||||
|
| HEAD
|
||||||
|
| CONNECT
|
||||||
|
| OPTIONS
|
||||||
|
| TRACE
|
||||||
|
| PATCH
|
||||||
|
|
||||||
|
let string_of_meth = function
|
||||||
|
| GET -> "GET"
|
||||||
|
| POST _ -> "POST"
|
||||||
|
| PUT -> "PUT"
|
||||||
|
| DELETE -> "DELETE"
|
||||||
|
| HEAD -> "HEAD"
|
||||||
|
| CONNECT -> "CONNECT"
|
||||||
|
| OPTIONS -> "OPTIONS"
|
||||||
|
| TRACE -> "TRACE"
|
||||||
|
| PATCH -> "PATCH"
|
||||||
|
|
||||||
|
let pp_meth out m = Format.pp_print_string out (string_of_meth m)
|
||||||
|
|
||||||
|
module type IO = sig
|
||||||
|
type 'a t
|
||||||
|
|
||||||
|
val return : 'a -> 'a t
|
||||||
|
val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
|
||||||
|
val ( >|= ) : 'a t -> ('a -> 'b) -> 'b t
|
||||||
|
val fail : exn -> 'a t
|
||||||
|
val perform : Curl.t -> Curl.curlCode t
|
||||||
|
end
|
||||||
|
|
||||||
|
module type S = sig
|
||||||
|
type 'a io
|
||||||
|
|
||||||
|
val http :
|
||||||
|
?tries:int ->
|
||||||
|
?client:t ->
|
||||||
|
?config:Config.t ->
|
||||||
|
?range:string ->
|
||||||
|
?content:[ `String of string | `Write of bytes -> int -> int ] ->
|
||||||
|
?headers:(string * string) list ->
|
||||||
|
url:string ->
|
||||||
|
meth:meth ->
|
||||||
|
unit ->
|
||||||
|
(string response, Curl.curlCode * string) result io
|
||||||
|
(** General purpose HTTP call via cURL.
|
||||||
|
@param url the URL to query
|
||||||
|
@param meth which method to use (see {!meth})
|
||||||
|
@param tries how many times to retry in case of [CURLE_AGAIN] code
|
||||||
|
@param client a client to reuse (instead of allocating a new one)
|
||||||
|
@param range an optional
|
||||||
|
{{: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests} byte range}
|
||||||
|
to fetch (either to get large pages
|
||||||
|
by chunks, or to resume an interrupted download).
|
||||||
|
@param config configuration to set
|
||||||
|
@param content the content to send as the query's body, either
|
||||||
|
a [`String s] to write a single string, or [`Write f]
|
||||||
|
where [f] is a callback that is called on a buffer [b] with len [n]
|
||||||
|
(as in [f b n]) and returns how many bytes it wrote in the buffer
|
||||||
|
[b] starting at index [0] (at most [n] bytes).
|
||||||
|
It must return [0] when the content is entirely written, and not
|
||||||
|
before.
|
||||||
|
@param headers headers of the query
|
||||||
|
*)
|
||||||
|
|
||||||
|
(** Push-stream of bytes
|
||||||
|
@since NEXT_RELEASE *)
|
||||||
|
class type input_stream = object
|
||||||
|
method on_close : unit -> unit
|
||||||
|
method on_input : bytes -> int -> int -> unit
|
||||||
|
end
|
||||||
|
|
||||||
|
val http_stream :
|
||||||
|
?tries:int ->
|
||||||
|
?client:t ->
|
||||||
|
?config:Config.t ->
|
||||||
|
?range:string ->
|
||||||
|
?content:[ `String of string | `Write of bytes -> int -> int ] ->
|
||||||
|
?headers:(string * string) list ->
|
||||||
|
url:string ->
|
||||||
|
meth:meth ->
|
||||||
|
write_into:#input_stream ->
|
||||||
|
unit ->
|
||||||
|
(unit response, Curl.curlCode * string) result io
|
||||||
|
(** HTTP call via cURL, with a streaming response body.
|
||||||
|
@since NEXT_RELEASE *)
|
||||||
|
|
||||||
|
val get :
|
||||||
|
?tries:int ->
|
||||||
|
?client:t ->
|
||||||
|
?config:Config.t ->
|
||||||
|
?range:string ->
|
||||||
|
?headers:(string * string) list ->
|
||||||
|
url:string ->
|
||||||
|
unit ->
|
||||||
|
(string response, Curl.curlCode * string) result io
|
||||||
|
(** Shortcut for [http ~meth:GET]
|
||||||
|
See {!http} for more info.
|
||||||
|
*)
|
||||||
|
|
||||||
|
val put :
|
||||||
|
?tries:int ->
|
||||||
|
?client:t ->
|
||||||
|
?config:Config.t ->
|
||||||
|
?headers:(string * string) list ->
|
||||||
|
url:string ->
|
||||||
|
content:[ `String of string | `Write of bytes -> int -> int ] ->
|
||||||
|
unit ->
|
||||||
|
(string response, Curl.curlCode * string) result io
|
||||||
|
(** Shortcut for [http ~meth:PUT]
|
||||||
|
See {!http} for more info.
|
||||||
|
*)
|
||||||
|
|
||||||
|
val post :
|
||||||
|
?tries:int ->
|
||||||
|
?client:t ->
|
||||||
|
?config:Config.t ->
|
||||||
|
?headers:(string * string) list ->
|
||||||
|
?content:[ `String of string | `Write of bytes -> int -> int ] ->
|
||||||
|
params:Curl.curlHTTPPost list ->
|
||||||
|
url:string ->
|
||||||
|
unit ->
|
||||||
|
(string response, Curl.curlCode * string) result io
|
||||||
|
(** Shortcut for [http ~meth:(POST params)]
|
||||||
|
See {!http} for more info.
|
||||||
|
*)
|
||||||
|
end
|
||||||
|
|
||||||
|
exception Parse_error of Curl.curlCode * string
|
||||||
|
|
||||||
|
let mk_res (self : t) headers body : (_ response, _) result =
|
||||||
|
let split_colon s =
|
||||||
|
match String.index s ':' with
|
||||||
|
| exception Not_found ->
|
||||||
|
raise
|
||||||
|
(Parse_error (Curl.CURLE_CONV_FAILED, "header line without a ':': " ^ s))
|
||||||
|
| i ->
|
||||||
|
( String.sub s 0 i,
|
||||||
|
String.trim (String.sub s (i + 1) (String.length s - i - 1)) )
|
||||||
|
in
|
||||||
|
try
|
||||||
|
let code = Curl.get_httpcode self.curl in
|
||||||
|
let headers =
|
||||||
|
match headers with
|
||||||
|
| [] -> []
|
||||||
|
| _ :: tl ->
|
||||||
|
List.map split_colon tl (* first one is "http1.1 NNN <descr>" *)
|
||||||
|
in
|
||||||
|
let info =
|
||||||
|
{
|
||||||
|
ri_redirect_count = Curl.get_redirectcount self.curl;
|
||||||
|
ri_response_time = Curl.get_totaltime self.curl;
|
||||||
|
}
|
||||||
|
in
|
||||||
|
Ok { headers; code; body; info }
|
||||||
|
with Parse_error (e, msg) -> Error (e, Curl.strerror e ^ ": " ^ msg)
|
||||||
|
|
||||||
|
module Make (IO : IO) : S with type 'a io = 'a IO.t = struct
|
||||||
|
open IO
|
||||||
|
|
||||||
|
type 'a io = 'a IO.t
|
||||||
|
|
||||||
|
let content_read_fun_ content =
|
||||||
|
match content with
|
||||||
|
| `String s ->
|
||||||
|
let n = ref 0 in
|
||||||
|
fun i ->
|
||||||
|
let len = min i (String.length s - !n) in
|
||||||
|
let r = String.sub s !n len in
|
||||||
|
n := !n + len;
|
||||||
|
r
|
||||||
|
| `Write f ->
|
||||||
|
let buf = Bytes.create 1024 in
|
||||||
|
fun i ->
|
||||||
|
let len = min i (Bytes.length buf) in
|
||||||
|
let n = f buf len in
|
||||||
|
Bytes.sub_string buf i n
|
||||||
|
|
||||||
|
let content_size_ = function
|
||||||
|
| `String s -> Some (String.length s)
|
||||||
|
| `Write _ -> None
|
||||||
|
|
||||||
|
class type input_stream = object
|
||||||
|
method on_close : unit -> unit
|
||||||
|
method on_input : bytes -> int -> int -> unit
|
||||||
|
end
|
||||||
|
|
||||||
|
type http_state_ = {
|
||||||
|
client: client;
|
||||||
|
do_cleanup: bool;
|
||||||
|
mutable resp_headers: string list;
|
||||||
|
mutable resp_headers_done: bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
let http_setup_ ?client ?(config = Config.default) ?range ?content
|
||||||
|
?(headers = []) ~url ~meth () : http_state_ =
|
||||||
|
let headers = ref headers in
|
||||||
|
let do_cleanup, self =
|
||||||
|
match client with
|
||||||
|
| None -> true, make ()
|
||||||
|
| Some c -> false, c
|
||||||
|
in
|
||||||
|
_apply_config self config;
|
||||||
|
opt_iter range ~f:(fun s -> Curl.set_range self.curl s);
|
||||||
|
|
||||||
|
(* TODO: ability to make content a stream with a `read` function *)
|
||||||
|
opt_iter content ~f:(fun content ->
|
||||||
|
Curl.set_readfunction self.curl (content_read_fun_ content);
|
||||||
|
(* also set size if known *)
|
||||||
|
match content_size_ content, meth with
|
||||||
|
| None, _ ->
|
||||||
|
headers :=
|
||||||
|
("expect", "") :: ("transfer-encoding", "chunked") :: !headers
|
||||||
|
| Some size, POST _ -> Curl.set_postfieldsize self.curl size
|
||||||
|
| Some size, _ -> Curl.set_infilesize self.curl size);
|
||||||
|
|
||||||
|
(* local state *)
|
||||||
|
let st =
|
||||||
|
{
|
||||||
|
do_cleanup;
|
||||||
|
client = self;
|
||||||
|
resp_headers = [];
|
||||||
|
resp_headers_done = false;
|
||||||
|
}
|
||||||
|
in
|
||||||
|
|
||||||
|
(* once we get "\r\n" header line *)
|
||||||
|
Curl.set_url self.curl url;
|
||||||
|
(match meth with
|
||||||
|
| POST [] when content <> None -> Curl.set_post self.curl true
|
||||||
|
| POST l -> Curl.set_httppost self.curl l
|
||||||
|
| GET -> Curl.set_httpget self.curl true
|
||||||
|
| PUT ->
|
||||||
|
Curl.set_customrequest self.curl "PUT";
|
||||||
|
Curl.set_upload self.curl true
|
||||||
|
| DELETE -> Curl.set_customrequest self.curl "DELETE"
|
||||||
|
| HEAD -> Curl.set_customrequest self.curl "HEAD"
|
||||||
|
| CONNECT -> Curl.set_customrequest self.curl "CONNECT"
|
||||||
|
| OPTIONS -> Curl.set_customrequest self.curl "OPTIONS"
|
||||||
|
| TRACE -> Curl.set_customrequest self.curl "TRACE"
|
||||||
|
| PATCH -> Curl.set_customrequest self.curl "PATCH");
|
||||||
|
|
||||||
|
_set_headers self !headers;
|
||||||
|
Curl.set_headerfunction self.curl (fun s0 ->
|
||||||
|
let s = String.trim s0 in
|
||||||
|
(* Printf.printf "got header %S\n%!" s0; *)
|
||||||
|
if s0 = "\r\n" then
|
||||||
|
st.resp_headers_done <- true
|
||||||
|
else (
|
||||||
|
(* redirection: drop previous headers *)
|
||||||
|
if st.resp_headers_done then (
|
||||||
|
st.resp_headers_done <- false;
|
||||||
|
st.resp_headers <- []
|
||||||
|
);
|
||||||
|
|
||||||
|
st.resp_headers <- s :: st.resp_headers
|
||||||
|
);
|
||||||
|
String.length s0);
|
||||||
|
|
||||||
|
st
|
||||||
|
|
||||||
|
let http ?(tries = 1) ?client ?config ?range ?content ?headers ~url ~meth () :
|
||||||
|
(string response, _) result io =
|
||||||
|
(* at least one attempt *)
|
||||||
|
let tries = max tries 1 in
|
||||||
|
let st =
|
||||||
|
http_setup_ ?client ?config ?range ?content ?headers ~url ~meth ()
|
||||||
|
in
|
||||||
|
|
||||||
|
let body = Buffer.create 64 in
|
||||||
|
Curl.set_writefunction st.client.curl (fun s ->
|
||||||
|
Buffer.add_string body s;
|
||||||
|
String.length s);
|
||||||
|
|
||||||
|
let rec loop i =
|
||||||
|
IO.perform st.client.curl >>= function
|
||||||
|
| Curl.CURLE_OK ->
|
||||||
|
let r =
|
||||||
|
mk_res st.client (List.rev st.resp_headers) (Buffer.contents body)
|
||||||
|
in
|
||||||
|
if st.do_cleanup then Curl.cleanup st.client.curl;
|
||||||
|
return r
|
||||||
|
| Curl.CURLE_AGAIN when i > 1 -> loop (i - 1) (* try again *)
|
||||||
|
| c ->
|
||||||
|
if st.do_cleanup then Curl.cleanup st.client.curl;
|
||||||
|
return (Error (c, Curl.strerror c))
|
||||||
|
in
|
||||||
|
loop tries
|
||||||
|
|
||||||
|
let http_stream ?(tries = 1) ?client ?config ?range ?content ?headers ~url
|
||||||
|
~meth ~(write_into : #input_stream) () : (unit response, _) result io =
|
||||||
|
let tries = max tries 1 in
|
||||||
|
let st =
|
||||||
|
http_setup_ ?client ?config ?range ?content ?headers ~url ~meth ()
|
||||||
|
in
|
||||||
|
|
||||||
|
Curl.set_writefunction st.client.curl (fun s ->
|
||||||
|
let n = String.length s in
|
||||||
|
write_into#on_input (Bytes.unsafe_of_string s) 0 n;
|
||||||
|
n);
|
||||||
|
|
||||||
|
let rec loop i =
|
||||||
|
IO.perform st.client.curl >>= function
|
||||||
|
| Curl.CURLE_OK ->
|
||||||
|
let r = mk_res st.client (List.rev st.resp_headers) () in
|
||||||
|
write_into#on_close ();
|
||||||
|
if st.do_cleanup then Curl.cleanup st.client.curl;
|
||||||
|
return r
|
||||||
|
| Curl.CURLE_AGAIN when i > 1 -> loop (i - 1) (* try again *)
|
||||||
|
| c ->
|
||||||
|
write_into#on_close ();
|
||||||
|
if st.do_cleanup then Curl.cleanup st.client.curl;
|
||||||
|
return (Error (c, Curl.strerror c))
|
||||||
|
in
|
||||||
|
loop tries
|
||||||
|
|
||||||
|
let get ?tries ?client ?config ?range ?headers ~url () : _ result io =
|
||||||
|
http ?tries ?client ?config ?range ?headers ~url ~meth:GET ()
|
||||||
|
|
||||||
|
let post ?tries ?client ?config ?headers ?content ~params ~url () :
|
||||||
|
_ result io =
|
||||||
|
http ?tries ?client ?config ?headers ?content ~url ~meth:(POST params) ()
|
||||||
|
|
||||||
|
let put ?tries ?client ?config ?headers ~url ~content () : _ result io =
|
||||||
|
http ?tries ?client ?config ?headers ~url ~content ~meth:PUT ()
|
||||||
|
end
|
||||||
229
src/core/ezcurl_core.mli
Normal file
229
src/core/ezcurl_core.mli
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
(** Core signatures and implementation *)
|
||||||
|
|
||||||
|
(** Configuration for the client. *)
|
||||||
|
module Config : sig
|
||||||
|
type t
|
||||||
|
|
||||||
|
val default : t
|
||||||
|
val verbose : bool -> t -> t
|
||||||
|
val authmethod : Curl.curlAuth list -> t -> t
|
||||||
|
val max_redirects : int -> t -> t
|
||||||
|
val follow_location : bool -> t -> t
|
||||||
|
val username : string -> t -> t
|
||||||
|
val password : string -> t -> t
|
||||||
|
val pp : Format.formatter -> t -> unit
|
||||||
|
val to_string : t -> string
|
||||||
|
end
|
||||||
|
|
||||||
|
type t = private { curl: Curl.t } [@@unboxed]
|
||||||
|
(** A client, i.e. a cURL instance.
|
||||||
|
The wrapping record has been present since NEXT_RELEASE *)
|
||||||
|
|
||||||
|
val make :
|
||||||
|
?set_opts:(Curl.t -> unit) ->
|
||||||
|
?cookiejar_file:string ->
|
||||||
|
?enable_session_cookies:bool ->
|
||||||
|
unit ->
|
||||||
|
t
|
||||||
|
(** Create a new client.
|
||||||
|
@param set_opts called before returning the client, to set options
|
||||||
|
@param cookiejar_file if provided, tell curl to use the given file path to store/load cookies (since NEXT_RELEASE)
|
||||||
|
@param enable_session_cookies if provided, enable cookie handling in curl so it store/load cookies (since NEXT_RELEASE)
|
||||||
|
*)
|
||||||
|
|
||||||
|
val delete : t -> unit
|
||||||
|
(** Delete the client. It cannot be used anymore. *)
|
||||||
|
|
||||||
|
val with_client : ?set_opts:(Curl.t -> unit) -> (t -> 'a) -> 'a
|
||||||
|
(** Make a temporary client, call the function with it, then cleanup. *)
|
||||||
|
|
||||||
|
val set_no_signal : bool -> unit
|
||||||
|
(** Set no_signal default value for each new client instance. Default is [true].
|
||||||
|
See [CURLOPT_NOSIGNAL].
|
||||||
|
@since NEXT_RELEASE *)
|
||||||
|
|
||||||
|
(** Cookie handling.
|
||||||
|
|
||||||
|
@since NEXT_RELEASE *)
|
||||||
|
module Cookies : sig
|
||||||
|
val flush_cookiejar : t -> unit
|
||||||
|
(** If [cookiejar_file] was provided in {!make}, this flushes the current set of cookies
|
||||||
|
to the provided file.
|
||||||
|
@since NEXT_RELEASE *)
|
||||||
|
|
||||||
|
val reload_cookiejar : t -> unit
|
||||||
|
(** If [cookiejar_file] was provided in {!make}, this reloads cookies from
|
||||||
|
the provided file.
|
||||||
|
@since NEXT_RELEASE *)
|
||||||
|
|
||||||
|
val get_cookies : t -> string list
|
||||||
|
(** Get cookie list (in netscape format) *)
|
||||||
|
|
||||||
|
val set_cookies : t -> string list -> unit
|
||||||
|
(** Set cookie list (in netscape format) *)
|
||||||
|
|
||||||
|
val transfer : t -> t -> unit
|
||||||
|
(** [transfer c1 c2] copies cookies in [c1] into [c2] *)
|
||||||
|
end
|
||||||
|
|
||||||
|
(* TODO: duphandle is deprecated, how do we iterate on options?
|
||||||
|
val copy : t -> t
|
||||||
|
*)
|
||||||
|
|
||||||
|
type response_info = {
|
||||||
|
ri_response_time: float;
|
||||||
|
(** Total time (in seconds) for the request/response pair.
|
||||||
|
See {!Curl.get_totaltime}. *)
|
||||||
|
ri_redirect_count: int;
|
||||||
|
(** Number of redirects cURL followed.
|
||||||
|
See {!Curl.get_redirectcount}. *)
|
||||||
|
}
|
||||||
|
(** Metadata about a response from the server. *)
|
||||||
|
|
||||||
|
val pp_response_info : Format.formatter -> response_info -> unit
|
||||||
|
val string_of_response_info : response_info -> string
|
||||||
|
|
||||||
|
type 'body response = {
|
||||||
|
code: int;
|
||||||
|
(** Response code. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status *)
|
||||||
|
headers: (string * string) list; (** Response headers *)
|
||||||
|
body: 'body; (** Response body, or [""] *)
|
||||||
|
info: response_info; (** Information about the response *)
|
||||||
|
}
|
||||||
|
(** Response for a given request. *)
|
||||||
|
|
||||||
|
val pp_response_with :
|
||||||
|
(Format.formatter -> 'a -> unit) -> Format.formatter -> 'a response -> unit
|
||||||
|
|
||||||
|
val pp_response : Format.formatter -> string response -> unit
|
||||||
|
val string_of_response : string response -> string
|
||||||
|
|
||||||
|
(** The {{: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods} HTTP method}
|
||||||
|
to use *)
|
||||||
|
type meth =
|
||||||
|
| GET
|
||||||
|
| POST of Curl.curlHTTPPost list
|
||||||
|
| PUT
|
||||||
|
| DELETE
|
||||||
|
| HEAD
|
||||||
|
| CONNECT
|
||||||
|
| OPTIONS
|
||||||
|
| TRACE
|
||||||
|
| PATCH
|
||||||
|
|
||||||
|
val pp_meth : Format.formatter -> meth -> unit
|
||||||
|
val string_of_meth : meth -> string
|
||||||
|
|
||||||
|
(** {2 Underlying IO Monad} *)
|
||||||
|
module type IO = sig
|
||||||
|
type 'a t
|
||||||
|
|
||||||
|
val return : 'a -> 'a t
|
||||||
|
val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
|
||||||
|
val ( >|= ) : 'a t -> ('a -> 'b) -> 'b t
|
||||||
|
val fail : exn -> 'a t
|
||||||
|
val perform : Curl.t -> Curl.curlCode t
|
||||||
|
end
|
||||||
|
|
||||||
|
(** {2 Main Signature} *)
|
||||||
|
module type S = sig
|
||||||
|
type 'a io
|
||||||
|
|
||||||
|
val http :
|
||||||
|
?tries:int ->
|
||||||
|
?client:t ->
|
||||||
|
?config:Config.t ->
|
||||||
|
?range:string ->
|
||||||
|
?content:[ `String of string | `Write of bytes -> int -> int ] ->
|
||||||
|
?headers:(string * string) list ->
|
||||||
|
url:string ->
|
||||||
|
meth:meth ->
|
||||||
|
unit ->
|
||||||
|
(string response, Curl.curlCode * string) result io
|
||||||
|
(** General purpose HTTP call via cURL.
|
||||||
|
@param url the URL to query
|
||||||
|
@param meth which method to use (see {!meth})
|
||||||
|
@param tries how many times to retry in case of [CURLE_AGAIN] code
|
||||||
|
@param client a client to reuse (instead of allocating a new one)
|
||||||
|
@param range an optional
|
||||||
|
{{: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests} byte range}
|
||||||
|
to fetch (either to get large pages
|
||||||
|
by chunks, or to resume an interrupted download).
|
||||||
|
@param config configuration to set
|
||||||
|
@param content the content to send as the query's body, either
|
||||||
|
a [`String s] to write a single string, or [`Write f]
|
||||||
|
where [f] is a callback that is called on a buffer [b] with len [n]
|
||||||
|
(as in [f b n]) and returns how many bytes it wrote in the buffer
|
||||||
|
[b] starting at index [0] (at most [n] bytes).
|
||||||
|
It must return [0] when the content is entirely written, and not
|
||||||
|
before.
|
||||||
|
@param headers headers of the query
|
||||||
|
*)
|
||||||
|
|
||||||
|
(** Push-based stream of bytes
|
||||||
|
@since NEXT_RELEASE *)
|
||||||
|
class type input_stream = object
|
||||||
|
method on_close : unit -> unit
|
||||||
|
method on_input : bytes -> int -> int -> unit
|
||||||
|
end
|
||||||
|
|
||||||
|
val http_stream :
|
||||||
|
?tries:int ->
|
||||||
|
?client:t ->
|
||||||
|
?config:Config.t ->
|
||||||
|
?range:string ->
|
||||||
|
?content:[ `String of string | `Write of bytes -> int -> int ] ->
|
||||||
|
?headers:(string * string) list ->
|
||||||
|
url:string ->
|
||||||
|
meth:meth ->
|
||||||
|
write_into:#input_stream ->
|
||||||
|
unit ->
|
||||||
|
(unit response, Curl.curlCode * string) result io
|
||||||
|
(** HTTP call via cURL, with a streaming response body.
|
||||||
|
The body is given to [write_into] by chunks,
|
||||||
|
then [write_into#on_close ()] is called
|
||||||
|
and the response is returned.
|
||||||
|
@since NEXT_RELEASE *)
|
||||||
|
|
||||||
|
val get :
|
||||||
|
?tries:int ->
|
||||||
|
?client:t ->
|
||||||
|
?config:Config.t ->
|
||||||
|
?range:string ->
|
||||||
|
?headers:(string * string) list ->
|
||||||
|
url:string ->
|
||||||
|
unit ->
|
||||||
|
(string response, Curl.curlCode * string) result io
|
||||||
|
(** Shortcut for [http ~meth:GET]
|
||||||
|
See {!http} for more info.
|
||||||
|
*)
|
||||||
|
|
||||||
|
val put :
|
||||||
|
?tries:int ->
|
||||||
|
?client:t ->
|
||||||
|
?config:Config.t ->
|
||||||
|
?headers:(string * string) list ->
|
||||||
|
url:string ->
|
||||||
|
content:[ `String of string | `Write of bytes -> int -> int ] ->
|
||||||
|
unit ->
|
||||||
|
(string response, Curl.curlCode * string) result io
|
||||||
|
(** Shortcut for [http ~meth:PUT]
|
||||||
|
See {!http} for more info.
|
||||||
|
*)
|
||||||
|
|
||||||
|
val post :
|
||||||
|
?tries:int ->
|
||||||
|
?client:t ->
|
||||||
|
?config:Config.t ->
|
||||||
|
?headers:(string * string) list ->
|
||||||
|
?content:[ `String of string | `Write of bytes -> int -> int ] ->
|
||||||
|
params:Curl.curlHTTPPost list ->
|
||||||
|
url:string ->
|
||||||
|
unit ->
|
||||||
|
(string response, Curl.curlCode * string) result io
|
||||||
|
(** Shortcut for [http ~meth:(POST params)]
|
||||||
|
See {!http} for more info.
|
||||||
|
*)
|
||||||
|
end
|
||||||
|
|
||||||
|
module Make (IO : IO) : S with type 'a io = 'a IO.t
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
|
|
||||||
include Ezcurl_core
|
|
||||||
|
|
||||||
include Make(struct
|
|
||||||
include Lwt
|
|
||||||
let perform = Curl_lwt.perform
|
|
||||||
end)
|
|
||||||
7
src/lwt/ezcurl_lwt.ml
Normal file
7
src/lwt/ezcurl_lwt.ml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
include Ezcurl_core
|
||||||
|
|
||||||
|
include Make (struct
|
||||||
|
include Lwt
|
||||||
|
|
||||||
|
let perform = Curl_lwt.perform
|
||||||
|
end)
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
|
|
||||||
(** {1 Synchronous API} *)
|
|
||||||
|
|
||||||
include Ezcurl_core
|
|
||||||
|
|
||||||
include Ezcurl_core.Make(struct
|
|
||||||
type 'a t = 'a
|
|
||||||
let return x = x
|
|
||||||
let (>>=) x f = f x
|
|
||||||
let (>|=) x f = f x
|
|
||||||
let fail e = raise e
|
|
||||||
let perform c =
|
|
||||||
try Curl.perform c; Curl.CURLE_OK
|
|
||||||
with Curl.CurlException (c, _, _) -> c
|
|
||||||
end)
|
|
||||||
|
|
||||||
19
src/sync/ezcurl.ml
Normal file
19
src/sync/ezcurl.ml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
(** {1 Synchronous API} *)
|
||||||
|
|
||||||
|
include Ezcurl_core
|
||||||
|
|
||||||
|
include Ezcurl_core.Make (struct
|
||||||
|
type 'a t = 'a
|
||||||
|
|
||||||
|
let return x = x
|
||||||
|
let ( >>= ) x f = f x
|
||||||
|
let ( >|= ) x f = f x
|
||||||
|
let fail e = raise e
|
||||||
|
|
||||||
|
let perform c =
|
||||||
|
try
|
||||||
|
Curl.perform c;
|
||||||
|
Curl.CURLE_OK
|
||||||
|
with Curl.CurlException (c, _, _) -> c
|
||||||
|
end)
|
||||||
|
|
||||||
39
test/basic_test.expected
Normal file
39
test/basic_test.expected
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
get: OK
|
||||||
|
body=```
|
||||||
|
version = 0.26.2
|
||||||
|
profile=conventional
|
||||||
|
margin=80
|
||||||
|
if-then-else=k-r
|
||||||
|
parens-ite=true
|
||||||
|
parens-tuple=multi-line-only
|
||||||
|
sequence-style=terminator
|
||||||
|
type-decl=sparse
|
||||||
|
break-cases=toplevel
|
||||||
|
cases-exp-indent=2
|
||||||
|
field-space=tight-decl
|
||||||
|
leading-nested-match-parens=true
|
||||||
|
module-item-spacing=compact
|
||||||
|
quiet=true
|
||||||
|
ocaml-version=4.08.0
|
||||||
|
|
||||||
|
```
|
||||||
|
streaming get: OK
|
||||||
|
body=```
|
||||||
|
version = 0.26.2
|
||||||
|
profile=conventional
|
||||||
|
margin=80
|
||||||
|
if-then-else=k-r
|
||||||
|
parens-ite=true
|
||||||
|
parens-tuple=multi-line-only
|
||||||
|
sequence-style=terminator
|
||||||
|
type-decl=sparse
|
||||||
|
break-cases=toplevel
|
||||||
|
cases-exp-indent=2
|
||||||
|
field-space=tight-decl
|
||||||
|
leading-nested-match-parens=true
|
||||||
|
module-item-spacing=compact
|
||||||
|
quiet=true
|
||||||
|
ocaml-version=4.08.0
|
||||||
|
|
||||||
|
```
|
||||||
|
same buf? true
|
||||||
30
test/basic_test.ml
Normal file
30
test/basic_test.ml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
let body = ref ""
|
||||||
|
|
||||||
|
let url =
|
||||||
|
"https://raw.githubusercontent.com/c-cube/ezcurl/refs/heads/main/.ocamlformat"
|
||||||
|
|
||||||
|
let () =
|
||||||
|
match Ezcurl.get ~url () with
|
||||||
|
| Error (code, msg) ->
|
||||||
|
Format.eprintf "curl error: code `%s` (%s)@." (Curl.strerror code) msg
|
||||||
|
| Ok res ->
|
||||||
|
body := res.body;
|
||||||
|
Format.printf "get: OK@.body=```@.%s@.```@." !body
|
||||||
|
|
||||||
|
let () =
|
||||||
|
let buf = Buffer.create 32 in
|
||||||
|
match
|
||||||
|
Ezcurl.http_stream ~meth:GET ~url
|
||||||
|
~write_into:
|
||||||
|
(object
|
||||||
|
method on_input bs i len = Buffer.add_subbytes buf bs i len
|
||||||
|
method on_close () = ()
|
||||||
|
end)
|
||||||
|
()
|
||||||
|
with
|
||||||
|
| Error (code, msg) ->
|
||||||
|
Format.eprintf "curl error: code `%s` (%s)@." (Curl.strerror code) msg
|
||||||
|
| Ok _res ->
|
||||||
|
let new_body = Buffer.contents buf in
|
||||||
|
Format.printf "streaming get: OK@.body=```@.%s@.```@." new_body;
|
||||||
|
Format.printf "same buf? %b@." (new_body = !body)
|
||||||
3
test/dune
Normal file
3
test/dune
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
(test
|
||||||
|
(name basic_test)
|
||||||
|
(libraries ezcurl))
|
||||||
Loading…
Add table
Reference in a new issue