open! Core
open! Async
include Io_intf

module Tls_error = struct
  module Alert = struct
    type t = Tls.Packet.alert_type
    let sexp_of_t a =
      Sexplib.Sexp.Atom (Tls.Packet.alert_type_to_string a)
  end
  module Fail = struct
    type t = Tls.Engine.failure
    let sexp_of_t a =
      Sexplib.Sexp.Atom (Tls.Engine.string_of_failure a)
  end
  type t =
    | Tls_alert of Alert.t
    (** [Tls_alert] exception received from the other endpoint *)
    | Tls_failure of Fail.t
    (** [Tls_failure] exception while processing incoming data *)
    | Connection_closed
    | Connection_not_ready
    | Unexpected_eof
    | Unable_to_renegotiate
    | Unable_to_update_key
  [@@deriving sexp_of]
end

module Make (Fd : Fd) : S with module Fd := Fd = struct
  open Deferred.Or_error.Let_syntax

  module State = struct
    type t =
      | Active of Tls.Engine.state
      | Eof
      | Error of Tls_error.t
  end

  type t =
    { fd : Fd.t
    ; mutable state : State.t
    ; mutable linger : string option
    ; recv_buf : bytes
    }

  let tls_error = Fn.compose Deferred.Or_error.error_s Tls_error.sexp_of_t

  let rec read_react t =
    let handle tls buf =
      match Tls.Engine.handle_tls tls buf with
      | Ok (state, eof, `Response resp, `Data data) ->
        t.state
        <- (match eof with
          | None -> Active state
          | Some `Eof -> Eof);
        let%map () =
          match resp with
          | None -> return ()
          | Some resp -> Fd.write_full t.fd resp
        in
        `Ok data
      | Error (alert, `Response resp) ->
        t.state <- Error (match alert with `Alert a -> Tls_alert a | f -> Tls_failure f);
        let%bind () = Fd.write_full t.fd resp in
        read_react t
    in
    match t.state with
    | Error e -> tls_error e
    | Eof -> return `Eof
    | Active _ ->
      let%bind n = Fd.read t.fd t.recv_buf in
      (match t.state, n with
       | Active _, `Eof ->
         t.state <- Eof;
         return `Eof
       | Active tls, `Ok n -> handle tls (Stdlib.Bytes.sub_string t.recv_buf 0 n)
       | Error e, _ -> tls_error e
       | Eof, _ -> return `Eof)
  ;;

  let rec read t buf =
    let writeout res =
      let rlen = String.length res in
      let n = min (Bytes.length buf) rlen in
      Stdlib.Bytes.blit_string res 0 buf 0 n;
      t.linger <- (if n < rlen then Some (Stdlib.String.sub res n (rlen - n)) else None);
      return n
    in
    match t.linger with
    | Some res -> writeout res
    | None ->
      (match%bind read_react t with
       | `Eof -> return 0
       | `Ok None -> read t buf
       | `Ok (Some res) -> writeout res)
  ;;

  let writev t css =
    match t.state with
    | Error err -> tls_error err
    | Eof -> tls_error Connection_closed
    | Active tls ->
      (match Tls.Engine.send_application_data tls css with
       | Some (tls, tlsdata) ->
         t.state <- Active tls;
         Fd.write_full t.fd tlsdata
       | None -> tls_error Connection_not_ready)
  ;;

  (*
   * XXX bad XXX
   * This is a point that should particularly be protected from concurrent r/w.
   * Doing this before a `t` is returned is safe; redoing it during rekeying is
   * not, as the API client already sees the `t` and can mistakenly interleave
   * writes while this is in progress.
   * *)
  let rec drain_handshake t =
    let push_linger t mcs =
      match mcs, t.linger with
      | None, _ -> ()
      | scs, None -> t.linger <- scs
      | Some cs, Some l -> t.linger <- Some (l ^ cs)
    in
    match t.state with
    | Active tls when not (Tls.Engine.handshake_in_progress tls) -> return t
    | _ ->
      (match%bind read_react t with
       | `Eof -> tls_error Unexpected_eof
       | `Ok cs ->
         push_linger t cs;
         drain_handshake t)
  ;;

  let reneg ?authenticator ?acceptable_cas ?cert ?(drop = true) t =
    match t.state with
    | Error err -> tls_error err
    | Eof -> tls_error Connection_closed
    | Active tls ->
      (match Tls.Engine.reneg ?authenticator ?acceptable_cas ?cert tls with
       | None -> tls_error Unable_to_renegotiate
       | Some (tls', buf) ->
         if drop then t.linger <- None;
         t.state <- Active tls';
         let%bind () = Fd.write_full t.fd buf in
         let%bind _ = drain_handshake t in
         return ())
  ;;

  let key_update ?request t =
    match t.state with
    | Error err -> tls_error err
    | Eof -> tls_error Connection_closed
    | Active tls ->
      (match Tls.Engine.key_update ?request tls with
       | Error _ -> tls_error Unable_to_update_key
       | Ok (tls', buf) ->
         t.state <- Active tls';
         Fd.write_full t.fd buf)
  ;;

  let close_tls t =
    match t.state with
    | Active tls ->
      let _, buf = Tls.Engine.send_close_notify tls in
      t.state <- Eof;
      Fd.write_full t.fd buf
    | _ -> return ()
  ;;

  let server_of_fd config fd =
    drain_handshake
      { state = Active (Tls.Engine.server config)
      ; fd
      ; linger = None
      ; recv_buf = Bytes.create 4096
      }
  ;;

  let client_of_fd config ?host fd =
    let config' =
      match host with
      | None -> config
      | Some host -> Tls.Config.peer config host
    in
    let t = { state = Eof; fd; linger = None; recv_buf = Bytes.create 4096 } in
    let tls, init = Tls.Engine.client config' in
    let t = { t with state = Active tls } in
    let%bind () = Fd.write_full t.fd init in
    drain_handshake t
  ;;

  let epoch t =
    match t.state with
    | Active tls -> (match Tls.Engine.epoch tls with
       | Ok _ as o -> o
       | Error () -> Or_error.error_string "no TLS state available yet")
    | Eof -> Or_error.error_string "TLS state is end of file"
    | Error _ -> Or_error.error_string "TLS state is error"
  ;;
end
