web_site.ml 72.3 KB
Newer Older
Stephane Glondu's avatar
Stephane Glondu committed
1 2 3
(**************************************************************************)
(*                                BELENIOS                                *)
(*                                                                        *)
Stephane Glondu's avatar
Stephane Glondu committed
4
(*  Copyright © 2012-2018 Inria                                           *)
Stephane Glondu's avatar
Stephane Glondu committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
(*                                                                        *)
(*  This program is free software: you can redistribute it and/or modify  *)
(*  it under the terms of the GNU Affero General Public License as        *)
(*  published by the Free Software Foundation, either version 3 of the    *)
(*  License, or (at your option) any later version, with the additional   *)
(*  exemption that compiling, linking, and/or using OpenSSL is allowed.   *)
(*                                                                        *)
(*  This program is distributed in the hope that it will be useful, but   *)
(*  WITHOUT ANY WARRANTY; without even the implied warranty of            *)
(*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *)
(*  Affero General Public License for more details.                       *)
(*                                                                        *)
(*  You should have received a copy of the GNU Affero General Public      *)
(*  License along with this program.  If not, see                         *)
(*  <http://www.gnu.org/licenses/>.                                       *)
(**************************************************************************)

22
open Lwt
23
open Platform
Stephane Glondu's avatar
Stephane Glondu committed
24
open Serializable_builtin_t
25
open Serializable_j
26
open Signatures
27
open Common
28
open Web_serializable_builtin_t
Stephane Glondu's avatar
Stephane Glondu committed
29
open Web_serializable_j
30
open Web_common
31
open Web_services
Stephane Glondu's avatar
Stephane Glondu committed
32

33
let source_file = ref "belenios.tar.gz"
34
let maxmailsatonce = ref 1000
35
let uuid_length = ref None
36
let default_group = ref ""
Stephane Glondu's avatar
Stephane Glondu committed
37

38 39
let ( / ) = Filename.concat

Stephane Glondu's avatar
Stephane Glondu committed
40
module PString = String
Stephane Glondu's avatar
Stephane Glondu committed
41

Stephane Glondu's avatar
Stephane Glondu committed
42 43 44 45 46 47 48 49 50
open Eliom_service
open Eliom_registration

(* Table with elections in setup mode. *)
let election_stable = Ocsipersist.open_table "site_setup"

(* Table with tokens given to trustees. *)
let election_pktokens = Ocsipersist.open_table "site_pktokens"

51 52 53
(* Table with tokens given to trustees (in threshold mode). *)
let election_tpktokens = Ocsipersist.open_table "site_tpktokens"

Stephane Glondu's avatar
Stephane Glondu committed
54 55 56
(* Table with tokens given to trustees (in threshold mode) to decrypt *)
let election_tokens_decrypt = Ocsipersist.open_table "site_tokens_decrypt"

Stephane Glondu's avatar
Stephane Glondu committed
57 58 59 60 61
(* Table with tokens given to credential authorities. *)
let election_credtokens = Ocsipersist.open_table "site_credtokens"

module T = Web_templates

Stephane Glondu's avatar
Stephane Glondu committed
62
let raw_find_election uuid =
63
  let%lwt raw_election = Web_persist.get_raw_election uuid in
64 65
  match raw_election with
  | Some raw_election ->
Stephane Glondu's avatar
Stephane Glondu committed
66
     return (Election.of_string raw_election)
67
  | _ -> Lwt.fail Not_found
Stephane Glondu's avatar
Stephane Glondu committed
68

Stephane Glondu's avatar
Stephane Glondu committed
69
module WCacheTypes = struct
70
  type key = uuid
Stephane Glondu's avatar
Stephane Glondu committed
71
  type value = Yojson.Safe.json election
Stephane Glondu's avatar
Stephane Glondu committed
72 73 74 75 76 77 78 79
end

module WCache = Ocsigen_cache.Make (WCacheTypes)

let find_election =
  let cache = new WCache.cache raw_find_election 100 in
  fun x -> cache#find x

80
let get_setup_election uuid =
Stephane Glondu's avatar
Stephane Glondu committed
81
  let%lwt se = Ocsipersist.find election_stable (raw_string_of_uuid uuid) in
82
  return (setup_election_of_string se)
83

84
let set_setup_election uuid se =
Stephane Glondu's avatar
Stephane Glondu committed
85
  Ocsipersist.add election_stable (raw_string_of_uuid uuid) (string_of_setup_election se)
86

Stephane Glondu's avatar
Stephane Glondu committed
87
let dump_passwords dir table =
Stephane Glondu's avatar
Stephane Glondu committed
88 89 90 91 92 93
  Lwt_io.(with_file Output (dir / "passwords.csv") (fun oc ->
    Ocsipersist.iter_step (fun voter (salt, hashed) ->
      write_line oc (voter ^ "," ^ salt ^ "," ^ hashed)
    ) table
  ))

94
let finalize_election uuid se =
Stephane Glondu's avatar
Stephane Glondu committed
95
  let uuid_s = raw_string_of_uuid uuid in
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
  (* voters *)
  let () =
    if se.se_voters = [] then failwith "no voters"
  in
  (* passwords *)
  let () =
    match se.se_metadata.e_auth_config with
    | Some [{auth_system = "password"; _}] ->
       if not @@ List.for_all (fun v -> v.sv_password <> None) se.se_voters then
         failwith "some passwords are missing"
    | _ -> ()
  in
  (* credentials *)
  let () =
    if not se.se_public_creds_received then
      failwith "public credentials are missing"
  in
  (* trustees *)
  let group = Group.of_string se.se_group in
  let module G = (val group : GROUP) in
116 117 118 119 120 121 122 123 124 125 126
  let%lwt y, trustees, pk_or_tp, private_keys =
    match se.se_threshold_trustees with
    | None ->
       let module KG = Trustees.MakeSimple (G) (LwtRandom) in
       let%lwt trustees, public_keys, private_key =
         match se.se_public_keys with
         | [] ->
            let%lwt private_key = KG.generate () in
            let%lwt public_key = KG.prove private_key in
            return (None, [public_key], `KEY private_key)
         | _ :: _ ->
127 128 129 130 131 132 133 134 135 136 137 138
            let private_key =
              List.fold_left (fun accu {st_private_key; _} ->
                  match st_private_key with
                  | Some x -> x :: accu
                  | None -> accu
                ) [] se.se_public_keys
            in
            let private_key = match private_key with
              | [] -> `None
              | [x] -> `KEY x
              | _ -> failwith "multiple private keys"
            in
139 140 141 142 143 144 145
            return (
                Some (List.map (fun {st_id; _} -> st_id) se.se_public_keys),
                (List.map
                   (fun {st_public_key; _} ->
                     if st_public_key = "" then failwith "some public keys are missing";
                     trustee_public_key_of_string G.read st_public_key
                   ) se.se_public_keys),
146
                private_key)
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
       in
       let y = KG.combine (Array.of_list public_keys) in
       return (y, trustees, `PK public_keys, private_key)
    | Some ts ->
       match se.se_threshold_parameters with
       | None -> failwith "key establishment not finished"
       | Some tp ->
          let tp = threshold_parameters_of_string G.read tp in
          let module P = Trustees.MakePKI (G) (LwtRandom) in
          let module C = Trustees.MakeChannels (G) (LwtRandom) (P) in
          let module K = Trustees.MakePedersen (G) (LwtRandom) (P) (C) in
          let trustees = List.map (fun {stt_id; _} -> stt_id) ts in
          let private_keys =
            List.map (fun {stt_voutput; _} ->
                match stt_voutput with
                | Some v ->
                   let voutput = voutput_of_string G.read v in
                   voutput.vo_private_key
                | None -> failwith "inconsistent state"
              ) ts
          in
          let y = K.combine tp in
          return (y, Some trustees, `TP tp, `KEYS private_keys)
170 171
  in
  (* election parameters *)
172 173 174 175 176 177 178 179 180
  let e_server_is_trustee = match private_keys with
      | `KEY _ -> Some true
      | `None | `KEYS _ -> None
  in
  let metadata = {
      se.se_metadata with
      e_trustees = trustees;
      e_server_is_trustee;
    } in
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
  let template = se.se_questions in
  let params = {
    e_description = template.t_description;
    e_name = template.t_name;
    e_public_key = {wpk_group = G.group; wpk_y = y};
    e_questions = template.t_questions;
    e_uuid = uuid;
  } in
  let raw_election = string_of_params (write_wrapped_pubkey G.write_group G.write) params in
  (* write election files to disk *)
  let dir = !spool_dir / uuid_s in
  let create_file fname what xs =
    Lwt_io.with_file
      ~flags:(Unix.([O_WRONLY; O_NONBLOCK; O_CREAT; O_TRUNC]))
      ~perm:0o600 ~mode:Lwt_io.Output (dir / fname)
      (fun oc ->
        Lwt_list.iter_s
          (fun v ->
            Lwt_io.write oc (what v) >>
              Lwt_io.write oc "\n") xs)
  in
  Lwt_unix.mkdir dir 0o700 >>
203 204 205 206
  (match pk_or_tp with
   | `PK pk -> create_file "public_keys.jsons" (string_of_trustee_public_key G.write) pk
   | `TP tp -> create_file "threshold.json" (string_of_threshold_parameters G.write) [tp]
  ) >>
207
  create_file "voters.txt" (fun x -> x.sv_id) se.se_voters >>
208
  create_file "metadata.json" string_of_metadata [metadata] >>
209 210
  create_file "election.json" (fun x -> x) [raw_election] >>
  (* construct Web_election instance *)
211 212 213 214
  let election = Election.of_string raw_election in
  let module W = (val Election.get_group election) in
  let module E = Election.Make (W) (LwtRandom) in
  let module B = Web_election.Make (E) in
215
  (* set up authentication *)
216
  let%lwt () =
217
    match metadata.e_auth_config with
218 219 220 221 222 223 224
    | None -> return ()
    | Some xs ->
       let auth_config =
         List.map (fun {auth_system; auth_instance; auth_config} ->
           auth_instance, (auth_system, List.map snd auth_config)
         ) xs
       in
225
       Web_persist.set_auth_config (Some uuid) auth_config
226 227
  in
  (* inject credentials *)
228
  let%lwt () =
229
    let fname = !spool_dir / uuid_s ^ ".public_creds.txt" in
230 231 232 233 234 235
    match%lwt read_file fname with
    | Some xs ->
       Lwt_list.iter_s B.inject_cred xs
       >> B.update_files ()
       >> Lwt_unix.unlink fname
    | None -> return_unit
236
  in
237
  (* create file with private keys, if any *)
238
  let%lwt () =
239 240 241 242
    match private_keys with
    | `None -> return_unit
    | `KEY x -> create_file "private_key.json" string_of_number [x]
    | `KEYS x -> create_file "private_keys.jsons" (fun x -> x) x
243 244 245 246
  in
  (* clean up setup database *)
  Ocsipersist.remove election_credtokens se.se_public_creds >>
  Lwt_list.iter_s
247
    (fun {st_token; _} ->
248 249 250 251
      if st_token <> "" then (
        Ocsipersist.remove election_pktokens st_token
      ) else return_unit
    )
252
    se.se_public_keys >>
253 254 255 256 257 258 259
  (match se.se_threshold_trustees with
   | None -> return_unit
   | Some ts ->
      Lwt_list.iter_s
        (fun x -> Ocsipersist.remove election_tpktokens x.stt_token)
        ts
  ) >>
260 261
  Ocsipersist.remove election_stable uuid_s >>
  (* inject passwords *)
262
  (match metadata.e_auth_config with
263
  | Some [{auth_system = "password"; _}] ->
264
     let table = "password_" ^ underscorize uuid in
265 266 267 268 269 270 271 272
     let table = Ocsipersist.open_table table in
     Lwt_list.iter_s
       (fun v ->
         let _, login = split_identity v.sv_id in
         match v.sv_password with
         | Some x -> Ocsipersist.add table login x
         | None -> return_unit
       ) se.se_voters >>
273
       dump_passwords (!spool_dir / uuid_s) table
274 275
  | _ -> return_unit) >>
  (* finish *)
276
  Web_persist.set_election_state uuid `Open >>
277
  Web_persist.set_election_date `Finalization uuid (now ())
Stephane Glondu's avatar
Stephane Glondu committed
278

Stephane Glondu's avatar
Stephane Glondu committed
279 280 281 282
let cleanup_table ?uuid_s table =
  let table = Ocsipersist.open_table table in
  match uuid_s with
  | None ->
283
     let%lwt indexes = Ocsipersist.fold_step (fun k _ accu ->
Stephane Glondu's avatar
Stephane Glondu committed
284 285 286 287 288 289
       return (k :: accu)) table []
     in
     Lwt_list.iter_s (Ocsipersist.remove table) indexes
  | Some u -> Ocsipersist.remove table u

let cleanup_file f =
290
  try%lwt Lwt_unix.unlink f
Stephane Glondu's avatar
Stephane Glondu committed
291 292
  with _ -> return_unit

293
let archive_election uuid =
Stephane Glondu's avatar
Stephane Glondu committed
294
  let uuid_s = raw_string_of_uuid uuid in
295
  let uuid_u = underscorize uuid in
296
  let%lwt () = cleanup_table ~uuid_s "election_states" in
Stephane Glondu's avatar
Stephane Glondu committed
297
  let%lwt () = cleanup_table ~uuid_s "site_tokens_decrypt" in
298 299 300 301 302 303 304
  let%lwt () = cleanup_table ~uuid_s "election_pds" in
  let%lwt () = cleanup_table ~uuid_s "auth_configs" in
  let%lwt () = cleanup_table ("password_" ^ uuid_u) in
  let%lwt () = cleanup_table ("records_" ^ uuid_u) in
  let%lwt () = cleanup_table ("creds_" ^ uuid_u) in
  let%lwt () = cleanup_table ("ballots_" ^ uuid_u) in
  let%lwt () = cleanup_file (!spool_dir / uuid_s / "private_key.json") in
305
  let%lwt () = cleanup_file (!spool_dir / uuid_s / "private_keys.jsons") in
306
  let%lwt () = Web_persist.set_election_date `Archive uuid (now ()) in
Stephane Glondu's avatar
Stephane Glondu committed
307 308
  return_unit

Stephane Glondu's avatar
Stephane Glondu committed
309 310
let () = Any.register ~service:home
  (fun () () ->
311
    Eliom_reference.unset Web_state.cont >>
312
    Redirection.send admin
Stephane Glondu's avatar
Stephane Glondu committed
313 314
  )

315
let get_finalized_elections_by_owner u =
316
  let%lwt elections, tallied, archived =
317
    Web_persist.get_elections_by_owner u >>=
318 319 320
    Lwt_list.fold_left_s (fun accu uuid ->
        let%lwt w = find_election uuid in
        let%lwt state = Web_persist.get_election_state uuid in
321 322 323 324 325
        let%lwt date = Web_persist.get_election_date `Finalization uuid in
        let date = match date with
          | None -> default_finalization_date
          | Some x -> x
        in
Stephane Glondu's avatar
Stephane Glondu committed
326
        let elections, tallied, archived = accu in
327
        match state with
Stephane Glondu's avatar
Stephane Glondu committed
328 329 330 331
        | `Tallied _ -> return (elections, (date, w) :: tallied, archived)
        | `Archived -> return (elections, tallied, (date, w) :: archived)
        | _ -> return ((date, w) :: elections, tallied, archived)
    ) ([], [], [])
