|
| 1 | +(* NOTE: mostly copied from miou/tls_miou_unix.ml, so any change should be synchronized. *) |
| 2 | + |
| 3 | +let src = Logs.Src.create "tls-unix" |
| 4 | + |
| 5 | +module Log = (val Logs.src_log src : Logs.LOG) |
| 6 | + |
| 7 | +external reraise : exn -> 'a = "%reraise" |
| 8 | + |
| 9 | +let ( $ ) f x = f x |
| 10 | + |
| 11 | +exception Tls_alert of Tls.Packet.alert_type |
| 12 | +exception Tls_failure of Tls.Engine.failure |
| 13 | +exception Closed_by_peer |
| 14 | + |
| 15 | +let () = |
| 16 | + Printexc.register_printer @@ function |
| 17 | + | Closed_by_peer -> Some "Connection closed by peer" |
| 18 | + | Tls_alert alert -> Some (Tls.Packet.alert_type_to_string alert) |
| 19 | + | Tls_failure failure -> Some (Tls.Engine.string_of_failure failure) |
| 20 | + | _ -> None |
| 21 | + |
| 22 | +type state = |
| 23 | + [ `Active of Tls.Engine.state |
| 24 | + | `Read_closed of Tls.Engine.state |
| 25 | + | `Write_closed of Tls.Engine.state |
| 26 | + | `Closed |
| 27 | + | `Error of exn ] |
| 28 | + |
| 29 | +type t = { |
| 30 | + role : [ `Server | `Client ]; |
| 31 | + fd : Unix.file_descr; |
| 32 | + mutable state : state; |
| 33 | + mutable linger : string option; |
| 34 | + read_buffer_size : int; |
| 35 | + buf : bytes; |
| 36 | + mutable rd_closed : bool; |
| 37 | +} |
| 38 | + |
| 39 | +let file_descr { fd; _ } = fd |
| 40 | + |
| 41 | +let half_close state mode = |
| 42 | + match (state, mode) with |
| 43 | + | `Active tls, `read -> `Read_closed tls |
| 44 | + | `Active tls, `write -> `Write_closed tls |
| 45 | + | `Active _, `read_write -> `Closed |
| 46 | + | `Read_closed tls, `read -> `Read_closed tls |
| 47 | + | `Read_closed _, (`write | `read_write) -> `Closed |
| 48 | + | `Write_closed tls, `write -> `Write_closed tls |
| 49 | + | `Write_closed _, (`read | `read_write) -> `Closed |
| 50 | + | ((`Closed | `Error _) as e), (`read | `write | `read_write) -> e |
| 51 | + |
| 52 | +let inject_state tls = function |
| 53 | + | `Active _ -> `Active tls |
| 54 | + | `Read_closed _ -> `Read_closed tls |
| 55 | + | `Write_closed _ -> `Write_closed tls |
| 56 | + | (`Closed | `Error _) as e -> e |
| 57 | + |
| 58 | +let tls_alert a = Tls_alert a |
| 59 | +let tls_fail f = Tls_failure f |
| 60 | +let inhibit fn v = try fn v with _ -> () |
| 61 | + |
| 62 | +let rec unix_write fd str off len = |
| 63 | + let written = Unix.write_substring fd str off len in |
| 64 | + if not (Int.equal written len) then |
| 65 | + unix_write fd str (off + written) (len - written) |
| 66 | + |
| 67 | +let write flow str = |
| 68 | + Log.debug (fun m -> m "try to write %d byte(s)" (String.length str)); |
| 69 | + try unix_write flow.fd str 0 (String.length str) with |
| 70 | + | Unix.Unix_error ((Unix.EPIPE | Unix.ECONNRESET), _, _) -> |
| 71 | + flow.state <- half_close flow.state `write; |
| 72 | + raise Closed_by_peer |
| 73 | + | Unix.Unix_error (_, _, _) as exn -> |
| 74 | + flow.state <- `Error exn; |
| 75 | + reraise exn |
| 76 | + |
| 77 | +let handle flow tls str = |
| 78 | + match Tls.Engine.handle_tls tls str with |
| 79 | + | Ok (state, eof, `Response resp, `Data data) -> |
| 80 | + Log.debug (fun m -> m "We handled %d byte(s)" (String.length str)); |
| 81 | + let state = inject_state state flow.state in |
| 82 | + let state = Option.(value ~default:state (map (fun `Eof -> half_close state `read) eof)) in |
| 83 | + flow.state <- state; |
| 84 | + let to_close = flow.state = `Closed in |
| 85 | + Option.iter (inhibit $ write flow) resp; |
| 86 | + (* NOTE(dinosaure): [write flow] can set [flow.state]. So we must |
| 87 | + check if the actual [flow.state] or the [flow.state] after [write flow] |
| 88 | + want to close the underlying file-descriptor. *) |
| 89 | + if to_close || flow.state = `Closed then Unix.close flow.fd; |
| 90 | + data |
| 91 | + | Error (fail, `Response resp) -> |
| 92 | + let exn = match fail with |
| 93 | + | `Alert a -> tls_alert a | f -> tls_fail f in |
| 94 | + flow.state <- `Error exn; |
| 95 | + let _ = inhibit (write flow) resp in |
| 96 | + raise exn |
| 97 | + |
| 98 | +let read flow = |
| 99 | + match Unix.read flow.fd flow.buf 0 (Bytes.length flow.buf) with |
| 100 | + | 0 -> Ok String.empty |
| 101 | + | len -> Ok (Bytes.sub_string flow.buf 0 len) |
| 102 | + | exception Unix.Unix_error (Unix.ECONNRESET, _, _) -> Ok String.empty |
| 103 | + | exception exn -> Error exn |
| 104 | + |
| 105 | +let not_errored = function `Error _ -> false | _ -> true |
| 106 | + |
| 107 | +let garbage flow = match flow.linger with |
| 108 | + | Some "" | None -> false |
| 109 | + | _ -> true |
| 110 | + |
| 111 | +let read_react flow = |
| 112 | + match flow.state with |
| 113 | + | `Error exn -> raise exn |
| 114 | + | `Read_closed _ | `Closed when garbage flow -> |
| 115 | + (* XXX(dinosaure): [`Closed] can appear "at the same time" than some |
| 116 | + application-data. In that case, we stored them into [t.linger]. Depending |
| 117 | + on who closed the connection, [read_react] gives this /garbage/ in any |
| 118 | + situation (even if the user closed the connection). |
| 119 | +
|
| 120 | + An extra layer with [read] below check if [`Read_closed]/[`Close] comes |
| 121 | + from the network (the peer closed the connection) or the user. In the |
| 122 | + first case, we must give pending application-data. In the second case, |
| 123 | + we must return [0] (or raise [End_of_file]). *) |
| 124 | + let mbuf = flow.linger in |
| 125 | + flow.linger <- None; |
| 126 | + mbuf |
| 127 | + | `Read_closed _ | `Closed -> |
| 128 | + (* XXX(dinosaure): the goal of [read_react] is to read some encrypted bytes |
| 129 | + and try to decrypt them with [handle]. If the linger is empty, this means |
| 130 | + that we're trying to get more data (to decrypt) when we can't get any |
| 131 | + more. From this point of view, it's an error that needs to be notified. |
| 132 | + However, this error can be interpreted in 2 ways: |
| 133 | + - we want to have more data decrypted. In this case, this error is |
| 134 | + expected and may result in the user being told that there is nothing |
| 135 | + left to read (for example, returning 0). |
| 136 | + - we attempt a handshake. In this case, we are dealing with an unexpected |
| 137 | + error. *) |
| 138 | + raise End_of_file |
| 139 | + | `Active _ | `Write_closed _ -> |
| 140 | + Log.debug (fun m -> m "read something from the TLS session"); |
| 141 | + match read flow with |
| 142 | + | Error exn -> |
| 143 | + if not_errored flow.state then flow.state <- `Error exn; |
| 144 | + raise exn |
| 145 | + | Ok "" -> |
| 146 | + (* XXX(dinosaure): see [`Read_closed _ | `Closed] case. *) |
| 147 | + raise End_of_file |
| 148 | + | Ok str -> |
| 149 | + Log.debug (fun m -> m "got %d byte(s)" (String.length str)); |
| 150 | + match flow.state with |
| 151 | + | `Active tls | `Read_closed tls | `Write_closed tls -> handle flow tls str |
| 152 | + | `Closed -> raise End_of_file |
| 153 | + | `Error exn -> raise exn |
| 154 | +[@@ocamlformat "disable"] |
| 155 | + |
| 156 | +let rec read_in flow ?(off= 0) ?len buf = |
| 157 | + let len = Option.value ~default:(Bytes.length buf - off) len in |
| 158 | + let write_in res = |
| 159 | + let rlen = String.length res in |
| 160 | + let mlen = min len rlen in |
| 161 | + Bytes.blit_string res 0 buf off mlen; |
| 162 | + let linger = if mlen < rlen |
| 163 | + then Some (String.sub res mlen (rlen - mlen)) |
| 164 | + else None in |
| 165 | + flow.linger <- linger; mlen |
| 166 | + in |
| 167 | + match flow.linger with |
| 168 | + | Some res -> write_in res |
| 169 | + | None -> ( |
| 170 | + match read_react flow with |
| 171 | + | None -> read_in ~off ~len flow buf |
| 172 | + | Some res -> write_in res) |
| 173 | + |
| 174 | +let writev flow bufs = |
| 175 | + match flow.state with |
| 176 | + | `Closed | `Write_closed _ -> raise Closed_by_peer |
| 177 | + | `Error exn -> reraise exn |
| 178 | + | `Active tls | `Read_closed tls -> ( |
| 179 | + match Tls.Engine.send_application_data tls bufs with |
| 180 | + | Some (tls, answer) -> |
| 181 | + flow.state <- inject_state tls flow.state; |
| 182 | + write flow answer |
| 183 | + | None -> assert false) |
| 184 | + |
| 185 | +let rec drain_handshake flow = |
| 186 | + let push_linger flow mcs = |
| 187 | + match (mcs, flow.linger) with |
| 188 | + | None, _ -> () |
| 189 | + | scs, None -> flow.linger <- scs |
| 190 | + | Some cs, Some l -> flow.linger <- Some (l ^ cs) |
| 191 | + in |
| 192 | + match flow.state with |
| 193 | + | `Active tls when not (Tls.Engine.handshake_in_progress tls) -> flow |
| 194 | + | (`Read_closed _ | `Closed) when garbage flow -> flow |
| 195 | + | _ -> |
| 196 | + Log.debug (fun m -> m "start to read something from the TLS session"); |
| 197 | + let mcs = read_react flow in |
| 198 | + push_linger flow mcs; |
| 199 | + drain_handshake flow |
| 200 | + |
| 201 | +let close flow = |
| 202 | + match flow.state with |
| 203 | + | `Active tls | `Read_closed tls -> |
| 204 | + let tls, str = Tls.Engine.send_close_notify tls in |
| 205 | + flow.rd_closed <- true; |
| 206 | + flow.state <- inject_state tls flow.state; |
| 207 | + flow.state <- `Closed; |
| 208 | + inhibit (write flow) str; |
| 209 | + Unix.close flow.fd |
| 210 | + | `Write_closed _ -> |
| 211 | + flow.rd_closed <- true; |
| 212 | + flow.state <- `Closed; |
| 213 | + Unix.close flow.fd |
| 214 | + | `Closed -> flow.rd_closed <- true |
| 215 | + | `Error _ -> |
| 216 | + flow.rd_closed <- true; |
| 217 | + Unix.close flow.fd |
| 218 | + |
| 219 | +let closed_by_user flow = function |
| 220 | + | `read | `read_write -> flow.rd_closed <- true |
| 221 | + | `write -> () |
| 222 | + |
| 223 | +let shutdown flow mode = |
| 224 | + closed_by_user flow mode; |
| 225 | + match (flow.state, mode) with |
| 226 | + | `Active tls, `read -> |
| 227 | + Log.debug (fun m -> m "shutdown `read"); |
| 228 | + flow.state <- inject_state tls (half_close flow.state mode) |
| 229 | + | (`Active tls | `Read_closed tls), (`write | `read_write) -> |
| 230 | + let tls, str = Tls.Engine.send_close_notify tls in |
| 231 | + flow.state <- inject_state tls (half_close flow.state mode); |
| 232 | + (* NOTE(dinosaure): [write flow] can set [flow.state]. So we must |
| 233 | + check if the actual [flow.state] or the [flow.state] after [write flow] |
| 234 | + want to close the underlying file-descriptor. *) |
| 235 | + let to_close = flow.state = `Closed in |
| 236 | + inhibit (write flow) str; |
| 237 | + if to_close || flow.state = `Closed then Unix.close flow.fd |
| 238 | + | `Write_closed tls, (`read | `read_write) -> |
| 239 | + flow.state <- inject_state tls (half_close flow.state mode); |
| 240 | + if flow.state = `Closed then Unix.close flow.fd |
| 241 | + | `Error _, _ -> Unix.close flow.fd |
| 242 | + | `Read_closed _, `read -> () |
| 243 | + | `Write_closed _, `write -> () |
| 244 | + | `Closed, _ -> () |
| 245 | + |
| 246 | +let client_of_fd conf ?(read_buffer_size = 0x1000) ?host fd = |
| 247 | + let conf' = |
| 248 | + match host with None -> conf | Some host -> Tls.Config.peer conf host |
| 249 | + in |
| 250 | + let tls, init = Tls.Engine.client conf' in |
| 251 | + let tls_flow = |
| 252 | + { |
| 253 | + role = `Client; |
| 254 | + fd; |
| 255 | + state = `Active tls; |
| 256 | + linger = None; |
| 257 | + read_buffer_size; |
| 258 | + buf = Bytes.make read_buffer_size '\000'; |
| 259 | + rd_closed = false; |
| 260 | + } |
| 261 | + in |
| 262 | + write tls_flow init; |
| 263 | + drain_handshake tls_flow |
| 264 | + |
| 265 | +let server_of_fd conf ?(read_buffer_size = 0x1000) fd = |
| 266 | + let tls = Tls.Engine.server conf in |
| 267 | + let tls_flow = |
| 268 | + { |
| 269 | + role = `Server; |
| 270 | + fd; |
| 271 | + state = `Active tls; |
| 272 | + linger = None; |
| 273 | + read_buffer_size; |
| 274 | + buf = Bytes.make read_buffer_size '\000'; |
| 275 | + rd_closed = false; |
| 276 | + } |
| 277 | + in |
| 278 | + drain_handshake tls_flow |
| 279 | + |
| 280 | +let write flow ?(off = 0) ?len str = |
| 281 | + let len = Option.value ~default:(String.length str - off) len in |
| 282 | + if off < 0 || len < 0 || off > String.length str - len |
| 283 | + then invalid_arg "Tls_unix.write"; |
| 284 | + if len > 0 then writev flow [ String.sub str off len ] |
| 285 | + |
| 286 | +let read t ?(off= 0) ?len buf = |
| 287 | + let len = Option.value ~default:(Bytes.length buf - off) len in |
| 288 | + if off < 0 || len < 0 || off > Bytes.length buf - len |
| 289 | + then invalid_arg "Tls_unix.read"; |
| 290 | + if t.rd_closed then 0 |
| 291 | + else try read_in t ~off ~len buf with End_of_file -> 0 |
| 292 | + |
| 293 | +let rec really_read_go t off len buf = |
| 294 | + let len' = read t buf ~off ~len in |
| 295 | + if len' == 0 then raise End_of_file |
| 296 | + else if len - len' > 0 |
| 297 | + then really_read_go t (off + len') (len - len') buf |
| 298 | + |
| 299 | +let really_read t ?(off= 0) ?len buf = |
| 300 | + let len = Option.value ~default:(Bytes.length buf - off) len in |
| 301 | + if off < 0 || len < 0 || off > Bytes.length buf - len |
| 302 | + then invalid_arg "Tls_unix.really_read"; |
| 303 | + if len > 0 then really_read_go t off len buf |
| 304 | + |
| 305 | +let resolve host service = |
| 306 | + let tcp = Unix.getprotobyname "tcp" in |
| 307 | + match Unix.getaddrinfo host service [ AI_PROTOCOL tcp.p_proto ] with |
| 308 | + | [] -> Fmt.invalid_arg "No address for %s:%s" host service |
| 309 | + | ai :: _ -> ai.ai_addr |
| 310 | + |
| 311 | +let connect authenticator (v, port) = |
| 312 | + let conf = |
| 313 | + match Tls.Config.client ~authenticator () with |
| 314 | + | Ok config -> config |
| 315 | + | Error `Msg msg -> Fmt.invalid_arg "Configuration failure: %s" msg |
| 316 | + in |
| 317 | + let addr = resolve v (string_of_int port) in |
| 318 | + let fd = |
| 319 | + match addr with |
| 320 | + | Unix.ADDR_UNIX _ -> invalid_arg "Tls_unix.connect: Invalid UNIX socket" |
| 321 | + | Unix.ADDR_INET (inet_addr, _) -> |
| 322 | + if Unix.is_inet6_addr inet_addr then |
| 323 | + Unix.socket Unix.PF_INET6 Unix.SOCK_STREAM 0 |
| 324 | + else |
| 325 | + Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 |
| 326 | + in |
| 327 | + let host = Result.to_option Domain_name.(Result.bind (of_string v) host) in |
| 328 | + match Unix.connect fd addr with |
| 329 | + | () -> client_of_fd conf ?host fd |
| 330 | + | exception exn -> |
| 331 | + Unix.close fd; |
| 332 | + raise exn |
| 333 | + |
| 334 | +let epoch flow = match flow.state with |
| 335 | + | `Active tls | `Read_closed tls | `Write_closed tls -> |
| 336 | + ( match Tls.Engine.epoch tls with |
| 337 | + | Error () -> assert false |
| 338 | + | Ok data -> Some data ) |
| 339 | + | _ -> None |
0 commit comments