package hyper

  1. Overview
  2. Docs

Source file connect.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
(* This file is part of Hyper, released under the MIT license. See LICENSE.md
   for details, or visit https://github.com/aantron/hyper.

   Copyright 2022 Anton Bachin *)



module Httpaf_lwt_unix = Dream_httpaf__lwt_unix.Httpaf_lwt_unix
module H2_lwt_unix = Dream_h2_lwt_unix.H2_lwt_unix

module Message = Dream_pure.Message
(* module Stream = Dream_pure.Stream *)

type response = Message.response
type request = Message.request



(* TODO Discard HTTP/1 connections on which there has been an error. *)
(* TODO In HTTP/1, treat early close by either side as an error. *)
(* TODO It should be possible to reuse WebSocket connections that returned
   redirects, until they actually return an upgrade. *)
(* TODO Test DNS failures. *)
(* TODO Test connection failures. *)

(* TODO Is this the right representation? *)
type connection =
  | Cleartext of Httpaf_lwt_unix.Client.t (* TODO Rename constructor. *)
  | SSL of Httpaf_lwt_unix.Client.SSL.t
  | H2 of H2_lwt_unix.Client.SSL.t (* TODO No h2c support. *)
  | WebSocket of Lwt_unix.file_descr
  (* | WebSocket of Stream.stream *)
    (* TODO NOTE WebSocket connections over HTTP/1.1 are currently
       single-use. We still go through the pool so as to give it the chance to
       refuse the connection based on the number of other connections to the
       same endpoint or host. The actual closing of WebSocket connections by the
       pool is not yet implemented, so it might try to multiplex them. *)
    (* TODO WebSockets over https and WebSockets over HTTP/2. *)

type 'a promise = 'a Dream_pure.Message.promise

(* let close = function
  | Cleartext connection -> Httpaf_lwt_unix.Client.shutdown connection
  | SSL connection -> Httpaf_lwt_unix.Client.SSL.shutdown connection
  | H2 connection -> H2_lwt_unix.Client.SSL.shutdown connection
  | WebSocket connection -> Lwt_unix.close connection *)

(* let concurrency = function
  | Cleartext _ -> `Pipeline
  | SSL _ -> `Pipeline
  | H2 _ -> `Multiplex *)

(* TODO How to really do DNS? Especially with IPv6 and QUIC (UDP). *)
let resolve target =
  let uri = Uri.of_string target in
  let host = Uri.host uri |> Option.get (* TODO Questonable. *)
  and port =
    match Uri.port uri with
    | Some port -> port
    | None ->
      match Uri.scheme uri with
      | Some ("https" | "wss") -> 443
      | _ -> 80
  in
  let%lwt addresses =
    Lwt_unix.getaddrinfo host (string_of_int port) [Unix.(AI_FAMILY PF_INET)] in
  let address = (List.hd addresses).Unix.ai_addr in
  Lwt.return address

(* TODO Network error handling. *)
let http1_cleartext_tcp target =
  let socket = Lwt_unix.(socket PF_INET SOCK_STREAM 0) in
  let%lwt address = resolve target in
  let%lwt () = Lwt_unix.connect socket address in
  let%lwt connection = Httpaf_lwt_unix.Client.create_connection socket in
  Lwt.return (Cleartext connection)

let alpn_https_tcp ?(protocols = ["h2"; "http/1.1"]) target =
  (* TODO The context needs to be created once per process, or a cache
     should be used. *)
  let context = Ssl.(create_context TLSv1_2 Client_context) in
  (* TODO For WebSockets (wss://), the client should probably do SSL
      without offering h2 by ALPN. Do any servers implement WebSockets over
      HTTP/2? *)
  Ssl.set_context_alpn_protos context protocols;

  let socket = Lwt_unix.(socket PF_INET SOCK_STREAM 0) in
  let%lwt address = resolve target in
  let%lwt () = Lwt_unix.connect socket address in
  let%lwt ssl_socket = Lwt_ssl.ssl_connect socket context in
  (* TODO Next line is pretty suspicious. *)
  let underlying = Lwt_ssl.ssl_socket ssl_socket |> Option.get in
  begin match Ssl.get_negotiated_alpn_protocol underlying with
  | Some "h2" ->
    (* TODO What about the error handler? *)
    let%lwt connection =
      H2_lwt_unix.Client.SSL.create_connection
        ~error_handler:ignore
        ssl_socket
    in
    Lwt.return (H2 connection)
  | _ -> (* TODO Match http/1.1 or None, while Some _ should be an error. *)
    let%lwt connection =
      Httpaf_lwt_unix.Client.SSL.create_connection ssl_socket in
    Lwt.return (SSL connection)
  end
  (* TODO Need to do server certificate validation here, etc. *)

(* TODO wss *)
let ws_cleartext_tcp target =
  (* TODO Need to return a different response depending on error conditions.
     websocket/af provides the server's response. *)
  (* let hyper_response =
    Message.response ~status:`OK *)

  let socket = Lwt_unix.(socket PF_INET SOCK_STREAM 0) in
  let%lwt address = resolve target in
  let%lwt () = Lwt_unix.connect socket address in

  Lwt.return (WebSocket socket)

let choose request =
  let scheme =
    Message.target request
    |> Uri.of_string
    |> Uri.scheme
    |> Option.get (* TODO Questionable. *)
  in
  match scheme with
  | "https" -> alpn_https_tcp ?protocols:None
  | "ws" -> ws_cleartext_tcp
  | _ -> http1_cleartext_tcp

let send' connection request =
  match connection with
  | Cleartext connection -> Http1.http connection request
  | SSL connection -> Http1.https connection request
  | H2 connection -> Http2.https connection request
  | WebSocket connection -> Websocket.ws connection request

(* let send request =
  let connect = choose request in

  (* TODO Or choose a connection from the pool. The connect return value would
     have to be different here, so we can choose the pool. *)
  let%lwt connection = connect (Message.target request) in

  (* TODO Based on the kind of connection, wrap the request stream to monitor
     close. *)

  send' connection request

  (* TODO Based on the kind of connection, wrap the response or WebSocket stream
     to monitor close. *) *)

let no_pool ?transport request =
  let connect =
    match transport with
    | None -> choose request
    | Some `HTTP1 -> http1_cleartext_tcp
    | Some `HTTPS -> alpn_https_tcp ~protocols:["http/1.1"]
    | Some `HTTP2 -> alpn_https_tcp ~protocols:["h2"]
    | Some `WS -> ws_cleartext_tcp
  in
  let%lwt connection = connect (Message.target request) in
  send' connection request
  (* TODO Have to monitor the closing of the response stream, and close the
     connection when that occurs. *)
OCaml

Innovation. Community. Security.