332 333 334 335 336
  in
  let sort l =
    List.sort (fun (x, _) (y, _) -> datetime_compare x y) l |>
    List.map (fun (_, x) -> x)
  in
Stephane Glondu's avatar
Stephane Glondu committed
337
  return (sort elections, sort tallied, sort archived)
338

339 340 341 342 343
let with_site_user f =
  match%lwt Web_state.get_site_user () with
  | Some u -> f u
  | None -> forbidden ()

344 345 346 347 348 349 350
let () =
  Redirection.register ~service:admin_gdpr_accept
    (fun () () ->
      Eliom_reference.set Web_state.show_cookie_disclaimer false >>
      return admin
    )

Stephane Glondu's avatar
Stephane Glondu committed
351 352
let () = Html5.register ~service:admin
  (fun () () ->
353 354
    let%lwt gdpr = Eliom_reference.get Web_state.show_cookie_disclaimer in
    if gdpr then T.admin_gdpr () else
Stephane Glondu's avatar
Stephane Glondu committed
355
    let cont () = Redirection.send admin in
356
    Eliom_reference.set Web_state.cont [cont] >>
357 358
    let%lwt site_user = Web_state.get_site_user () in
    let%lwt elections =
359
      match site_user with
360
      | None -> return None
Stephane Glondu's avatar
Stephane Glondu committed
361
      | Some u ->
362 363
         let%lwt elections, tallied, archived = get_finalized_elections_by_owner u in
         let%lwt setup_elections =
364
           Ocsipersist.fold_step (fun k v accu ->
365
             let v = setup_election_of_string v in
366
             if v.se_owner = u then
Stephane Glondu's avatar
Stephane Glondu committed
367
               return ((uuid_of_raw_string k, v.se_questions.t_name) :: accu)
368 369
             else return accu
           ) election_stable []
370
         in
Stephane Glondu's avatar
Stephane Glondu committed
371
         return @@ Some (elections, tallied, archived, setup_elections)
Stephane Glondu's avatar
Stephane Glondu committed
372
    in
373
    T.admin ~elections ()
Stephane Glondu's avatar
Stephane Glondu committed
374 375
  )

