File: netmech_spnego_http.ml

package info (click to toggle)
ocamlnet 4.1.2-1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 51,764 kB
  • ctags: 16,446
  • sloc: ml: 148,419; ansic: 10,989; sh: 1,885; makefile: 1,355
file content (459 lines) | stat: -rw-r--r-- 14,666 bytes parent folder | download | duplicates (2)
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
(* $Id$ *)

(* see RFC 4559 for SPNEGO for HTTP *)

(* SPNEGO authenticates the whole TCP connection, and not the individual
   request. Our design how to handle this:

    - There is a new transport_layer_id: spnego_trans_id. This implies
      SPNEGO with "default configuration" and https.
    - If we get a www-authenticate: negotiate header from a server,
      and the current connection isn't for spnego_trans_id, we return
      a special code so that the request is re-routed to a new connection
      which is then bound to spnego_trans_id.
    - Type changes:
       * client_match may return a new tag for re-routing to a different
         trans_id: `Reroute.
    - Changes in Nethttp_client:
       * pass trans_id as part of client_match call
       * what to do for `Reroute: we always follow this. Check whether
         a connection with the new trans_id exists. If yes, use it. If not,
         create a new one.

         Trace for `Reroute:
         - new case for auth_state: `Resubmit of trans_id
         - generic_auth_session_for_challenge: test for `Reroute condition
           in initializer. If so, we still create the session, but 
           [authenticate] immediately returns `Reroute.
           If the user has set the trans_id we MUST reject this auth method.
         - postprocess_complete_message_e: check whether 
           content_response=`Reroute. set_auth_state `Resubmit.
         - From that point on, the request is handled as a special
           redirect
         - add_with_callback: check whether auth_state=`Resubmit. 
           In this case, set the trans_id of the request and re-add

 *)

(* Re-auth:

   Better: SPNEGO indicates `Accept on client_match, and reroutes the second
   request

   New auth_status:
     - `Continue_reroute. Returned for the first request.

   client_domain: return [ "/" ]

 *)


open Printf

let spnego_oid = [| 1;3;6;1;5;5;2 |]

let spnego_trans_id = Nethttp.spnego_trans_id

let map_opt f =
  function
  | None -> None
  | Some x -> Some(f x)


module type PROFILE =
  sig
    val acceptable_transports_http : Nethttp.transport_layer_id list
    val acceptable_transports_https : Nethttp.transport_layer_id list
    val enable_delegation : bool
    val deleg_credential : exn option

    (* future: configure SPNEGO *)

  end


module Default : PROFILE =
  struct
    let acceptable_transports_http = [ ]
    let acceptable_transports_https = [ spnego_trans_id ]
    let enable_delegation = true
    let deleg_credential = None
  end


module SPNEGO(P:PROFILE)(G:Netsys_gssapi.GSSAPI) : Nethttp.HTTP_CLIENT_MECHANISM = 
  struct
    module M = Netgssapi_auth.Manage(G)
    module C = struct
      let raise_error = failwith
    end
    module A = Netgssapi_auth.Auth(G)(C)

    let mechanism_name = "Negotiate"

    let available() = true
      (* FIXME: check whether spnego supported *)                       
 
    let restart_supported = true
                              
    type credentials = unit
                         
    let init_credentials _ = ()

    let realm = "SPNEGO"

    let client_match ~params (challenge : Nethttp.Header.auth_challenge) =
      let param name =
        let (_, v, _) =
          List.find (fun (n, _, _) -> n = name) params in
        v in
      try
        let (ch_name, ch_params) = challenge in
        if String.lowercase ch_name <> "negotiate" then raise Not_found;
        let trans_id = int_of_string (param "trans_id") in
        let https = bool_of_string (param "https") in
        let acceptable_transports =
          if https then
            P.acceptable_transports_https
          else
            P.acceptable_transports_http in
        let is_acceptable_trans =
          List.mem trans_id acceptable_transports in
        if is_acceptable_trans then
          `Accept(realm, None)
        else
          match acceptable_transports with
            | [] -> raise Not_found
            | pref_id :: _ -> 
                 `Accept_reroute(realm, None, pref_id)
      with
        | Not_found -> `Reject

    type client_sub_state =
        [ `Pre_init_context | `Init_context | `Established | `Restart
        ]

    type client_session =
        { mutable ccontext : G.context option;
          cstate : Netsys_sasl_types.client_state;
          csubstate : client_sub_state;
          ctoken : string;
          cconn : int;
          cconf : Netsys_gssapi.client_config;
          ctarget_name : G.name;
          ccred : G.credential;
          cprops : Netsys_gssapi.client_props option;
        }

    let client_state cs = cs.cstate

    let client_del_ctx cs =
      match cs.ccontext with
        | None -> cs
        | Some ctx ->
            M.delete_context cs.ccontext ();
            { cs with ccontext = None }

    let cvalidity cs0 =
      let cs1 = {cs0 with ccontext = cs0.ccontext} in
      cs0.ccontext <- None;
      cs1
                 

    let check_gssapi_status fn_name 
                            ((calling_error,routine_error,_) as major_status)
                            minor_status =
      if calling_error <> `None || routine_error <> `None then (
        let msg =
          M.format_status ~fn:fn_name ~minor_status major_status in
        failwith msg
      )

    let client_check_gssapi_status cs fn_name major_status minor_status =
      try
        check_gssapi_status fn_name major_status minor_status
      with
        | error ->
            ignore(client_del_ctx cs);
            raise error
           

    let call_init_sec_context cs input_token =
      let (out_context, out_token, ret_flags, props_opt) =
        A.init_sec_context
          ~initiator_cred:cs.ccred
          ~context:cs.ccontext
          ~target_name:cs.ctarget_name
          ~req_flags:(A.get_client_flags cs.cconf)
          ~chan_bindings:None
          ~input_token
          cs.cconf in
      let cs =
        { cs with
          ccontext = Some out_context;
          ctoken = out_token;
          cprops = props_opt;
        } in
      let auth_done = (props_opt <> None) in
      if auth_done then (
        let cs =
          { cs with
            cstate = if out_token = "" then `OK else `Emit;
            csubstate = `Established;
          } in
        client_del_ctx cs;  (* no longer needed *)
      )
      else
        { cs with
          cstate = `Emit;
          csubstate = `Init_context
        }

    let create_client_session ~user ~creds ~params () =
      let params = 
        Netsys_sasl_util.preprocess_params
          "Netmech_krb5_sasl.create_client_session:"
          [ "realm"; "id"; "target-host"; "trans_id"; "conn_id"; "https" ]
          params in
      let conn_id =
        try int_of_string (List.assoc "conn_id" params)
        with Not_found -> failwith "missing parameter: conn_id" in

      let acceptor_name =
        try
          "HTTP@" ^ List.assoc "target-host" params
        with
          | Not_found -> failwith "missing parameter 'target-host'" in
      let acceptor_name_type =
        Netsys_gssapi.nt_hostbased_service in
      let cconf =
        Netsys_gssapi.create_client_config
          ~mech_type:spnego_oid
          ~target_name:(acceptor_name, acceptor_name_type)
          ~privacy:`If_possible
          ~integrity:`Required
          ~flags:( [ `Mutual_flag, `Required ] @
                     ( if P.enable_delegation then [`Deleg_flag, `Required] 
                       else [] ) )
          () in
      let ctarget_name =
        A.get_target_name cconf in
      let ccred =
        match P.deleg_credential with
          | Some (G.Credential c) -> c
          | _ -> G.interface # no_credential in
      let cstate = `Wait (* HTTP auth is always "server-first" *) in
      let cs =
        { ccontext = None;
          cstate;
          csubstate = `Pre_init_context;
          ctoken = "";
          ctarget_name;
          cconf;
          cconn = conn_id;
          ccred;
          cprops = None;
        } in
      cs

    let client_configure_channel_binding cs cb =
      if cb <> `None then
        failwith "Netmech_spnego_http.client_configure_channel_binding: \
                  not supported"
      else
        cs
                 
    let client_state cs = cs.cstate
    let client_channel_binding cs = `None

    let client_restart ~params cs =
      (* There is actually no restart protocol. As we authenticate the TCP
         connection, we just claim we can restart.
       *)
      if cs.cstate <> `OK then
        failwith "Netmech_spnego_http.client_restart: unfinished auth";
      let cs = cvalidity cs in
      let params = 
        Netsys_sasl_util.preprocess_params
          "Netmech_krb5_sasl.create_client_session:"
          [ "realm"; "id"; "target-host"; "trans_id"; "conn_id"; "https" ]
          params in
      let conn_id =
        try int_of_string (List.assoc "conn_id" params)
        with Not_found -> failwith "missing parameter: conn_id" in
      let cs =
        { cs with
          ccontext = None;
          cstate = `Emit;
          ctoken = "";
          csubstate = `Pre_init_context;
          cconn = conn_id;
        } in
      call_init_sec_context cs None
        

    let client_context cs =
      match cs.ccontext with
        | None -> failwith "client_context"
        | Some c -> c


    let client_process_challenge cs meth uri hdr challenge =
      let cs = cvalidity cs in
      try
        if cs.cstate <> `Wait then
          failwith "protocol error";
        let (ch_name, ch_params) = challenge in
        if String.lowercase ch_name <> "negotiate" then
          failwith "bad auth scheme";
        let msg =
          match ch_params with
            | [ "credentials", `V msg ] -> msg
            | [] -> ""
            | _ -> failwith "bad www-authenticate header" in
        let msg =
          try
            Netencoding.Base64.decode msg
          with 
            | Invalid_argument _ -> failwith "Base64 error" in
        match cs.csubstate with
          | `Pre_init_context ->
               if msg <> "" then failwith "unexpected token";
               call_init_sec_context cs None  (* sets cs.cstate to `Emit *)
          | `Init_context ->
               call_init_sec_context cs (Some msg)
          | `Restart ->
               (* THIS PATH IS CURRENTLY NOT TAKEN: on restart, we directly
                  enter `Pre_init_context state, and generate the token
                *)
               (* As SPNEGO authenticates the connection and not the message,
                  we are done when
                  the server responds with a non-401 message, and there is
                  no www-authenticate (here: ch_params=[]). Otherwise,
                  handle it like `Pre_init_context, and re-run the protocol.
                *)
               if ch_params = [] then (
                 { cs with
                   cstate = `OK;
                   csubstate = `Established
                 }
               ) else
                 call_init_sec_context cs None  (* sets cs.cstate to `Emit *)
          | `Established ->
               failwith "unexpected token"
      with
        | Failure msg ->
             let cs = client_del_ctx cs in
             { cs with cstate = `Auth_error msg }

    let client_emit_response cs meth uri hdr =
      if cs.cstate <> `Emit then
        failwith "Netmech_spnego_http.client_emit_response: bad state";
      let cs =
        match cs.csubstate with
          | `Pre_init_context ->
               assert false
          | `Established ->
              let cs = client_del_ctx cs in
              { cs with cstate = `OK }
          | _ ->
              { cs with cstate = `Wait } in
      let b64 = Netencoding.Base64.encode cs.ctoken in
      let creds =
        ( "Negotiate", 
          if cs.ctoken="" then [] else [ "credentials", `Q b64 ] ) in
      (* NB. The case creds=(something,[]) is special-cased in the http client,
         so that no auth header is added at all
       *)
      (cs, creds, [])

    let client_session_id cs =
      None
      
    let client_prop cs key =
      raise Not_found

    let client_gssapi_props cs =
      match cs.cprops with
        | None -> raise Not_found
        | Some p -> p

    let client_user_name cs =
      ""

    let client_authz_name cs =
      ""

    let client_stash_session cs =
      (* GSSAPI does not support that unfinished contexts are exported.
         We do not need the context anyway after session establishment,
         so we don't save it at all.
       *)
      if cs.cstate <> `OK then
        failwith "Netmech_spnego_http.client_stash_session: the session \
                  must be established (implementation restriction)";
      "client,t=SPNEGO;" ^
        Marshal.to_string
          (map_opt Netsys_gssapi.marshal_client_props cs.cprops)
          []

    let cs_re = 
      Netstring_str.regexp "client,t=SPNEGO;"

    let client_resume_session s =
      match Netstring_str.string_match cs_re s 0 with
        | None ->
            failwith "Netmech_spnego_http.client_resume_session"
        | Some m ->
            let p = Netstring_str.match_end m in
            let data = String.sub s p (String.length s - p) in
            let (mprops) = Marshal.from_string data 0 in
            { ccontext = None;
              cstate = `OK;
              csubstate = `Established;
              ctoken = "";
              ctarget_name = G.interface # no_name;
              cconn = 0;  (* FIXME *)
              cconf = Netsys_gssapi.create_client_config();
              ccred =  G.interface # no_credential;
              cprops = map_opt Netsys_gssapi.unmarshal_client_props mprops;
            }

    let client_domain s = [ "/" ]
      (* This way the auth sessions get cached *)

  end

(*
#use "topfind";;
#require "netclient,netgss-system,nettls-gnutls";;

module D = Netmech_spnego_http.Default;;
module D = 
  struct include Netmech_spnego_http.Default let enable_delegation=true end;;

module A = Netmech_spnego_http.SPNEGO(D)(Netgss.System);;

open Nethttp_client;;
Debug.enable := true;;
let keys = new key_ring ~no_invalidation:true ();;
keys # add_key (key ~user:"krb" ~password:"" ~realm:"SPNEGO" ~domain:[]);;
let a = new generic_auth_handler keys [ (module A : Nethttp.HTTP_CLIENT_MECHANISM) ];;
let p = new pipeline;;
p # add_auth_handler a;;
let c1 = new get "https://gps.dynxs.de/krb/";;
let c2 = new get "https://gps.dynxs.de/krb/index.html";;

p # add c1;;
p # add c2;;
p # run();;

p # add_with_callback c1 (fun _ -> p # add c2);;
p # run();;

c2 # set_transport_layer Nethttp_client.spnego_trans_id;;
p # add_with_callback c1 (fun _ -> p # add c2);;
p # run();;


 *)