-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathunikernel.ml
2194 lines (2126 loc) · 93.6 KB
/
unikernel.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
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
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
open Lwt.Infix
type images = {
molly_img : string;
robur_img : string;
albatross_img : string;
mirage_img : string;
dashboard_img : string;
}
module Main
(R : Mirage_crypto_rng_mirage.S)
(P : Mirage_clock.PCLOCK)
(M : Mirage_clock.MCLOCK)
(T : Mirage_time.S)
(S : Tcpip.Stack.V4V6)
(KV_ASSETS : Mirage_kv.RO)
(BLOCK : Mirage_block.S)
(Http_client : Http_mirage_client.S) =
struct
module Paf = Paf_mirage.Make (S.TCP)
let js_contents assets =
KV_ASSETS.get assets (Mirage_kv.Key.v "main.js") >|= function
| Error _e -> invalid_arg "JS file could not be loaded"
| Ok js -> js
let css_contents assets =
KV_ASSETS.get assets (Mirage_kv.Key.v "style.css") >|= function
| Error _e -> invalid_arg "CSS file could not be loaded"
| Ok css -> css
let read_image assets key =
KV_ASSETS.get assets (Mirage_kv.Key.v key) >|= function
| Error _e -> invalid_arg "Image could not be loaded"
| Ok img -> img
let images assets =
let molly_img = read_image assets "molly_bird.jpeg" in
let robur_img = read_image assets "robur.png" in
let albatross_img = read_image assets "albatross_1.png" in
let mirage_img = read_image assets "mirage_os_1.png" in
let dashboard_img = read_image assets "dashboard_1.png" in
Lwt.all [ molly_img; robur_img; albatross_img; mirage_img; dashboard_img ]
>|= function
| [ molly_img; robur_img; albatross_img; mirage_img; dashboard_img ] ->
{ molly_img; robur_img; albatross_img; mirage_img; dashboard_img }
| _ -> failwith "Unexpected number of images"
module Store = Storage.Make (BLOCK)
module Map = Map.Make (String)
let read_multipart_data reqd =
let response_body = Httpaf.Reqd.request_body reqd in
let finished, notify_finished = Lwt.wait () in
let wakeup v = Lwt.wakeup_later notify_finished v in
let on_eof data () = wakeup data in
let f acc s = acc ^ s in
let rec on_read on_eof acc bs ~off ~len =
let str = Bigstringaf.substring ~off ~len bs in
let acc = acc >>= fun acc -> Lwt.return (f acc str) in
Httpaf.Body.schedule_read response_body ~on_read:(on_read on_eof acc)
~on_eof:(on_eof acc)
in
let f_init = Lwt.return "" in
Httpaf.Body.schedule_read response_body ~on_read:(on_read on_eof f_init)
~on_eof:(on_eof f_init);
finished >>= fun data ->
data >>= fun data ->
let content_type =
Httpaf.(
Headers.get_exn (Reqd.request reqd).Request.headers "content-type")
in
let ct = Multipart_form.Content_type.of_string (content_type ^ "\r\n") in
match ct with
| Error (`Msg msg) ->
Logs.warn (fun m ->
m "couldn't parse content-type %s: %S" content_type msg);
Error
(`Msg ("couldn't parse content-type " ^ content_type ^ ": " ^ msg))
|> Lwt.return
| Ok ct -> (
match Multipart_form.of_string_to_list data ct with
| Error (`Msg msg) ->
Logs.warn (fun m -> m "couldn't decode multipart data: %s" msg);
Error (`Msg ("Couldn't decode multipart data: " ^ msg))
|> Lwt.return
| Ok (m, assoc) -> Ok (m, assoc) |> Lwt.return)
let to_map ~assoc m =
let open Multipart_form in
let rec go (map, rest) = function
| Leaf { header; body } -> (
let filename =
Option.bind
(Header.content_disposition header)
Content_disposition.filename
in
match
Option.bind
(Header.content_disposition header)
Content_disposition.name
with
| Some name ->
(Map.add name (filename, List.assoc body assoc) map, rest)
| None -> (map, (body, (filename, List.assoc body assoc)) :: rest))
| Multipart { body; _ } ->
let fold acc = function Some elt -> go acc elt | None -> acc in
List.fold_left fold (map, rest) body
in
go (Map.empty, []) m
let generate_csrf_token store user now reqd =
let csrf = Middleware.generate_csrf_cookie now reqd in
let updated_user =
User_model.update_user user ~updated_at:now
~cookies:(csrf :: user.cookies) ()
in
Store.update_user store updated_user >>= function
| Ok () -> Lwt.return (Ok csrf.value)
| Error (`Msg err) ->
let error =
{
Utils.Status.code = 500;
title = "CSRF Token Error";
success = false;
data =
`String
("An error occured while generating a CSRF token. Error: " ^ err);
}
in
Lwt.return (Error error)
let decode_request_body reqd =
let request_body = Httpaf.Reqd.request_body reqd in
let finished, notify_finished = Lwt.wait () in
let wakeup v = Lwt.wakeup_later notify_finished v in
let on_eof data () = wakeup data in
let f acc s = acc ^ s in
let rec on_read on_eof acc bs ~off ~len =
let str = Bigstringaf.substring ~off ~len bs in
let acc = acc >>= fun acc -> Lwt.return (f acc str) in
Httpaf.Body.schedule_read request_body ~on_read:(on_read on_eof acc)
~on_eof:(on_eof acc)
in
let f_init = Lwt.return "" in
Httpaf.Body.schedule_read request_body ~on_read:(on_read on_eof f_init)
~on_eof:(on_eof f_init);
finished >>= fun data -> data
let extract_json_body reqd =
decode_request_body reqd >>= fun data ->
match
try Ok (Yojson.Basic.from_string data)
with Yojson.Json_error s -> Error (`Msg s)
with
| Error (`Msg err) ->
Logs.warn (fun m -> m "Failed to parse JSON: %s" err);
Lwt.return (Error (`Msg err))
| Ok (`Assoc json_dict) -> Lwt.return (Ok json_dict)
| Ok _ ->
Logs.warn (fun m -> m "JSON is not a dictionary: %s" data);
Lwt.return (Error (`Msg "not a dictionary"))
let csrf_verification f user csrf reqd =
let now = Ptime.v (P.now_d_ps ()) in
Middleware.csrf_verification user now csrf f reqd
let extract_json_csrf_token f token_or_cookie user reqd =
extract_json_body reqd >>= function
| Error (`Msg err) ->
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Bad_request
| Ok json_dict -> (
match token_or_cookie with
| `Token -> f user json_dict reqd
| `Cookie -> (
match Utils.Json.get User_model.csrf_cookie json_dict with
| Some (`String token) ->
csrf_verification (f user json_dict) user token reqd
| _ ->
Logs.warn (fun m ->
m "No csrf token in session request with Json body");
Middleware.http_response reqd ~title:"Error"
~data:(`String "Couldn't find CSRF token") `Bad_request))
let extract_multipart_csrf_token f token_or_cookie user reqd =
match Middleware.header "Content-Type" reqd with
| Some header when String.starts_with ~prefix:"multipart/form-data" header
-> (
read_multipart_data reqd >>= function
| Error (`Msg err) ->
Logs.warn (fun m -> m "Failed to read multipart data: %s" err);
Middleware.http_response reqd ~title:"Error"
~data:(`String ("Couldn't process multipart request: " ^ err))
`Bad_request
| Ok (m, assoc) -> (
let multipart_body, _r = to_map ~assoc m in
match token_or_cookie with
| `Token -> f user multipart_body reqd
| `Cookie -> (
match Map.find_opt "molly_csrf" multipart_body with
| None ->
Logs.warn (fun m -> m "No csrf token in multipart request");
Middleware.http_response reqd ~title:"Error"
~data:(`String "Couldn't find CSRF token") `Bad_request
| Some (_, token) ->
csrf_verification (f user multipart_body) user token reqd)))
| None | _ ->
Logs.warn (fun m -> m "Not a multipart request");
Middleware.http_response reqd ~title:"Error"
~data:(`String "Expected multipart form data") `Bad_request
module Albatross = Albatross.Make (T) (P) (S)
let email_verification f _ user reqd =
if false (* TODO *) then
Middleware.email_verified_middleware user (f user) reqd
else f user reqd
let authenticate_user ~check_admin ~check_token store reqd =
let ( let* ) = Result.bind in
let current_time = Ptime.v (P.now_d_ps ()) in
let user_is_active user =
if user.User_model.active then Ok ()
else Error "User account is deactivated"
in
let user_is_admin user =
if (check_admin && user.User_model.super_user) || not check_admin then
Ok ()
else
Error "You don't have the necessary permissions to access this service."
in
let check_cookie reqd =
match Middleware.session_cookie_value reqd with
| Error (`Msg err) ->
Error (`Cookie, "No molly-session in cookie header. %s" ^ err)
| Ok cookie_value -> (
match Store.find_by_cookie store cookie_value with
| None ->
Error (`Cookie, "Failed to find user with cookie " ^ cookie_value)
| Some (user, cookie) ->
if User_model.is_valid_cookie cookie current_time then
match
let* () = user_is_active user in
user_is_admin user
with
| Error msg -> Error (`Cookie, msg)
| Ok () -> Ok (`Cookie (user, cookie))
else
Error
( `Cookie,
"Session value doesn't match user session " ^ cookie_value
))
in
let valid_token token_value =
match Store.find_by_api_token store token_value with
| Some (user, token) ->
if User_model.is_valid_token token current_time then Ok (user, token)
else Error (`Token, "Token value is not valid " ^ token_value)
| None -> Error (`Token, "Failed to find user with token " ^ token_value)
in
if check_token then
match Middleware.api_authentication reqd with
| Some token_value -> (
let* user, token = valid_token token_value in
match
let* () = user_is_active user in
user_is_admin user
with
| Ok () -> Ok (`Token (user, token))
| Error msg -> Error (`Token, msg))
| None -> check_cookie reqd
else check_cookie reqd
let authenticate ?(check_admin = false) ?(api_meth = false)
?(check_token = false) store reqd f =
match authenticate_user ~check_admin ~check_token store reqd with
| Error (v, msg) ->
Logs.err (fun m -> m "authenticate: %s" msg);
if api_meth || v = `Token then
Middleware.http_response reqd ~title:"Error" ~data:(`String msg)
`Bad_request
else
Middleware.redirect_to_page ~path:"/sign-in" ~clear_session:true
~with_error:true ~msg reqd ()
| Ok (`Token (user, token)) -> (
Store.increment_token_usage store token user >>= function
| Error (`Msg err) ->
Middleware.http_response reqd ~title:"Error" ~data:(`String err)
`Internal_server_error
| Ok () -> f `Token user reqd)
| Ok (`Cookie (user, cookie)) -> (
Store.update_cookie_usage store cookie user reqd >>= function
| Error (`Msg err) ->
Logs.err (fun m -> m "Error with storage: %s" err);
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Internal_server_error
| Ok () -> f `Cookie user reqd)
let reply reqd ?(content_type = "text/plain") ?(header_list = []) data status
=
let h =
Httpaf.Headers.of_list
[
("content-length", string_of_int (String.length data));
("content-type", content_type);
]
in
let headers = Httpaf.Headers.add_list h header_list in
let resp = Httpaf.Response.create ~headers status in
Httpaf.Reqd.respond_with_string reqd resp data;
Lwt.return_unit
let user_volumes albatross user_name =
Albatross.query albatross ~domain:user_name (`Block_cmd `Block_info)
>|= function
| Error msg ->
Logs.err (fun m -> m "error while communicating with albatross: %s" msg);
[]
| Ok (_hdr, `Success (`Block_devices blocks)) -> blocks
| Ok reply ->
Logs.err (fun m ->
m "expected a block info reply, received %a"
(Vmm_commands.pp_wire ~verbose:false)
reply);
[]
let user_unikernels albatross user_name =
Albatross.query albatross ~domain:user_name (`Unikernel_cmd `Unikernel_info)
>|= function
| Error msg ->
Logs.err (fun m -> m "error while communicating with albatross: %s" msg);
[]
| Ok (_hdr, `Success (`Old_unikernel_info3 unikernels)) -> unikernels
| Ok reply ->
Logs.err (fun m ->
m "expected a unikernel info reply, received %a"
(Vmm_commands.pp_wire ~verbose:false)
reply);
[]
let user_unikernel albatross ~user_name ~unikernel_name =
Albatross.query albatross ~domain:user_name ~name:unikernel_name
(`Unikernel_cmd `Unikernel_info)
>|= function
| Error err ->
Logs.err (fun m ->
m
"Error while communicating with albatross. Trying to fetch %s \
resulted in : %s"
unikernel_name err);
Error err
| Ok (_hdr, `Success (`Old_unikernel_info3 [ unikernel ])) -> Ok unikernel
| Ok (_hdr, `Success (`Unikernel_info unikernels)) ->
let message =
Printf.sprintf
"Expected a single unikernel information from albatross, received \
%u"
(List.length unikernels)
in
Logs.err (fun m -> m "%s" message);
Error message
| Ok reply ->
let message =
Printf.sprintf
"Trying to fetch %s: expected a unikernel info reply, received %s"
unikernel_name
(Format.asprintf "%a" (Vmm_commands.pp_wire ~verbose:false) reply)
in
Logs.err (fun m -> m "%s" message);
Error message
let sign_up reqd =
let now = Ptime.v (P.now_d_ps ()) in
let csrf = Middleware.generate_csrf_cookie now reqd in
let csrf_cookie = csrf.name ^ "=" ^ csrf.value ^ ";Path=/;HttpOnly=true" in
match Middleware.session_cookie_value reqd with
| Ok x when x <> "" -> Middleware.redirect_to_dashboard reqd ()
| Ok _ | Error (`Msg _) ->
reply reqd ~content_type:"text/html"
(Sign_up.register_page ~csrf:csrf.value ~icon:"/images/robur.png")
~header_list:
[ ("Set-Cookie", csrf_cookie); ("X-MOLLY-CSRF", csrf.value) ]
`OK
let sign_in reqd =
match Middleware.session_cookie_value reqd with
| Ok x when x <> "" -> Middleware.redirect_to_dashboard reqd ()
| Ok _ | Error (`Msg _) ->
reply reqd ~content_type:"text/html"
(Sign_in.login_page ~icon:"/images/robur.png" ())
`OK
let register store reqd =
decode_request_body reqd >>= fun data ->
let json =
try Ok (Yojson.Basic.from_string data)
with Yojson.Json_error s -> Error (`Msg s)
in
match json with
| Error (`Msg err) ->
Logs.warn (fun m -> m "Failed to parse JSON: %s" err);
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Bad_request
| Ok (`Assoc json_dict) -> (
let validate_user_input ~name ~email ~password ~form_csrf =
if name = "" || email = "" || password = "" then
Error "All fields must be filled."
else if String.length name < 4 then
Error "Name must be at least 3 characters long."
else if not (Utils.Email.validate_email email) then
Error "Invalid email address."
else if not (User_model.password_validation password) then
Error "Password must be at least 8 characters long."
else if form_csrf = "" then
Error "CSRF token mismatch error. Please referesh and try again."
else Ok "Validation passed."
in
match
Utils.Json.
( get "email" json_dict,
get "password" json_dict,
get "name" json_dict,
get "form_csrf" json_dict )
with
| ( Some (`String email),
Some (`String password),
Some (`String name),
Some (`String form_csrf) ) -> (
match validate_user_input ~name ~email ~password ~form_csrf with
| Error err ->
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Bad_request
| Ok _ ->
if Middleware.csrf_cookie_verification form_csrf reqd then
let existing_email = Store.find_by_email store email in
let existing_name = Store.find_by_name store name in
match (existing_name, existing_email) with
| Some _, None ->
Middleware.http_response reqd ~title:"Error"
~data:(`String "A user with this name already exist.")
`Bad_request
| None, Some _ ->
Middleware.http_response reqd ~title:"Error"
~data:(`String "A user with this email already exist.")
`Bad_request
| None, None -> (
let created_at = Ptime.v (P.now_d_ps ()) in
let user, cookie =
let active, super_user =
if Store.count_users store = 0 then (true, true)
else (false, false)
in
User_model.create_user ~name ~email ~password
~created_at ~active ~super_user
~user_agent:(Middleware.user_agent reqd)
in
Store.add_user store user >>= function
| Ok () ->
let cookie_value =
cookie.name ^ "=" ^ cookie.value
^ ";Path=/;HttpOnly=true"
in
let header_list =
[
("Set-Cookie", cookie_value);
("location", "/dashboard");
]
in
Middleware.http_response reqd ~header_list
~title:"Success"
~data:(User_model.user_to_json user)
`OK
| Error (`Msg err) ->
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Bad_request)
| _ ->
Middleware.http_response reqd ~title:"Error"
~data:
(`String
"A user with this name or email already exist.")
`Bad_request
else
Middleware.http_response reqd ~title:"Error"
~data:
(`String
"CSRF token mismatch error. Please referesh and try \
again.") `Bad_request)
| _ ->
Middleware.http_response reqd ~title:"Error"
~data:
(`String
(Fmt.str "Register: Unexpected fields. Got %s"
(Yojson.Basic.to_string (`Assoc json_dict))))
`Bad_request)
| _ ->
Middleware.http_response reqd ~title:"Error"
~data:(`String "Register account: expected a dictionary") `Bad_request
let login store reqd =
decode_request_body reqd >>= fun data ->
let json =
try Ok (Yojson.Basic.from_string data)
with Yojson.Json_error s -> Error (`Msg s)
in
match json with
| Error (`Msg err) ->
Logs.warn (fun m -> m "Failed to parse JSON: %s" err);
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Bad_request
| Ok (`Assoc json_dict) -> (
let validate_user_input ~email ~password =
if email = "" || password = "" then Error "All fields must be filled."
else if not (Utils.Email.validate_email email) then
Error "Invalid email address."
else if String.length password < 8 then
Error "Password must be at least 8 characters long."
else Ok "Validation passed."
in
match Utils.Json.(get "email" json_dict, get "password" json_dict) with
| Some (`String email), Some (`String password) -> (
match validate_user_input ~email ~password with
| Error err ->
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Bad_request
| Ok _ -> (
let now = Ptime.v (P.now_d_ps ()) in
let user = Store.find_by_email store email in
match
User_model.login_user ~email ~password
~user_agent:(Middleware.user_agent reqd)
user now
with
| Error (`Msg err) ->
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Bad_request
| Ok (user, cookie) -> (
Store.update_user store user >>= function
| Ok () ->
let cookie_value =
cookie.name ^ "=" ^ cookie.value
^ ";Path=/;HttpOnly=true"
in
let header_list =
[
("Set-Cookie", cookie_value);
("location", "/dashboard");
]
in
Middleware.http_response reqd ~header_list
~title:"Success"
~data:(User_model.user_to_json user)
`OK
| Error (`Msg err) ->
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Internal_server_error)))
| _ ->
Middleware.http_response reqd ~title:"Error"
~data:
(`String
(Fmt.str "Update password: Unexpected fields. Got %s"
(Yojson.Basic.to_string (`Assoc json_dict))))
`Bad_request)
| _ ->
Middleware.http_response reqd ~title:"Error"
~data:(`String "Update password: expected a dictionary") `Bad_request
let verify_email store (user : User_model.user) reqd =
let now = Ptime.v (P.now_d_ps ()) in
generate_csrf_token store user now reqd >>= function
| Ok csrf -> (
let email_verification_uuid = User_model.generate_uuid () in
let updated_user =
User_model.update_user user
~updated_at:(Ptime.v (P.now_d_ps ()))
~email_verification_uuid:(Some email_verification_uuid) ()
in
Store.update_user store updated_user >>= function
| Ok () ->
let verification_link =
Utils.Email.generate_verification_link email_verification_uuid
in
Logs.info (fun m -> m "Verification link is: %s" verification_link);
reply reqd ~content_type:"text/html"
(Verify_email.verify_page user ~csrf ~icon:"/images/robur.png")
~header_list:[ ("X-MOLLY-CSRF", csrf) ]
`OK
| Error (`Msg err) ->
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Internal_server_error)
| Error err ->
reply reqd ~content_type:"text/html"
(Guest_layout.guest_layout ~page_title:"500 | Mollymawk"
~content:(Error_page.error_layout err)
~icon:"/images/robur.png" ())
`Internal_server_error
let verify_email_token store verification_token (user : User_model.user) reqd
=
match
let ( let* ) = Result.bind in
let* uuid =
Option.to_result ~none:(`Msg "invalid UUID")
(Uuidm.of_string verification_token)
in
let u = Store.find_email_verification_token store uuid in
User_model.verify_email_token u verification_token
(Ptime.v (P.now_d_ps ()))
with
| Ok user' ->
if String.equal user.uuid user'.uuid then
Store.update_user store user >>= function
| Ok () -> Middleware.redirect_to_dashboard reqd ()
| Error (`Msg msg) ->
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped msg))
`Internal_server_error
else
Middleware.http_response reqd ~title:"Error"
~data:(`String "Logged in user is not the to-be-verified one")
`Bad_request
| Error (`Msg s) ->
Middleware.redirect_to_page ~path:"/sign-in" ~clear_session:true
~with_error:true reqd ~msg:s ()
let toggle_account_attribute json_dict store reqd ~key update_fn error_on_last
~error_message =
match Utils.Json.get "uuid" json_dict with
| Some (`String uuid) -> (
match Store.find_by_uuid store uuid with
| None ->
Logs.warn (fun m -> m "%s : Account not found" key);
Middleware.http_response reqd ~title:"Error"
~data:(`String "Account not found") `Not_found
| Some user -> (
if error_on_last user then (
Logs.warn (fun m ->
m "%s : Can't perform action on last user" key);
Middleware.http_response reqd ~title:"Error" ~data:error_message
`Forbidden)
else
let updated_user = update_fn user in
Store.update_user store updated_user >>= function
| Ok () ->
Middleware.http_response reqd ~title:"OK"
~data:(`String "Updated user successfully") `OK
| Error (`Msg msg) ->
Logs.warn (fun m -> m "%s : Storage error with %s" key msg);
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped msg))
`Internal_server_error))
| _ ->
Logs.warn (fun m -> m "%s: Failed to parse JSON - no UUID found" key);
Middleware.http_response reqd ~title:"Error"
~data:(`String "Couldn't find a UUID in the JSON.") `Not_found
let toggle_account_activation store _user json_dict reqd =
toggle_account_attribute json_dict store reqd ~key:"toggle-active-account"
(fun user ->
User_model.update_user user ~active:(not user.active)
~updated_at:(Ptime.v (P.now_d_ps ()))
())
(fun user -> user.active && Store.count_active store <= 1)
~error_message:(`String "Cannot deactivate last active user")
let toggle_admin_activation store _user json_dict reqd =
toggle_account_attribute json_dict store reqd ~key:"toggle-admin-account"
(fun user ->
User_model.update_user user ~super_user:(not user.super_user)
~updated_at:(Ptime.v (P.now_d_ps ()))
())
(fun user -> user.super_user && Store.count_superusers store <= 1)
~error_message:(`String "Cannot remove last administrator")
let dashboard store albatross _ (user : User_model.user) reqd =
let now = Ptime.v (P.now_d_ps ()) in
generate_csrf_token store user now reqd >>= function
| Ok csrf ->
(* TODO use uuid in the future *)
user_unikernels albatross user.name >>= fun unikernels ->
reply reqd ~content_type:"text/html"
(Dashboard.dashboard_layout ~csrf user
~content:
(Unikernel_index.unikernel_index_layout unikernels
(Ptime.v (P.now_d_ps ())))
~icon:"/images/robur.png" ())
`OK
| Error err ->
reply reqd ~content_type:"text/html"
(Guest_layout.guest_layout ~page_title:"500 | Mollymawk"
~content:(Error_page.error_layout err)
~icon:"/images/robur.png" ())
`Internal_server_error
let account_page store _ (user : User_model.user) reqd =
match Middleware.session_cookie_value reqd with
| Ok active_cookie_value -> (
let now = Ptime.v (P.now_d_ps ()) in
generate_csrf_token store user now reqd >>= function
| Ok csrf ->
reply reqd ~content_type:"text/html"
(Dashboard.dashboard_layout ~csrf user
~page_title:"Account | Mollymawk"
~content:
(User_account.user_account_layout user ~active_cookie_value
now)
~icon:"/images/robur.png" ())
~header_list:[ ("X-MOLLY-CSRF", csrf) ]
`OK
| Error err ->
reply reqd ~content_type:"text/html"
(Guest_layout.guest_layout ~page_title:"500 | Mollymawk"
~content:(Error_page.error_layout err)
~icon:"/images/robur.png" ())
`Internal_server_error)
| Error (`Msg err) ->
let error =
{
Utils.Status.code = 401;
title = "Unauthenticated";
success = false;
data = `String err;
}
in
reply reqd ~content_type:"text/html"
(Guest_layout.guest_layout ~page_title:"401 | Mollymawk"
~content:(Error_page.error_layout error)
~icon:"/images/robur.png" ())
`Unauthorized
let update_password store (user : User_model.user) json_dict reqd =
match
Utils.Json.
( get "current_password" json_dict,
get "new_password" json_dict,
get "confirm_password" json_dict )
with
| ( Some (`String current_password),
Some (`String new_password),
Some (`String confirm_password) ) -> (
let now = Ptime.v (P.now_d_ps ()) in
let new_password_hash =
User_model.hash_password ~password:new_password ~uuid:user.uuid
in
if
not
(String.equal user.password
(User_model.hash_password ~password:current_password
~uuid:user.uuid))
then
Middleware.http_response reqd ~title:"Error"
~data:(`String "The current password entered is wrong.")
`Bad_request
else if not (String.equal new_password confirm_password) then
Middleware.http_response reqd ~title:"Error"
~data:(`String "New password and confirm password do not match")
`Bad_request
else if not (User_model.password_validation new_password) then
Middleware.http_response reqd ~title:"Error"
~data:(`String "New password must be atleast 8 characters.")
`Internal_server_error
else
let updated_user =
User_model.update_user user ~password:new_password_hash
~updated_at:now ()
in
Store.update_user store updated_user >>= function
| Ok () ->
Middleware.http_response reqd ~title:"OK"
~data:(`String "Updated password successfully") `OK
| Error (`Msg err) ->
Logs.warn (fun m -> m "Storage error with %s" err);
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Internal_server_error)
| _ ->
Middleware.http_response reqd ~title:"Error"
~data:
(`String
(Fmt.str "Update password: Unexpected fields. Got %s"
(Yojson.Basic.to_string (`Assoc json_dict))))
`Bad_request
let new_user_cookies ~user ~filter ~redirect store reqd =
let now = Ptime.v (P.now_d_ps ()) in
let cookies = List.filter filter user.User_model.cookies in
let updated_user =
User_model.update_user user ~cookies ~updated_at:now ()
in
Store.update_user store updated_user >>= function
| Ok () -> redirect
| Error (`Msg err) ->
Logs.warn (fun m -> m "Storage error with %s" err);
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Internal_server_error
let close_sessions ?to_logout_cookie ?(logout = false) store
(user : User_model.user) _json_dict reqd =
match Middleware.session_cookie_value reqd with
| Ok cookie_value -> (
match User_model.user_session_cookie user cookie_value with
| Some cookie ->
let filter, redirect =
match (to_logout_cookie, logout) with
| None, false ->
( (fun (c : User_model.cookie) ->
not
(String.equal c.name User_model.session_cookie
&& c.value <> cookie.value)),
Middleware.http_response reqd ~title:"OK"
~data:(`String "Closed all sessions succesfully") `OK )
| _, true ->
( (fun (c : User_model.cookie) ->
not (String.equal c.value cookie.value)),
Middleware.http_response reqd ~title:"OK"
~data:(`String "Logout succesful") `OK )
| Some to_logout_cookie_value, false ->
( (fun (c : User_model.cookie) ->
not (String.equal to_logout_cookie_value c.value)),
Middleware.redirect_to_page ~path:"/account"
~msg:"Closed session succesfully" reqd () )
in
new_user_cookies ~user ~filter ~redirect store reqd
| None ->
let error =
{
Utils.Status.code = 401;
title = "Unauthenticated";
success = false;
data = `String "Auth cookie not found";
}
in
reply reqd ~content_type:"text/html"
(Guest_layout.guest_layout ~page_title:"401 | Mollymawk"
~content:(Error_page.error_layout error)
~icon:"/images/robur.png" ())
`Unauthorized)
| Error (`Msg err) ->
let error =
{
Utils.Status.code = 401;
title = "Unauthenticated";
success = false;
data = `String err;
}
in
reply reqd ~content_type:"text/html"
(Guest_layout.guest_layout ~page_title:"401 | Mollymawk"
~content:(Error_page.error_layout error)
~icon:"/images/robur.png" ())
`Unauthorized
let close_session store (user : User_model.user) json_dict reqd =
match Utils.Json.(get "session_value" json_dict) with
| Some (`String session_value) -> (
let now = Ptime.v (P.now_d_ps ()) in
let cookies =
List.filter
(fun (cookie : User_model.cookie) ->
not (String.equal cookie.value session_value))
user.cookies
in
let updated_user =
User_model.update_user user ~cookies ~updated_at:now ()
in
Store.update_user store updated_user >>= function
| Ok () ->
Middleware.http_response reqd ~title:"Success"
~data:(`String "Session closed succesfully") `OK
| Error (`Msg err) ->
Logs.warn (fun m -> m "Storage error with %s" err);
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Internal_server_error)
| _ ->
Middleware.http_response reqd ~title:"Error"
~data:
(`String
(Fmt.str "Close session: Unexpected fields. Got %s"
(Yojson.Basic.to_string (`Assoc json_dict))))
`Bad_request
let users store _ (user : User_model.user) reqd =
let now = Ptime.v (P.now_d_ps ()) in
generate_csrf_token store user now reqd >>= function
| Ok csrf ->
reply reqd ~content_type:"text/html"
(Dashboard.dashboard_layout ~csrf user ~page_title:"Users | Mollymawk"
~content:
(Users_index.users_index_layout (Store.users store)
(Ptime.v (P.now_d_ps ())))
~icon:"/images/robur.png" ())
`OK
| Error err ->
reply reqd ~content_type:"text/html"
(Guest_layout.guest_layout ~page_title:"500 | Mollymawk"
~content:(Error_page.error_layout err)
~icon:"/images/robur.png" ())
`Internal_server_error
let settings store _ (user : User_model.user) reqd =
let now = Ptime.v (P.now_d_ps ()) in
generate_csrf_token store user now reqd >>= function
| Ok csrf ->
reply reqd ~content_type:"text/html"
(Dashboard.dashboard_layout ~csrf user
~page_title:"Settings | Mollymawk"
~content:
(Settings_page.settings_layout (Store.configuration store))
~icon:"/images/robur.png" ())
~header_list:[ ("X-MOLLY-CSRF", csrf) ]
`OK
| Error err ->
reply reqd ~content_type:"text/html"
(Guest_layout.guest_layout ~page_title:"500 | Mollymawk"
~content:(Error_page.error_layout err)
~icon:"/images/robur.png" ())
`Internal_server_error
let update_settings stack store albatross _user json_dict reqd =
match
Configuration.of_json_from_http json_dict (Ptime.v (P.now_d_ps ()))
with
| Ok configuration_settings -> (
Store.update_configuration store configuration_settings >>= function
| Ok () ->
Albatross.init stack configuration_settings.server_ip
~port:configuration_settings.server_port
configuration_settings.certificate
configuration_settings.private_key
>>= fun new_albatross ->
albatross := new_albatross;
Middleware.http_response reqd ~title:"Success"
~data:(`String "Configuration updated successfully") `OK
| Error (`Msg err) ->
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Internal_server_error)
| Error (`Msg err) ->
Middleware.http_response ~title:"Error"
~data:(`String (String.escaped err))
reqd `Bad_request
let deploy_form store _ (user : User_model.user) reqd =
let now = Ptime.v (P.now_d_ps ()) in
generate_csrf_token store user now reqd >>= function
| Ok csrf ->
reply reqd ~content_type:"text/html"
(Dashboard.dashboard_layout ~csrf user
~page_title:"Deploy a Unikernel | Mollymawk"
~content:Unikernel_create.unikernel_create_layout
~icon:"/images/robur.png" ())
~header_list:[ ("X-MOLLY-CSRF", csrf) ]
`OK
| Error err ->
reply reqd ~content_type:"text/html"
(Guest_layout.guest_layout ~page_title:"500 | Mollymawk"
~content:(Error_page.error_layout err)
~icon:"/images/robur.png" ())
`Internal_server_error
let unikernel_info albatross _ (user : User_model.user) reqd =
(* TODO use uuid in the future *)
Albatross.query albatross ~domain:user.name (`Unikernel_cmd `Unikernel_info)
>>= function
| Error msg ->
Middleware.http_response reqd ~title:"Error"
~data:(`String ("Error while querying albatross: " ^ msg))
`Internal_server_error
| Ok (_hdr, res) -> (
match Albatross_json.res res with
| Ok res -> Middleware.http_response reqd ~title:"Success" ~data:res `OK
| Error (`String err) ->
Middleware.http_response reqd ~title:"Error"
~data:(`String (String.escaped err))
`Internal_server_error)
let unikernel_info_one albatross store name _ (user : User_model.user) reqd =
(* TODO use uuid in the future *)
user_unikernel albatross ~user_name:user.name ~unikernel_name:name
>>= fun unikernel_info ->
match unikernel_info with
| Error err ->
reply reqd ~content_type:"text/html"
(Guest_layout.guest_layout ~page_title:"500 | Mollymawk"
~content:
(Error_page.error_layout
{