376
let () = File.register ~service:source_code
Stephane Glondu's avatar
Stephane Glondu committed
377 378 379
  ~content_type:"application/x-gzip"
  (fun () () -> return !source_file)

380 381
let generate_uuid =
  let gen = Uuidm.v4_gen (Random.State.make_self_init ()) in
382 383 384 385 386 387
  fun () ->
  match !uuid_length with
  | Some length ->
     let%lwt token = generate_token ~length () in
     return @@ uuid_of_raw_string token
  | None -> return @@ uuid_of_raw_string @@ Uuidm.to_string @@ gen ()
Stephane Glondu's avatar
Stephane Glondu committed
388

389 390
let redir_preapply s u () = Redirection.send (preapply s u)

Stephane Glondu's avatar
Stephane Glondu committed
391 392 393 394 395 396 397 398 399 400
let create_new_election owner cred auth =
  let e_cred_authority = match cred with
    | `Automatic -> Some "server"
    | `Manual -> None
  in
  let e_auth_config = match auth with
    | `Password -> Some [{auth_system = "password"; auth_instance = "password"; auth_config = []}]
    | `Dummy -> Some [{auth_system = "dummy"; auth_instance = "dummy"; auth_config = []}]
    | `CAS server -> Some [{auth_system = "cas"; auth_instance = "cas"; auth_config = ["server", server]}]
  in
401
  let%lwt uuid = generate_uuid () in
Stephane Glondu's avatar
Stephane Glondu committed
402
  let uuid_s = raw_string_of_uuid uuid in
403
  let%lwt token = generate_token () in
Stephane Glondu's avatar
Stephane Glondu committed
404 405 406 407
  let se_metadata = {
    e_owner = Some owner;
    e_auth_config;
    e_cred_authority;
408
    e_trustees = None;
409
    e_languages = Some ["en"; "fr"];
410
    e_contact = None;
411
    e_server_is_trustee = None;
Stephane Glondu's avatar
Stephane Glondu committed
412 413
  } in
  let question = {
Stephane Glondu's avatar
Stephane Glondu committed
414
    q_answers = [| "Answer 1"; "Answer 2"; "Answer 3" |];
415
    q_blank = None;
Stephane Glondu's avatar
Stephane Glondu committed
416
    q_min = 1;
Stephane Glondu's avatar
Stephane Glondu committed
417
    q_max = 2;
Stephane Glondu's avatar
Stephane Glondu committed
418 419 420 421 422 423 424 425 426
    q_question = "Question 1?";
  } in
  let se_questions = {
    t_description = "Description of the election.";
    t_name = "Name of the election";
    t_questions = [| question |];
  } in
  let se = {
    se_owner = owner;
427
    se_group = !default_group;
Stephane Glondu's avatar
Stephane Glondu committed
428 429 430 431 432
    se_voters = [];
    se_questions;
    se_public_keys = [];
    se_metadata;
    se_public_creds = token;
433
    se_public_creds_received = false;
434 435 436 437
    se_threshold = None;
    se_threshold_trustees = None;
    se_threshold_parameters = None;
    se_threshold_error = None;
438
    se_creation_date = Some (now ());
Stephane Glondu's avatar
Stephane Glondu committed
439
  } in
440
  let%lwt () = set_setup_election uuid se in
441
  let%lwt () = Ocsipersist.add election_credtokens token uuid_s in
442
  redir_preapply election_setup uuid ()
Stephane Glondu's avatar
Stephane Glondu committed
443 444 445 446

let () = Html5.register ~service:election_setup_pre
  (fun () () -> T.election_setup_pre ())

447
let () = Any.register ~service:election_setup_new
Stephane Glondu's avatar
Stephane Glondu committed
448
  (fun () (credmgmt, (auth, cas_server)) ->
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
    with_site_user (fun u ->
        let%lwt credmgmt = match credmgmt with
          | Some "auto" -> return `Automatic
          | Some "manual" -> return `Manual
          | _ -> fail_http 400
        in
        let%lwt auth = match auth with
          | Some "password" -> return `Password
          | Some "dummy" -> return `Dummy
          | Some "cas" -> return @@ `CAS cas_server
          | _ -> fail_http 400
        in
        create_new_election u credmgmt auth
      )
  )
