package awsm-lwt

  1. Overview
  2. Docs

Source file http.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
open! Import
open Lwt.Infix

module Io = struct
  include Awsm.Http.Monad.Make (struct
    type +'a t = 'a Lwt.t
  end)

  let monad =
    { Awsm.Http.Monad.bind =
        (fun x f ->
          let open Lwt.Infix in
          inj (prj x >>= fun x -> prj (f x)))
    ; return = (fun x -> inj (Lwt.return x))
    }
  ;;

  let make_stream stream () = inj (Lwt_stream.get stream)

  module Call : sig
    val cohttp_lwt
      :  ?endpoint_url:string
      -> cfg:Awsm.Cfg.t
      -> service:Awsm.Service.t
      -> Awsm.Http.Meth.t
      -> Awsm.Http.Request.t
      -> Uri.t
      -> (t Awsm.Http.Response.t, Awsm.Http.Io.Error.call) result Lwt.t
  end = struct
    let find_xml_redirect_endpoint xml =
      let get x = Awsm.Xml.child_exn xml x |> Awsm.Xml.string_data_exn in
      let code = get "Code" in
      assert (String.equal "PermanentRedirect" code);
      get "Endpoint"
    ;;

    let set_host_headers headers ~host = Cohttp.Header.replace headers "host" host

    let set_host request ~host =
      { request with
        Cohttp.Request.headers =
          request |> Cohttp.Request.headers |> set_host_headers ~host
      }
    ;;

    let rec interpret_response ~limit req_body request (resp, body)
      : (Cohttp.Response.t * Cohttp.Body.t, Awsm.Http.Io.Error.call) result s
      =
      if limit >= 50
      then Lwt.return (Error `Too_many_redirects)
      else (
        match Cohttp.Response.status resp with
        | #Cohttp.Code.success_status -> Lwt.return (Ok (resp, body))
        | #Cohttp.Code.redirection_status ->
          Cohttp.Body.to_string body
          >>= fun body ->
          let xml = Awsm.Xml.parse_response body in
          let host = find_xml_redirect_endpoint xml in
          let new_request = set_host request ~host in
          Cohttp.Client.call
            ~chunked:false
            ~headers:(Cohttp.Request.headers new_request)
            ~body:req_body
            (Cohttp.Request.meth new_request)
            (Cohttp.Request.uri new_request)
          >>= interpret_response ~limit:(succ limit) req_body new_request
        | code ->
          Cohttp.Body.to_string body
          >>= fun body ->
          let x_amzn_error_type =
            let headers = Cohttp.Response.headers resp in
            match Cohttp.Header.get headers "x-amzn-ErrorType" with
            | None -> None
            | Some value -> (
              match String.lsplit2 value ~on:':' with
              | None -> Some value
              | Some (v, _) -> Some v)
          in
          let bad_response =
            { Awsm.Http.Io.Error.code = Cohttp.Code.code_of_status code
            ; body
            ; x_amzn_error_type
            }
          in
          Lwt.return (Error (`Bad_response bad_response)))
    ;;

    let interpret_response = interpret_response ~limit:0

    (** Wrapper around [Cohttp.Client.request] that always uses https.

    @see <https://github.com/mirage/ocaml-cohttp/issues/670> *)
    let cohttp_lwt_client_request request req_body =
      (* 2022-10-24 mbac: the Async version uses [Cohttp.Client.request request], which doesn't
         exist in Lwt version. So we're trying [Cohttp.Client.call] instead. *)
      Cohttp.Client.call
        ~chunked:false
        ~headers:(Cohttp.Request.headers request)
        ~body:(Cohttp.Body.of_string req_body)
        (Cohttp.Request.meth request)
        (Uri.with_scheme (Cohttp.Request.uri request) (Some "https"))
    ;;

    let request_and_follow request req_body =
      cohttp_lwt_client_request request req_body
      >>= interpret_response (Cohttp.Body.of_string req_body) request
    ;;

    let stream_of_body = function
      | `Empty -> fun () -> monad.return None
      | `String x -> fun () -> monad.return (Some x)
      | `Strings l -> fun () -> monad.return (Some (String.concat ~sep:"" l))
      | `Stream s -> make_stream s
    ;;

    let cohttp_lwt ?endpoint_url ~cfg ~service meth request uri =
      let { Awsm.Cfg.region
          ; aws_access_key_id
          ; aws_secret_access_key
          ; aws_session_token
          ; _
          }
        =
        cfg
      in
      let region = Option.value_exn region ~message:"config must set 'region'" in
      let meth = Cohttp.to_meth meth in
      let endpoint =
        match endpoint_url with
        | Some endpoint_url -> Uri.of_string endpoint_url
        | None -> Awsm.Botocore_endpoints.lookup_uri ~region service `HTTPS
      in
      let uri =
        Uri.with_uri ~scheme:(Uri.scheme endpoint) ~host:(Uri.host endpoint) uri
      in
      let host =
        Core.Option.value_exn
          (Uri.host endpoint)
          ~message:
            (sprintf "could not extract 'host' from url %s" (Uri.to_string endpoint))
      in
      let headers =
        let headers = Cohttp.to_headers request in
        Cohttp.Header.add headers "host" host
      in
      let req_body = Awsm.Http.Request.body request in
      let body_length = Int64.of_int (String.length req_body) in
      let payload_hash = Awsm.Auth.payload_hash req_body in
      (*
      eprintf "request: %s\n" (uri |> Uri.to_string);
      eprintf "headers:\n";
      List.iter (headers |> Cohttp.Header.to_list) ~f:(fun (k, v) ->
        eprintf " %s=%s\n" k v);
      eprintf "body: %s\n" req_body;
      eprintf "meth: %s\n" (Cohttp.Code.string_of_method meth);
      eprintf "\n%!";
      *)
      let request =
        Cohttp.Request.make_for_client ~headers ~chunked:false ~body_length meth uri
        |> Awsm.Auth.sign_request
             ~region
             ~service
             ?session_token:aws_session_token
             ?aws_access_key_id
             ?aws_secret_access_key
             ~payload_hash
      in
      request_and_follow request req_body
      >>= function
      | Error _ as err -> Lwt.return err
      | Ok (resp, body) ->
        let version = Cohttp.of_version resp in
        let headers = Cohttp.of_headers resp in
        let status = Cohttp.of_status resp in
        let body = stream_of_body body in
        Lwt.return (Ok (Awsm.Http.Response.make ~version ~headers ~body status))
    ;;
  end

  let make_http http meth request uri = inj (http meth request uri)

  let call ?endpoint_url ~cfg ~service meth request uri =
    make_http (Call.cohttp_lwt ?endpoint_url ~cfg ~service) meth request uri
  ;;
end