package colombe

  1. Overview
  2. Docs

Source file starttls.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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
module Client = struct
  let src = Logs.Src.create "starttls" ~doc:"logs starttls's events"
  let hxd_config = Hxd.O.default
  module Log = (val Logs.src_log src : Logs.LOG)

  type t =
    { q : q
    ; fiber : fiber }
  and 'a state =
    | Initialization : Tls.Engine.state -> handshake state
    | Send_handshake : Tls.Engine.state -> handshake state
    | Wait_handshake : Tls.Engine.state -> wait state
    | Send : Tls.Engine.state -> send state
    | Wait : Tls.Engine.state -> wait state
    | Close : Tls.Engine.state -> close state
    | Send_failure : Tls.Engine.failure * Tls.Engine.state -> send state
  and q = V : 'a * 'a state -> q
  and handshake = Cstruct.t
  and send = Cstruct.t (* Cstruct_cap? *)
  and wait = unit and close = unit
  and fiber = Fiber : ('s, 'error) Colombe.State.process -> fiber

  type Colombe.Rfc1869.error +=
    | Unexpected_arguments
    | Unexpected_application_data
    | Unexpected_payload
    | Unexpected_SMTP_response of { code : int; txts : string list }
    | End_of_stream

  type error = Colombe.Rfc1869.error

  let pp_error ppf = function
    | Unexpected_arguments -> Fmt.string ppf "Unexpected_arguments"
    | Unexpected_application_data -> Fmt.string ppf "Unexpected_application_data"
    | Unexpected_payload -> Fmt.string ppf "Unexpected_payload"
    | Unexpected_SMTP_response { code; txts; }->
      Fmt.pf ppf "(Unexpected_SMTP_response (@[<1>code: %d,@ txts= @[<hov>%a@]@]))"
        code Fmt.(Dump.list string) txts
    | End_of_stream -> Fmt.string ppf "End_of_stream"
    | err -> Colombe.Rfc1869.pp_error ppf err

  let ehlo t args =
    if args <> ""
    then Error Unexpected_arguments
    else Ok t

  let encode t = match t.q with
    | V (_, Initialization _) ->
      Log.app (fun m -> m "Send STARTTLS") ;
      Colombe.Rfc1869.Request { verb= "STARTTLS"; args= [] }
    | V (handshake, Send_handshake _) ->
      Log.app (fun m -> m "Send TLS handshake") ;
      let buf = Cstruct.to_bytes handshake in
      Colombe.Rfc1869.Payload { buf; off= 0; len= Bytes.length buf }
    | V (send, Send _) ->
      Log.app (fun m -> m "Send application data") ;
      let buf = Cstruct.to_bytes send in
      Colombe.Rfc1869.Payload { buf; off= 0; len= Bytes.length buf }
    | V (send, Send_failure (failure, _)) ->
      Log.err (fun m -> m "Send TLS failure (%s)" (Tls.Engine.string_of_failure failure)) ;
      let buf = Cstruct.to_bytes send in
      Colombe.Rfc1869.Payload { buf; off= 0; len= Bytes.length buf }
    | V (_, Wait_handshake _) -> assert false
    | V (_, Wait _) -> assert false
    | V (_, Close _) -> assert false

  let handle t = match t.q with
    | V (_, Wait_handshake state) ->
      assert (Tls.Engine.handshake_in_progress state = false) ;
      assert (Tls.Engine.can_handle_appdata state) ;

      (* XXX(dinosaure): should not be assertions but [if]. *)

      let Fiber fiber = t.fiber in

      ( match fiber with
        | Colombe.State.Read _ | Return _ | Error _ ->
          failwith "Inner process of STARTTLS flow MUST start with a Write operation"
        | Colombe.State.Write { buffer; off; len; k= _; } ->
          Log.app (fun m -> m "Fiber start with: @[<hov>%a@]" (Hxd_string.pp hxd_config) (String.sub buffer off len)) ;

          match Tls.Engine.send_application_data state [ Cstruct.of_string buffer ~off ~len ] with
          | Some (state, send) -> { t with q= V (send, Send state) }
          | None -> t (* XXX(dinosaure): [None] is an error? *) )
    | V (_, Send_handshake state) ->
      (* XXX(dinosaure): hmmhmm, if we look into [`q5] of [Sendmail_tls], we
         possible reach end of handshake even if we just sended TLS-data. In
         this case, [handle] should update internal state as [Wait_handshake]
         does. It's an undefined behavior. *)
      { t with q= V ((), Wait_handshake state) }
    | V (_, Send state) ->
      let Fiber fiber = t.fiber in

      let fiber = match fiber with
        | Colombe.State.Write { len; k; _ } ->
          Log.app (fun m -> m "%d byte(s) consumed on fiber" len) ;
          k len (* XXX(dinosaure): this is on top of this assumption:
                   [ocaml-tls] consumes entirely the fiber. *)
        | _ -> fiber in
      let q = match fiber with
        | Colombe.State.Read _ ->
          Log.app (fun m -> m "Fiber wants to read") ;
          V ((), Wait state)
        | Write { buffer; off; len; k= _ } ->
          Log.app (fun m -> m "Fiber wants to write: @[<hov>%a@]" (Hxd_string.pp hxd_config) (String.sub buffer off len)) ;
          ( match Tls.Engine.send_application_data state [ Cstruct.of_string buffer ~off ~len ] with
            | Some (state, send) -> V (send, Send state)
            | None -> V ((), Wait state) (* TODO! *) )
        | Return _ | Error _ ->
          Log.info (fun m -> m "Got Return or Error state from fiber.\n%!" ) ;
          (* XXX(dinosaure): any [Return] or [Error] wants to notify the server
             to close the connection. *)
          V ((), Close state) in
      { fiber= Fiber fiber; q }
    | _ -> t

  let action t = match t.q with
    | V (_, Initialization _) -> Some (Colombe.Rfc1869.Recv_code 220)
    | V (send, Send_handshake _) ->
      let buf = Cstruct.to_bytes send in
      Some Colombe.Rfc1869.(Send (Payload { buf; off= 0; len= Bytes.length buf; }))
    | V (send, Send _) ->
      let buf = Cstruct.to_bytes send in
      Some Colombe.Rfc1869.(Send (Payload { buf; off= 0; len= Bytes.length buf; }))
    | V (send, Send_failure _) ->
      let buf = Cstruct.to_bytes send in
      Some Colombe.Rfc1869.(Send (Payload { buf; off= 0; len= Bytes.length buf; }))
    | V (_, Wait_handshake _) ->
      Some Colombe.Rfc1869.Waiting_payload
    | V (_, Wait _) ->
      Some Colombe.Rfc1869.Waiting_payload
    | V (_, Close _) -> None

  let handle_handshake t ~buf ~off ~len state =
    match Tls.Engine.handle_tls state (Cstruct.of_bytes buf ~off ~len) with
    | `Ok (_, _, `Data (Some _)) -> Error Unexpected_application_data
    | `Ok (`Ok state, `Response None, _) ->
      if Tls.Engine.can_handle_appdata state && Tls.Engine.handshake_in_progress state = false
      then Ok (handle { t with q= V ((), Wait_handshake state) }) (* here, a dragoon ... *)
      else Ok { t with q= V ((), Wait_handshake state) }
    | `Ok (`Ok state, `Response (Some send), _) ->
      Ok { t with q= V (send, Send_handshake state) }
    | `Ok (`Eof, _, _) -> Error End_of_stream
    | `Ok (`Alert alert, _, _) ->
      Log.err (fun m -> m "Retrieve an alert: %s" (Tls.Packet.alert_type_to_string alert)) ;
      let state, send = Tls.Engine.send_close_notify state in
      Ok { t with q= V (send, Send state) }
    (* XXX(dinosaure): check this branch! *)
    | `Fail (failure, `Response send) ->
      Ok { t with q= V (send, Send_failure (failure, state)) }

  [@@@warning "-27"]

  let handle_tls t ~buf ~off ~len state =
    match Tls.Engine.handle_tls state (Cstruct.of_bytes buf ~off ~len) with
    | `Ok (`Ok state, `Response None, `Data (Some data)) ->
      Log.app (fun m -> m "Receive from the server: @[<hov>%a@]" (Hxd_string.pp hxd_config) (Cstruct.to_string data)) ;

      let Fiber fiber = t.fiber in

      let rec go data = function
        | Colombe.State.Read { buffer; off; len; k; } ->
          let len = min len (Cstruct.len data) in
          Cstruct.blit_to_bytes data 0 buffer off len ;
          go (Cstruct.shift data len) (k len)
        | Write { buffer; off; len; k= _; } as fiber ->
          Log.app (fun m -> m "Fiber wants to write: @[<hov>%a@]" (Hxd_string.pp hxd_config) (String.sub buffer off len)) ;

          ( match Tls.Engine.send_application_data state [ Cstruct.of_string ~off ~len buffer ] with
            | Some (state, send) ->
              Ok { fiber= Fiber fiber; q= V (send, Send state); }
            | None -> assert false )
        | Return _ as fiber ->
          Log.app (fun m -> m "Notify to close the process") ;

          let state, send = Tls.Engine.send_close_notify state in
          Ok { fiber= Fiber fiber; q= V (send, Send state) }
        | Error _ as fiber ->
          (* XXX(dinosaure): [fiber] should take care to [QUIT] properly.
             [STARTTLS] should not introspect [fiber] first, then [QUIT] it
             outside the scope of the already negociated TLS flow. *)
          Log.err (fun m -> m "Fiber returns an error, notify to close the process") ;
          let state, send = Tls.Engine.send_close_notify state in
          Ok { fiber= Fiber fiber; q= V (send, Send state) } in
      go data fiber

    | `Ok (`Ok state, `Response (Some send), `Data None) ->
      Ok { t with q= V (send, Send state) }
    | `Ok (`Ok state, `Response (Some send), `Data (Some data)) ->
      let Fiber fiber = t.fiber in

      let rec go data = function
        | Colombe.State.Read { buffer; off; len; k; } ->
          let len = min len (Cstruct.len data) in
          Cstruct.blit_to_bytes data 0 buffer off len ;
          go (Cstruct.shift data len) (k len)
        | (Write _ | Return _ | Error _) as fiber ->
          Ok { fiber= Fiber fiber; q= V (send, Send state) } in
      go data fiber

    | `Ok (`Ok state, `Response None, `Data None) ->
      Ok { t with q= V ((), Wait state) }
    | `Ok (`Eof, _, _) -> Error End_of_stream
    | `Ok (`Alert alert, _, _) ->
      Log.err (fun m -> m "Retrieve an alert: %s" (Tls.Packet.alert_type_to_string alert)) ;
      let state, send = Tls.Engine.send_close_notify state in
      Ok { t with q= V (send, Send state) }
    (* XXX(dinosaure): check this branch! *)
    | `Fail (failure, `Response send) ->
      Ok { t with q= V (send, Send_failure (failure, state)) }

  let decode resp t = match resp, t.q with
    | Colombe.Rfc1869.Response { code= 220; _ }, V (handshake, Initialization state) ->
      Ok { t with q= V (handshake, Send_handshake state) }
    | Payload { buf; off; len; }, V (_, Send_handshake state) ->
      Log.app (fun m -> m "Receive TLS handshake (client sended handshake)") ;
      handle_handshake t ~buf ~off ~len state
    | Payload { buf; off; len; }, V (_, Wait_handshake state) ->
      Log.app (fun m -> m "Receive TLS handshake (client expected handshake)") ;
      handle_handshake t ~buf ~off ~len state
    | Payload { buf; off; len; }, V (_, Send state) ->
      handle_tls t ~buf ~off ~len state
    | Payload { buf; off; len; }, V (_, Wait state) ->
      handle_tls t ~buf ~off ~len state
    | Response _, V (handshake, Initialization state) -> assert false (* server sended an other SMTP code *)
    | Response { code; txts; }, _ -> Error (Unexpected_SMTP_response { code; txts; })
    | Payload _, V (_, Initialization _) -> Error Unexpected_payload
    | Payload _, V (_, Close _) -> Error Unexpected_payload
    | Payload { buf; off; len; }, V (_, Send_failure (_, state)) ->
      Ok { t with q= V ((), Close state) } (* XXX(dinosaure): need to check! *)

  let mail_from _t _mail_from = []
  let rcpt_to _t _rcpt_to = []
end

type state = Client.t
type fiber = Client.fiber

let description : Colombe.Rfc1869.description =
  { name= "STARTTLS"
  ; elho= "STARTTLS"
  ; verb= [ "STARTTLS" ] }

let extension = Colombe.Rfc1869.inj (module Client)

module Extension = (val extension)
let inj v = Extension.T v

let fiber fiber = Client.Fiber fiber

let make fiber ?domain config =
  let config = match domain with
    | None -> config
    | Some domain -> Tls.Config.peer config (Domain_name.to_string domain) in
  let state, handshake = Tls.Engine.client config in
  { Client.q= V (handshake, Initialization state); fiber }
OCaml

Innovation. Community. Security.