Stephane Glondu's avatar
Stephane Glondu committed
464

465
let with_setup_election_ro uuid f =
466
  with_site_user (fun u ->
467
      let%lwt se = get_setup_election uuid in
468
      if se.se_owner = u then
469
        f se
470 471
      else forbidden ()
    )
472

473 474 475 476 477 478 479
let () =
  Html5.register ~service:election_setup
    (fun uuid () ->
      with_setup_election_ro uuid (fun se ->
          T.election_setup uuid se ()
        )
    )
Stephane Glondu's avatar
Stephane Glondu committed
480

481 482 483 484
let () =
  Any.register ~service:election_setup_trustees
    (fun uuid () ->
      with_setup_election_ro uuid (fun se ->
485 486 487
          match se.se_threshold_trustees with
          | None -> T.election_setup_trustees uuid se () >>= Html5.send
          | Some _ -> redir_preapply election_setup_threshold_trustees uuid ()
488 489
        )
    )
490

491 492 493 494 495 496 497
let () =
  Html5.register ~service:election_setup_threshold_trustees
    (fun uuid () ->
      with_setup_election_ro uuid (fun se ->
          T.election_setup_threshold_trustees uuid se ()
        )
    )
498

499 500 501 502 503 504 505
let () =
  Html5.register ~service:election_setup_credential_authority
    (fun uuid () ->
      with_setup_election_ro uuid (fun se ->
          T.election_setup_credential_authority uuid se ()
        )
    )
506

Stephane Glondu's avatar
Stephane Glondu committed
507 508
let election_setup_mutex = Lwt_mutex.create ()

509
let with_setup_election ?(save = true) uuid f =
510 511
  with_site_user (fun u ->
      Lwt_mutex.with_lock election_setup_mutex (fun () ->
512
          let%lwt se = get_setup_election uuid in
513 514
          if se.se_owner = u then (
            try%lwt
515
              let%lwt r = f se in
Stephane Glondu's avatar
Stephane Glondu committed
516
              let%lwt () = if save then set_setup_election uuid se else return_unit in
517
              return r
518 519 520 521 522 523
            with e ->
              let service = preapply election_setup uuid in
              T.generic_page ~title:"Error" ~service (Printexc.to_string e) () >>= Html5.send
          ) else forbidden ()
        )
    )
524

525
let () =
526
  Any.register ~service:election_setup_languages
527 528 529 530
    (fun uuid languages ->
      with_setup_election uuid (fun se ->
          let langs = languages_of_string languages in
          match langs with
Stephane Glondu's avatar
Stephane Glondu committed
531
          | [] ->
532 533 534
             let service = preapply election_setup uuid in
             T.generic_page ~title:"Error" ~service
               "You must select at least one language!" () >>= Html5.send
Stephane Glondu's avatar
Stephane Glondu committed
535
          | _ :: _ ->
536 537 538
             let unavailable =
               List.filter (fun x ->
                   not (List.mem x available_languages)
Stephane Glondu's avatar
Stephane Glondu committed
539
                 ) langs
540 541 542 543 544
             in
             match unavailable with
             | [] ->
                se.se_metadata <- {
                   se.se_metadata with
Stephane Glondu's avatar
Stephane Glondu committed
545
                   e_languages = Some langs
546 547 548
                 };
                redir_preapply election_setup uuid ()
             | l :: _ ->
549 550
                let service = preapply election_setup uuid in
                T.generic_page ~title:"Error" ~service
551 552 553
                  ("No such language: " ^ l) () >>= Html5.send
        )
    )
554

555 556 557 558
let () =
  Any.register ~service:election_setup_contact
    (fun uuid contact ->
      with_setup_election uuid (fun se ->
559 560 561 562 563
          let contact =
            if contact = "" || contact = default_contact then
              None
            else Some contact
          in
564 565 566 567 568 569 570 571
          se.se_metadata <- {
              se.se_metadata with
              e_contact = contact
            };
          redir_preapply election_setup uuid ()
        )
    )

572
let () =
573
  Any.register ~service:election_setup_description
574 575 576 577 578 579 580 581 582
    (fun uuid (name, description) ->
      with_setup_election uuid (fun se ->
          se.se_questions <- {se.se_questions with
                               t_name = name;
                               t_description = description;
                             };
          redir_preapply election_setup uuid ()
        )
    )
583

584
let generate_password metadata langs title url id =
585
  let email, login = split_identity id in
586 587
  let%lwt salt = generate_token () in
  let%lwt password = generate_token () in
588
  let hashed = sha256_hex (salt ^ password) in
Stephane Glondu's avatar
Stephane Glondu committed
589 590
  let bodies = List.map (fun lang ->
    let module L = (val Web_i18n.get_lang lang) in
591 592
    let contact = T.contact_footer metadata L.please_contact in
    Printf.sprintf L.mail_password title login password url contact
Stephane Glondu's avatar
Stephane Glondu committed
593 594 595
  ) langs in
  let body = PString.concat "\n\n----------\n\n" bodies in
  let body = body ^ "\n\n-- \nBelenios" in
Stephane Glondu's avatar
Stephane Glondu committed
596 597 598 599 600
  let subject =
    let lang = List.hd langs in
    let module L = (val Web_i18n.get_lang lang) in
    Printf.sprintf L.mail_password_subject title
  in
601
  send_email email subject body >>
602
  return (salt, hashed)
603

604
let handle_password se uuid ~force voters =
605 606 607
  if List.length voters > !maxmailsatonce then
    Lwt.fail (Failure (Printf.sprintf "Cannot send passwords, there are too many voters (max is %d)" !maxmailsatonce))
  else
608 609 610 611
  let title = se.se_questions.t_name in
  let url = Eliom_uri.make_string_uri ~absolute:true ~service:election_home
    (uuid, ()) |> rewrite_prefix
  in
612
  let langs = get_languages se.se_metadata.e_languages in
613 614 615 616 617
  let%lwt () =
    Lwt_list.iter_s (fun id ->
        match id.sv_password with
        | Some _ when not force -> return_unit
        | None | Some _ ->
618
           let%lwt x = generate_password se.se_metadata langs title url id.sv_id in
619 620 621 622 623 624
           return (id.sv_password <- Some x)
      ) voters
  in
  let service = preapply election_setup uuid in
  T.generic_page ~title:"Success" ~service
    "Passwords have been generated and mailed!" () >>= Html5.send
625

626
let () =
627
  Any.register ~service:election_setup_auth_genpwd
628 629 630 631 632
    (fun uuid () ->
      with_setup_election uuid (fun se ->
          handle_password se uuid ~force:false se.se_voters
        )
    )
633

634
let () =
635
  Any.register ~service:election_regenpwd
Stephane Glondu's avatar
Stephane Glondu committed
636
    (fun (uuid, ()) () ->
637 638
      T.regenpwd uuid () >>= Html5.send)

639 640 641 642 643 644 645 646 647 648 649
let find_user_id uuid user =
  let uuid_s = raw_string_of_uuid uuid in
  let db = Lwt_io.lines_of_file (!spool_dir / uuid_s / "voters.txt") in
  let%lwt db = Lwt_stream.to_list db in
  let rec loop = function
    | [] -> Lwt.fail Not_found
    | id :: xs ->
       let _, login = split_identity id in
       if login = user then return id else loop xs
  in loop db

650
let () =
651
  Any.register ~service:election_regenpwd_post
652
    (fun (uuid, ()) user ->
653
      with_site_user (fun u ->
Stephane Glondu's avatar
Stephane Glondu committed
654
          let%lwt election = find_election uuid in
655
          let%lwt metadata = Web_persist.get_election_metadata uuid in
656
          if metadata.e_owner = Some u then (
657
            let table = "password_" ^ underscorize uuid in
658
            let table = Ocsipersist.open_table table in
Stephane Glondu's avatar
Stephane Glondu committed
659
            let title = election.e_params.e_name in
660 661 662 663 664 665
            let url = Eliom_uri.make_string_uri
                        ~absolute:true ~service:election_home
                        (uuid, ()) |> rewrite_prefix
            in
            let service = preapply election_admin (uuid, ()) in
            (try%lwt
666
               let%lwt id = find_user_id uuid user in
667
               let langs = get_languages metadata.e_languages in
668
               let%lwt x = generate_password metadata langs title url id in
669
               Ocsipersist.add table user x >>
Stephane Glondu's avatar
Stephane Glondu committed
670
                 dump_passwords (!spool_dir / raw_string_of_uuid uuid) table >>
671
                 T.generic_page ~title:"Success" ~service
672
                   ("A new password has been mailed to " ^ id ^ ".") ()
673 674 675 676 677 678 679 680
               >>= Html5.send
              with Not_found ->
                T.generic_page ~title:"Error" ~service
                  (user ^ " is not a registered user for this election.") ()
                >>= Html5.send
            )
          ) else forbidden ()
        )
681 682
    )

Stephane Glondu's avatar
Stephane Glondu committed
683
let () =
684
  Html5.register ~service:election_setup_questions
Stephane Glondu's avatar
Stephane Glondu committed
685
    (fun uuid () ->
686 687
      with_setup_election_ro uuid (fun se ->
          T.election_setup_questions uuid se ()
688
        )
689 690
    )

Stephane Glondu's avatar
Stephane Glondu committed
691
let () =
692
  Any.register ~service:election_setup_questions_post
693 694 695 696 697 698
    (fun uuid template ->
      with_setup_election uuid (fun se ->
          se.se_questions <- template_of_string template;
          redir_preapply election_setup uuid ()
        )
    )
Stephane Glondu's avatar
Stephane Glondu committed
699

Stephane Glondu's avatar
Stephane Glondu committed
700
let () =
701
  Html5.register ~service:election_setup_voters
Stephane Glondu's avatar
Stephane Glondu committed
702
    (fun uuid () ->
703 704
      with_setup_election_ro uuid (fun se ->
          T.election_setup_voters uuid se !maxmailsatonce ()
705
        )
Stephane Glondu's avatar
Stephane Glondu committed
706 707 708
    )

(* see http://www.regular-expressions.info/email.html *)
Stephane Glondu's avatar
Stephane Glondu committed
709
let identity_rex = Pcre.regexp
Stephane Glondu's avatar
Stephane Glondu committed
710
  ~flags:[`CASELESS]
711
  "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,7}(,[A-Z0-9._%+-]+)?$"
Stephane Glondu's avatar
Stephane Glondu committed
712

Stephane Glondu's avatar
Stephane Glondu committed
713 714
let is_identity x =
  try ignore (Pcre.pcre_exec ~rex:identity_rex x); true
Stephane Glondu's avatar
Stephane Glondu committed
715 716
  with Not_found -> false

717
let merge_voters a b f =
718 719 720 721 722 723 724
  let existing = List.fold_left (fun accu sv ->
    SSet.add sv.sv_id accu
  ) SSet.empty a in
  let _, res = List.fold_left (fun (existing, accu) sv_id ->
    if SSet.mem sv_id existing then
      (existing, accu)
    else
725
      (SSet.add sv_id existing, {sv_id; sv_password = f sv_id} :: accu)
726 727 728
  ) (existing, List.rev a) b in
  List.rev res

Stephane Glondu's avatar
Stephane Glondu committed
729
let () =
730
  Any.register ~service:election_setup_voters_add
731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747
    (fun uuid voters ->
      with_setup_election uuid (fun se ->
          if se.se_public_creds_received then
            forbidden ()
          else (
            let voters = Pcre.split voters in
            let () =
              try
                let bad = List.find (fun x -> not (is_identity x)) voters in
                Printf.ksprintf failwith "%S is not a valid identity" bad
              with Not_found -> ()
            in
            se.se_voters <- merge_voters se.se_voters voters (fun _ -> None);
            redir_preapply election_setup_voters uuid ()
          )
        )
    )
748 749

let () =
750
  Any.register ~service:election_setup_voters_remove
751 752 753 754 755 756 757 758 759 760
    (fun uuid voter ->
      with_setup_election uuid (fun se ->
          if se.se_public_creds_received then
            forbidden ()
          else (
            se.se_voters <- List.filter (fun v -> v.sv_id <> voter) se.se_voters;
            redir_preapply election_setup_voters uuid ()
          )
        )
    )
Stephane Glondu's avatar
Stephane Glondu committed
761

762 763
let () =
  Any.register ~service:election_setup_voters_passwd
764 765 766 767 768 769
    (fun uuid voter ->
      with_setup_election uuid (fun se ->
          let voter = List.filter (fun v -> v.sv_id = voter) se.se_voters in
          handle_password se uuid ~force:true voter
        )
    )
770

Stephane Glondu's avatar
Stephane Glondu committed
771
let () =
772
  Any.register ~service:election_setup_trustee_add
773
    (fun uuid st_id ->
774
      with_setup_election uuid (fun se ->
775
          if is_email st_id then (
776
            let%lwt st_token = generate_token () in
777
            let trustee = {st_id; st_token; st_public_key = ""; st_private_key = None} in
778
            se.se_public_keys <- se.se_public_keys @ [trustee];
Stephane Glondu's avatar
Stephane Glondu committed
779
            let%lwt () = Ocsipersist.add election_pktokens st_token (raw_string_of_uuid uuid) in
780
            redir_preapply election_setup_trustees uuid ()
781 782 783 784 785 786
          ) else (
            let msg = st_id ^ " is not a valid e-mail address!" in
            let service = preapply election_setup_trustees uuid in
            T.generic_page ~title:"Error" ~service msg () >>= Html5.send
          )
        )
787 788
    )

789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805
let () =
  Any.register ~service:election_setup_trustee_add_server
    (fun uuid () ->
      with_setup_election uuid (fun se ->
          let st_id = "server" and st_token = "" in
          let module G = (val Group.of_string se.se_group) in
          let module K = Trustees.MakeSimple (G) (LwtRandom) in
          let%lwt private_key = K.generate () in
          let%lwt public_key = K.prove private_key in
          let st_public_key = string_of_trustee_public_key G.write public_key in
          let st_private_key = Some private_key in
          let trustee = {st_id; st_token; st_public_key; st_private_key} in
          se.se_public_keys <- se.se_public_keys @ [trustee];
          redir_preapply election_setup_trustees uuid ()
        )
    )

806
let () =
807
  Any.register ~service:election_setup_trustee_del
808
    (fun uuid index ->
809 810 811 812 813 814 815 816 817 818
      with_setup_election uuid (fun se ->
          let trustees, old =
            se.se_public_keys |>
              List.mapi (fun i x -> i, x) |>
              List.partition (fun (i, _) -> i <> index) |>
              (fun (x, y) -> List.map snd x, List.map snd y)
          in
          se.se_public_keys <- trustees;
          let%lwt () =
            Lwt_list.iter_s (fun {st_token; _} ->
819 820 821
                if st_token <> "" then (
                  Ocsipersist.remove election_pktokens st_token
                ) else return_unit
822 823 824
              ) old
          in
          redir_preapply election_setup_trustees uuid ()
825
        )
826 827
    )

Stephane Glondu's avatar
Stephane Glondu committed
828
let () =
829
  Html5.register ~service:election_setup_credentials
Stephane Glondu's avatar
Stephane Glondu committed
830
    (fun token () ->
831
     let%lwt uuid = Ocsipersist.find election_credtokens token in
Stephane Glondu's avatar
Stephane Glondu committed
832
     let uuid = uuid_of_raw_string uuid in
833
     let%lwt se = get_setup_election uuid in
Stephane Glondu's avatar
Stephane Glondu committed
834 835
     T.election_setup_credentials token uuid se ()
    )
836

Stephane Glondu's avatar
Stephane Glondu committed
837
let wrap_handler f =
838
  try%lwt f ()
Stephane Glondu's avatar
Stephane Glondu committed
839
  with
840
  | e -> T.generic_page ~title:"Error" (Printexc.to_string e) () >>= Html5.send
Stephane Glondu's avatar
Stephane Glondu committed
841 842

let handle_credentials_post token creds =
843
  let%lwt uuid = Ocsipersist.find election_credtokens token in
Stephane Glondu's avatar
Stephane Glondu committed
844
  let uuid = uuid_of_raw_string uuid in
845
  let%lwt se = get_setup_election uuid in
846
  if se.se_public_creds_received then forbidden () else
Stephane Glondu's avatar
Stephane Glondu committed
847
  let module G = (val Group.of_string se.se_group : GROUP) in
Stephane Glondu's avatar
Stephane Glondu committed
848
  let fname = !spool_dir / raw_string_of_uuid uuid ^ ".public_creds.txt" in
Stephane Glondu's avatar
Stephane Glondu committed
849 850 851 852 853 854 855 856
  Lwt_mutex.with_lock
    election_setup_mutex
    (fun () ->
     Lwt_io.with_file
       ~flags:(Unix.([O_WRONLY; O_NONBLOCK; O_CREAT; O_TRUNC]))
       ~perm:0o600 ~mode:Lwt_io.Output fname
       (fun oc -> Lwt_io.write_chars oc creds)
    ) >>
857
  let%lwt () =
Stephane Glondu's avatar
Stephane Glondu committed
858
    let i = ref 1 in
859 860 861 862 863 864 865 866 867 868 869 870 871
    match%lwt read_file fname with
    | Some xs ->
       return (
           List.iter (fun x ->
               try
                 let x = G.of_string x in
                 if not (G.check x) then raise Exit;
                 incr i
               with _ ->
                 Printf.ksprintf failwith "invalid credential at line %d" !i
             ) xs
         )
    | None -> return_unit
Stephane Glondu's avatar
Stephane Glondu committed
872
  in
873
  let () = se.se_metadata <- {se.se_metadata with e_cred_authority = None} in
874
  let () = se.se_public_creds_received <- true in
875
  set_setup_election uuid se >>
876
  T.generic_page ~title:"Success"
877
    "Credentials have been received and checked!" () >>= Html5.send
Stephane Glondu's avatar
Stephane Glondu committed
878 879

let () =
880
  Any.register ~service:election_setup_credentials_post
Stephane Glondu's avatar
Stephane Glondu committed
881 882 883
    (fun token creds ->
     let s = Lwt_stream.of_string creds in
     wrap_handler (fun () -> handle_credentials_post token s))
884

Stephane Glondu's avatar
Stephane Glondu committed
885
let () =
886
  Any.register ~service:election_setup_credentials_post_file
Stephane Glondu's avatar
Stephane Glondu committed
887 888 889 890
    (fun token creds ->
     let s = Lwt_io.chars_of_file creds.Ocsigen_extensions.tmp_filename in
     wrap_handler (fun () -> handle_credentials_post token s))

891
module CG = Credential.MakeGenerate (LwtRandom)
892 893

let () =
894
  Any.register ~service:election_setup_credentials_server
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916
    (fun uuid () ->
      with_setup_election uuid (fun se ->
          let nvoters = List.length se.se_voters in
          if nvoters > !maxmailsatonce then
            Lwt.fail (Failure (Printf.sprintf "Cannot send credentials, there are too many voters (max is %d)" !maxmailsatonce))
          else if nvoters = 0 then
            Lwt.fail (Failure "No voters")
          else if se.se_public_creds_received then
            forbidden ()
          else (
            let () = se.se_metadata <- {se.se_metadata with
                                         e_cred_authority = Some "server"
                                       } in
            let title = se.se_questions.t_name in
            let url = Eliom_uri.make_string_uri
                        ~absolute:true ~service:election_home
                        (uuid, ()) |> rewrite_prefix
            in
            let module G = (val Group.of_string se.se_group : GROUP) in
            let module CD = Credential.MakeDerive (G) in
            let%lwt creds =
              Lwt_list.fold_left_s (fun accu v ->
917
                  let email, _ = split_identity v.sv_id in
918 919 920 921 922
                  let cas =
                    match se.se_metadata.e_auth_config with
                    | Some [{auth_system = "cas"; _}] -> true
                    | _ -> false
                  in
923 924 925 926 927 928 929 930 931
                  let%lwt cred = CG.generate () in
                  let pub_cred =
                    let x = CD.derive uuid cred in
                    let y = G.(g **~ x) in
                    G.to_string y
                  in
                  let langs = get_languages se.se_metadata.e_languages in
                  let bodies = List.map (fun lang ->
                                   let module L = (val Web_i18n.get_lang lang) in
932
                                   let intro = if cas then L.mail_credential_cas else L.mail_credential_password i