/
webserver.ml
2389 lines (2250 loc) · 81 KB
/
webserver.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 Core_kernel
open Libcommon
open Lwt
module Cl = Cohttp_lwt
module Clu = Cohttp_lwt_unix
module SA = Static_assets
module S = Clu.Server
module CRequest = Clu.Request
module CResponse = Clu.Response
module Header = Cohttp.Header
module Cookie = Cohttp.Cookie
module Client = Clu.Client
module A = Account
module C = Canvas
module Exception = Libexecution.Exception
module Util = Libexecution.Util
module Dval = Libexecution.Dval
module DvalMap = Libexecution.Types.RuntimeT.DvalMap
module PReq = Libexecution.Parsed_request
module Types = Libexecution.Types
module Http = Libexecution.Http
module RTT = Types.RuntimeT
module Handler = Libexecution.Handler
module TL = Libexecution.Toplevel
module Prelude = Libexecution.Prelude
module Dbconnection = Libservice.Dbconnection
module Span = Telemetry.Span
module Op = Libserialize.Op
module Db = Libbackend_basics.Db
module Config = Libbackend_basics.Config
module File = Libbackend_basics.File
module Event_queue = Libbackend_basics.Event_queue
(* ------------------------------- *)
(* utils *)
(* ------------------------------- *)
type timing_header = string * float * string
let shutdown = ref false
let ready = ref false
let server_timing (times : timing_header list) =
times
|> List.map ~f:(fun (name, time, desc) ->
Printf.sprintf "%s;desc=\"%s\";dur=%0.2f" name desc time)
|> String.concat ~sep:","
|> fun x -> [("Server-timing", x)] |> Header.of_list
let time (name : string) (fn : _ -> 'a) : timing_header * 'a =
let start = Unix.gettimeofday () in
let result = fn () in
let finish = Unix.gettimeofday () in
((name, (finish -. start) *. 1000.0, name), result)
let get_ip_address (ch : Conduit_lwt_unix.flow) : string =
match Conduit_lwt_unix.endp_of_flow ch with
| `TCP (ip, port) ->
Ipaddr.to_string ip
| _ ->
assert false
let request_to_rollbar (body : string) (req : CRequest.t) :
Libservice.Rollbar.request_data =
{ body
; headers = req |> CRequest.headers |> Cohttp.Header.to_list
; url = req |> CRequest.uri |> Uri.to_string
; http_method = req |> CRequest.meth |> Cohttp.Code.string_of_method }
type response_or_redirect_params =
| Respond of
{ resp_headers : Header.t
; execution_id : Types.id
; status : Cohttp.Code.status_code
; body : string }
| Redirect of
{ uri : Uri.t
; headers : Header.t option }
let respond_or_redirect (parent : Span.t) (params : response_or_redirect_params)
=
match params with
| Redirect {uri; headers} ->
Span.set_attr parent "response.status" (`Int 302) ;
S.respond_redirect ?headers ~uri ()
| Respond {resp_headers; execution_id; status; body} ->
let resp_headers =
Header.add_list
resp_headers
[(Libshared.Header.execution_id, Types.string_of_id execution_id)]
in
(* add Content-Length if missing, e.g. when function is called directly
* and not from `respond_or_redirect_empty_body`
*)
let resp_headers =
if Header.get resp_headers "Content-Length" = None
then
Header.add
resp_headers
"Content-Length"
(string_of_int (String.length body))
else resp_headers
in
Span.set_attrs
parent
[ ("response.status", `Int (Cohttp.Code.code_of_status status))
; ("response.content_length", `Int (String.length body)) ] ;
( match Header.get resp_headers "content-type" with
| Some ct ->
Span.set_attr parent "response.content_type" (`String ct)
| None ->
() ) ;
S.respond_string ~status ~body ~headers:resp_headers ()
let respond_or_redirect_empty_body
(span : Span.t) (params : response_or_redirect_params) =
match params with
| Redirect _ ->
respond_or_redirect span params
| Respond r ->
let headers =
Header.add
r.resp_headers
"Content-Length"
(string_of_int (String.length r.body))
in
respond_or_redirect
span
(Respond {r with body = ""; resp_headers = headers})
let respond
?(resp_headers = Header.init ())
~(execution_id : Types.id)
(span : Span.t)
status
(body : string) =
respond_or_redirect span (Respond {resp_headers; execution_id; status; body})
type host_route =
| Canvas of string
| Static
| Admin
(* NB: canvas in the DB is a string, not a uuid, because we do routing by canvas
* name, not canvas_id (see the host_route type above).
*
* In addition:
* - there are other place we use canvas_name as an fk; it's not great, but it's
* tech debt we can't solve today (for instance, iirc event queues do this)
* - the external id will be part of a CNAME target - that is,
* some.customdomain.com -> ismith-foo.darkcustomdomain.com. Thus, if you were
* able to change your canvas' name (see previous bullet, you currently cannot),
* and we used canvas_id, now you'd have a CNAME pointing at the old
* canvas_name, but the custom_domains record would point to the new
* canvas_name (via JOIN canvases as c ON c.id = canvas_id). So that's not
* awesome either!
*)
let canvas_from_db_opt (host_parts : string list) : host_route option =
let host = String.concat host_parts ~sep:"." in
Db.fetch_one_option
~name:"get_custom_domain"
~subject:host
"SELECT canvas
FROM custom_domains WHERE host = $1"
~params:[Db.String host]
(* List.hd_exn because the list in question is a list of fields; we should never
* get the wrong shape back from a query *)
|> Option.map ~f:(fun canvas_name -> Canvas (canvas_name |> List.hd_exn))
let should_use_https uri =
let parts =
uri |> Uri.host |> Option.value ~default:"" |> fun h -> String.split h '.'
in
match parts with
| ["darklang"; "com"]
| ["builtwithdark"; "com"]
| [_; "builtwithdark"; "com"]
(* Customers - do not remove the marker below *)
(* ACD-should_use_https-MARKER *)
| ["hellobirb"; "com"]
| ["www"; "hellobirb"; "com"] ->
true
| parts ->
(* If we've set up a custom domain, we should force https. If we haven't,
* and we've fallen all the way through (this is not a known host), then we
* should not, because it is likely a healthcheck or other k8s endpoint *)
parts |> canvas_from_db_opt |> Option.is_some
let redirect_to uri =
let proto = uri |> Uri.scheme |> Option.value ~default:"" in
(* If it's http and on a domain that can be served with https,
we want to redirect to the same url but with the scheme
replaced by "https". *)
if proto = "http" && should_use_https uri
then Some "https" |> Uri.with_scheme uri |> Some
else None
(* there might be some better way to do this... *)
let over_headers (r : CResponse.t) ~(f : Header.t -> Header.t) : CResponse.t =
CResponse.make
~version:(CResponse.version r)
~status:(CResponse.status r)
~flush:(CResponse.flush r)
~encoding:(CResponse.encoding r)
~headers:(r |> CResponse.headers |> f)
()
let over_headers_promise
(resp_promise : (Cohttp.Response.t * Cohttp_lwt__.Body.t) Lwt.t)
~(f : Header.t -> Header.t) :
(Cohttp.Response.t * Cohttp_lwt__.Body.t) Lwt.t =
let%lwt resp, body = resp_promise in
return (over_headers ~f resp, body)
let wrap_editor_api_headers =
let headers =
[ ("Content-type", "application/json; charset=utf-8")
; (Libshared.Header.server_version, Config.build_hash) ]
in
over_headers_promise ~f:(fun h -> Header.add_list h headers)
(* Proxies that terminate HTTPs should give us X-Forwarded-Proto: http
or X-Forwarded-Proto: https.
Return the URI, adding the scheme to the URI if there is an X-Forwarded-Proto. *)
let with_x_forwarded_proto req =
let uri = CRequest.uri req in
match Header.get (CRequest.headers req) "X-Forwarded-Proto" with
| Some proto ->
Uri.with_scheme uri (Some proto)
| None ->
( match Uri.scheme uri with
| Some _ ->
uri
| None ->
Uri.with_scheme uri (Some "http") )
(* Currently we just ensure that the Uri scheme is set *)
let canonicalize_request request =
let new_uri = with_x_forwarded_proto request in
let new_req =
CRequest.make
~meth:(CRequest.meth request)
~version:(CRequest.version request)
~encoding:(CRequest.encoding request)
~headers:(CRequest.headers request)
new_uri
in
(* Somewhat unbelievable, but CRequest.make strips the scheme (eg https)
* from the uri, so we need to add it back in. *)
{new_req with resource = Uri.to_string new_uri}
(* sanitize both repeated '/' and final '/'.
"/foo//bar/" -> "/foo/bar"
but leave "/" [root] untouched *)
let sanitize_uri_path path : string =
path
|> (fun str -> Re2.replace_exn (Re2.create_exn "/+") str ~f:(fun _ -> "/"))
|> fun str -> if str = "/" then str else Util.maybe_chop_suffix "/" str
(* -------------------------------------------- *)
(* handlers for end users *)
(* -------------------------------------------- *)
let cors = ("Access-Control-Allow-Origin", "*")
let infer_cors_header
(origin : string option) (setting : Canvas.cors_setting option) :
string option =
match (origin, setting) with
(* if there's no explicit canvas setting, allow common localhosts *)
| Some origin, None
when let default_origins =
[ "http://localhost:3000"
; "http://localhost:5000"
; "http://localhost:8000" ]
in
List.mem ~equal:( = ) default_origins origin ->
Some origin
(* if there's no explicit canvas setting and no default match, fall back to "*" *)
| _, None ->
Some "*"
(* If there's a "*" in the setting, always use it.
This is help as a debugging aid since users will always see
Access-Control-Allow-Origin: * in their browsers, even if the
request has no Origin. *)
| _, Some AllOrigins ->
Some "*"
(* if there's no supplied origin, don't set the header at all. *)
| None, _ ->
None
(* Return the origin if and only if it's in the setting *)
| Some origin, Some (Origins os) when List.mem ~equal:( = ) os origin ->
Some origin
(* Otherwise: there was a supplied origin and it's not in the setting.
return "null" explicitly *)
| Some _, Some _ ->
Some "null"
let options_handler ~(execution_id : Types.id) (c : C.canvas) (req : CRequest.t)
=
(* When javascript in a browser tries to make an unusual cross-origin
request (for example, a POST with a weird content-type or something with
weird headers), the browser first makes an OPTIONS request to the
server in order to get its permission to make that request. It includes
"origin", the originating origin, and "access-control-request-headers",
which is the list of headers the javascript would like to use.
(Ordinary GETs and some POSTs get handled in result_to_response, above,
without an OPTIONS).
Our strategy here is: if it's from an allowed origin (i.e., in the canvas
cors_setting) to return an Access-Control-Allow-Origin header for that
origin, to return Access-Control-Allow-Headers with the requested headers,
and Access-Control-Allow-Methods for all of the methods we think might
be useful.
*)
let req_headers =
Cohttp.Header.get (CRequest.headers req) "access-control-request-headers"
in
let allow_headers = match req_headers with Some h -> h | None -> "*" in
let resp_headers =
match
infer_cors_header
(Header.get (CRequest.headers req) "Origin")
c.cors_setting
with
| None ->
[]
| Some origin ->
[ ( "Access-Control-Allow-Methods"
, "GET,PUT,POST,DELETE,PATCH,HEAD,OPTIONS" )
; ("Access-Control-Allow-Origin", origin)
; ("Access-Control-Allow-Headers", allow_headers) ]
in
Respond
{ resp_headers = Cohttp.Header.of_list resp_headers
; execution_id
; status = `OK
; body = "" }
let result_to_response
~(c : Canvas.canvas ref)
~(execution_id : Types.id)
~(req : CRequest.t)
(result : RTT.dval) =
let maybe_infer_cors headers =
(* Add the Access-Control-Allow-Origin, if it doens't exist
and if infer_cors_header tells us to. *)
infer_cors_header
(Header.get (CRequest.headers req) "Origin")
!c.cors_setting
|> Option.value_map ~default:headers ~f:(fun cors ->
Header.add_unless_exists headers "Access-Control-Allow-Origin" cors)
in
let maybe_infer_ct value resp_headers =
let inferred_ct =
match value with
| RTT.DObj _ | RTT.DList _ ->
"application/json; charset=utf-8"
| _ ->
"text/plain; charset=utf-8"
in
(* Add the content-type, if it doesn't exist *)
Header.add_unless_exists resp_headers "Content-Type" inferred_ct
in
match result with
| RTT.DIncomplete _ ->
Respond
{ resp_headers = maybe_infer_cors (Header.init ())
; execution_id
; status = `Internal_server_error
; body =
"Application error: the executed code was not complete. This error can be resolved by the application author by completing the incomplete code."
}
| RTT.DError _ ->
Respond
{ resp_headers = maybe_infer_cors (Header.init ())
; execution_id
; status = `Internal_server_error
; body =
"Application error: the executed program was invalid. This problem can be resolved by the application's author by resolving the invalid code (often a type error)."
}
| RTT.DResp (Redirect url, value) ->
Redirect
{ headers = Header.init () |> maybe_infer_cors |> Some
; uri = Uri.of_string url }
| RTT.DResp (Response (code, resp_headers), value) ->
let resp_headers =
Header.of_list resp_headers |> maybe_infer_ct value |> maybe_infer_cors
in
let body =
match value with
| DBytes body ->
(* If the body is a DBytes, don't re-encode it *)
body |> RTT.RawBytes.to_string
| _ ->
let content_type_prefix =
Header.get resp_headers "Content-Type"
|> Option.map ~f:(fun ct -> ct |> String.split ~on:';')
|> Option.bind ~f:List.hd
in
( match content_type_prefix with
(* TODO: only pretty print for a webbrowser *)
| Some "text/plain" | Some "application/xml" ->
Dval.to_enduser_readable_text_v0 value
| Some "text/html" ->
Dval.to_enduser_readable_html_v0 value
| Some "application/json" | _ ->
Dval.to_pretty_machine_json_v1 value )
in
let status = Cohttp.Code.status_of_code code in
Respond {resp_headers; execution_id; status; body}
| _ ->
let body = Dval.to_pretty_machine_json_v1 result in
(* for demonstrations sake, let's return 200 Okay when
* no HTTP response object is returned *)
let resp_headers =
Header.init () |> maybe_infer_ct result |> maybe_infer_cors
in
Respond {resp_headers; execution_id; status = `OK; body}
let user_page_handler
~(execution_id : Types.id)
~(canvas : string)
~(ip : string)
~(uri : Uri.t)
~(body : string)
~(owner : Uuidm.t)
~(canvas_id : Uuidm.t)
(req : CRequest.t) : response_or_redirect_params =
let verb = req |> CRequest.meth |> Cohttp.Code.string_of_method in
let headers = req |> CRequest.headers |> Header.to_list in
let query = req |> CRequest.uri |> Uri.query in
let c =
C.load_http_from_cache
canvas
~owner
~canvas_id
~verb
~path:(sanitize_uri_path (Uri.path uri))
|> Result.map_error ~f:(String.concat ~sep:", ")
|> Prelude.Result.ok_or_internal_exception "Canvas loading error"
in
let pages =
!c.handlers
|> TL.http_handlers
|> Http.filter_matching_handlers ~path:(sanitize_uri_path (Uri.path uri))
in
let trace_id = Util.create_uuid () in
match pages with
| [] when String.Caseless.equal verb "OPTIONS" ->
options_handler ~execution_id !c req
(* If we have a 404, and path is /favicon.ico, then serve the
* default dark favicon.ico. Because we're matching on [], this code
* path won't get run if a user has a /favicon.ico handler (or a /*
* handler!). *)
| [] when Uri.path uri = "/favicon.ico" ->
(* NB: we're sending back a png, not an ico - this is deliberate,
* favicon.ico can be png, and the png is 685 bytes vs a 4+kb .ico.
* *)
let filename = "favicon-32x32.png" in
let filetype = Magic_mime.lookup filename in
let file = File.readfile ~root:Webroot "favicon-32x32.png" in
let resp_headers =
Cohttp.Header.of_list [cors; ("content-type", filetype)]
in
Respond {resp_headers; execution_id; status = `OK; body = file}
| [] ->
let fof_timestamp =
PReq.from_request ~allow_unparseable:true uri headers query body
|> PReq.to_dval
|> Stored_event.store_event
~trace_id
~canvas_id
("HTTP", Uri.path uri, verb)
in
Stroller.push_new_404
~execution_id
~canvas_id
("HTTP", Uri.path uri, verb, fof_timestamp, trace_id) ;
let resp_headers = Cohttp.Header.of_list [cors] in
Respond
{ resp_headers
; execution_id
; status = `Not_found
; body = "404 Not Found: No route matches" }
| a :: b :: _ ->
let resp_headers = Cohttp.Header.of_list [cors] in
Respond
{ resp_headers
; execution_id
; status = `Internal_server_error
; body =
"500 Internal Server Error: More than one handler for route: "
^ Uri.path uri }
| [page] ->
let input = PReq.from_request uri headers query body in
( match (Handler.module_for page, Handler.modifier_for page) with
| Some m, Some mo ->
(* Store the event with the input path not the event name, because we
* want to be able to
* a) use this event if this particular handler changes
* b) use the input url params in the analysis for this handler
*)
let desc = (m, Uri.path uri, mo) in
ignore
(Stored_event.store_event
~trace_id
~canvas_id
desc
(PReq.to_dval input))
| _ ->
() ) ;
let bound =
Libexecution.Execution.http_route_input_vars page (Uri.path uri)
in
let result, touched_tlids =
Libexecution.Execution.execute_handler
page
~execution_id
~account_id:!c.owner
~canvas_id
~user_fns:(Types.IDMap.data !c.user_functions)
~user_tipes:(Types.IDMap.data !c.user_tipes)
~package_fns:!c.package_fns
~secrets:(Secret.secrets_in_canvas !c.id)
~tlid:page.tlid
~dbs:(TL.dbs !c.dbs)
~input_vars:([("request", PReq.to_dval input)] @ bound)
~store_fn_arguments:
(Stored_function_arguments.store ~canvas_id ~trace_id)
~store_fn_result:(Stored_function_result.store ~canvas_id ~trace_id)
in
Stroller.push_new_trace_id
~execution_id
~canvas_id
trace_id
(page.tlid :: touched_tlids) ;
result_to_response ~c ~execution_id ~req result
(* -------------------------------------------- *)
(* Admin server *)
(* -------------------------------------------- *)
let static_assets_upload_handler
~(execution_id : Types.id)
~(user : Account.user_info)
(parent : Span.t)
(canvas : string)
req
body : (Cohttp.Response.t * Cohttp_lwt__.Body.t) Lwt.t =
try
match try Some (Canvas.id_for_name canvas) with _ -> None with
| None ->
respond
parent
~execution_id
`Not_found
"No canvas with this name exists"
| Some canvas ->
let ct =
match Cohttp.Header.get (CRequest.headers req) "content-type" with
| Some s ->
s
| None ->
"error"
in
(* making branch a request-configurable option requires product work:
* https://trello.com/c/pAD4uoJc/520-figure-out-branch-feature-for-static-assets
*)
let branch = "main" in
let sa = Static_assets.start_static_asset_deploy ~user canvas branch in
Stroller.push_new_static_deploy ~execution_id ~canvas_id:canvas sa ;
let deploy_hash = sa.deploy_hash in
let%lwt stream =
Multipart.parse_stream (Lwt_stream.of_list [body]) ct
in
let%lwt upload_results =
let%lwt parts = Multipart.get_parts stream in
let files =
(Multipart.StringMap.filter (fun _ v ->
match v with `File _ -> true | `String _ -> false))
parts
in
let files =
Multipart.StringMap.fold
(fun _ v acc ->
List.cons
( match v with
| `File f ->
f
| _ ->
Exception.internal "didn't expect a non-`File here" )
acc)
files
([] : Multipart.file List.t)
in
let processfile file =
let filename = Multipart.file_name file in
(* file_stream gives us a stream of strings; get a single string out
of it *)
let%lwt body =
Lwt_stream.fold_s
(fun elt acc -> Lwt.return (acc ^ elt))
(Multipart.file_stream file)
""
in
(* Replace DARK_STATIC_ASSETS_BASE_URL with the deployed URL. This
* will allow users to create SPAs with a sentinel value in them to
* converts to the absolute url. In React, you would do this with
* PUBLIC_URL. In Gatsby, you would do this with assetPrefix and include https schema*)
let body =
let filetype = Magic_mime.lookup filename in
let is_valid_text body =
body |> Libexecution.Unicode_string.of_string |> Option.is_some
in
(* Other mime type prefixes are video, image, audio,
* chemical, model, x-conference and can be ignored without
* the expensive conversion check *)
if String.is_prefix ~prefix:"video" filetype
|| String.is_prefix ~prefix:"image" filetype
|| String.is_prefix ~prefix:"audio" filetype
|| String.is_prefix ~prefix:"chemical" filetype
|| String.is_prefix ~prefix:"model" filetype
|| String.is_prefix ~prefix:"x-conference" filetype
then body
else if String.is_prefix ~prefix:"text" filetype
|| is_valid_text body
(* application/ or unknown and valid UTF-8*)
then
if String.is_substring
~substring:"https://DARK_STATIC_ASSETS_BASE_URL"
body
then
String.substr_replace_all
body
~pattern:"https://DARK_STATIC_ASSETS_BASE_URL"
~with_:sa.url
else
String.substr_replace_all
body
~pattern:"DARK_STATIC_ASSETS_BASE_URL"
~with_:sa.url
else (* application/* or unknown and _not_ valid UTF-8 *)
body
in
Static_assets.upload_to_bucket filename body canvas deploy_hash
in
Lwt.return (files |> List.map ~f:processfile)
in
let%lwt _, errors =
upload_results
|> Lwt_list.partition_p (fun r ->
match%lwt r with
| Ok _ ->
Lwt.return true
| Error _ ->
Lwt.return false)
in
let deploy =
Static_assets.finish_static_asset_deploy canvas deploy_hash
in
Stroller.push_new_static_deploy ~execution_id ~canvas_id:canvas deploy ;
( match errors with
| [] ->
respond
~execution_id
parent
`OK
( Yojson.Safe.to_string
(`Assoc
[ ("deploy_hash", `String deploy_hash)
; ( "url"
, `String (Static_assets.url canvas deploy_hash `Short) )
; ( "long-url"
, `String (Static_assets.url canvas deploy_hash `Long) )
])
|> Yojson.Basic.prettify )
| _ ->
let err_strs =
errors
|> Lwt_list.map_p (fun e ->
match%lwt e with
| Error (`GcloudAuthError s) ->
Lwt.return s
| Error (`FailureUploadingStaticAsset s) ->
Lwt.return s
| Error (`FailureDeletingStaticAsset s) ->
Lwt.return s
| Ok _ ->
Exception.internal
"Can't happen, we partition error/ok above.")
in
err_strs
>>= (function
| err_strs ->
Log.erroR
"Failed to deploy static assets to "
~params:
[ ("canvas", Canvas.name_for_id canvas)
; ("errs", String.concat ~sep:";" err_strs) ] ;
Static_assets.delete_static_asset_deploy
~user
canvas
branch
deploy_hash ;
respond
~resp_headers:(server_timing []) (* t1; t2; etc *)
~execution_id
parent
`Internal_server_error
( Yojson.Safe.to_string
(`Assoc
[ ( "msg"
, `String "We couldn't put this upload in gcloud." )
; ( "execution_id"
, `String (Types.string_of_id execution_id) )
; ( "errors"
, `List (List.map ~f:(fun s -> `String s) err_strs) )
])
|> Yojson.Basic.prettify )) )
with e -> raise e
let admin_add_op_handler
~(execution_id : Types.id)
~(user : Account.user_info)
(parent : Span.t)
(host : string)
body : (Cohttp.Response.t * Cohttp_lwt__.Body.t) Lwt.t =
let t1, (params, canvas_id) =
time "1-read-api-ops" (fun _ ->
let canvas_id, owner = Canvas.id_and_account_id_for_name_exn host in
let params = Api.to_add_op_rpc_params body in
if Serialize.is_latest_op_request
params.clientOpCtrId
params.opCtr
canvas_id
then (params, canvas_id)
else
( {params with ops = params.ops |> Op.filter_ops_received_out_of_order}
, canvas_id ))
in
let ops = params.ops in
let tlids = List.map ~f:Op.tlidOf ops in
let t2, maybe_c =
(* NOTE: Because we run canvas-wide validation logic, it's important
* that we load _at least_ the context (ie. datastores, functions, types etc. )
* and not just the tlids in the API payload.
* *)
time "2-load-saved-ops" (fun _ ->
match Op.required_context_to_validate_oplist ops with
| NoContext ->
C.load_only_tlids ~tlids host ops
| AllDatastores ->
C.load_with_dbs ~tlids host ops)
in
let params : Api.add_op_rpc_params =
{ ops = params.ops
; opCtr = params.opCtr
; clientOpCtrId = params.clientOpCtrId }
in
match maybe_c with
| Ok c ->
let t3, result =
time "3-to-frontend" (fun _ -> !c |> Analysis.to_add_op_rpc_result)
in
let t4, _ =
time "4-save-to-disk" (fun _ ->
(* work out the result before we save it, in case it has a
stackoverflow or other crashing bug *)
if Api.causes_any_changes params then C.save_tlids !c tlids else ())
in
let t5, strollerMsg =
(* To make this work with prodclone, we might want to have it specify
* more ... else people's prodclones will stomp on each other ... *)
time "5-send-ops-to-stroller" (fun _ ->
if Api.causes_any_changes params
then (
let strollerMsg =
{result; params}
|> Analysis.add_op_stroller_msg_to_yojson
|> Yojson.Safe.to_string
in
Stroller.push_new_event
~execution_id
~canvas_id
~event:"add_op"
strollerMsg ;
Some strollerMsg )
else None)
in
let t6, _ =
time "send event to heapio" (fun _ ->
(* NB: I believe we only send one op at a time, but the type is op
* list *)
ops
(* MoveTL and TLSavepoint make for noisy data, so exclude it from heapio *)
|> List.filter ~f:(function
| MoveTL _ | TLSavepoint _ ->
false
| _ ->
true)
|> List.iter ~f:(fun op ->
Lwt.async (fun () ->
Stroller.heapio_track
~canvas_id
~canvas:host
~user_id:user.id
~execution_id
Track
~event:(op |> Op.event_name_of_op)
(* currently empty, but we could add annotations later *)
(`Assoc []))))
in
Span.set_attr parent "op_ctr" (`Int params.opCtr) ;
respond
~resp_headers:(server_timing [t1; t2; t3; t4; t5; t6])
~execution_id
parent
`OK
(Option.value
~default:
( {result = Analysis.empty_to_add_op_rpc_result; params}
|> Analysis.add_op_stroller_msg_to_yojson
|> Yojson.Safe.to_string )
strollerMsg)
| Error errs ->
let body = String.concat ~sep:", " errs in
respond
~resp_headers:(server_timing [t1; t2])
~execution_id
parent
`Bad_request
body
let fetch_all_traces
~(execution_id : Types.id)
~(user : Account.user_info)
~(canvas : string)
~(permission : Authorization.permission option)
(parent : Span.t)
body : (Cohttp.Response.t * Cohttp_lwt__.Body.t) Lwt.t =
try
let t1, c =
time "1-load-canvas" (fun _ ->
C.load_all_from_cache canvas
|> Result.map_error ~f:(String.concat ~sep:", ")
|> Prelude.Result.ok_or_internal_exception "Failed to load canvas")
in
let t2, traces =
time "2-load-traces" (fun _ ->
let htraces =
!c.handlers
|> TL.handlers
|> List.map ~f:(fun h ->
Analysis.traceids_for_handler !c h
|> List.map ~f:(fun traceid -> (h.tlid, traceid)))
|> List.concat
in
let uftraces =
!c.user_functions
|> Types.IDMap.data
|> List.map ~f:(fun uf ->
Analysis.traceids_for_user_fn !c uf
|> List.map ~f:(fun traceid -> (uf.tlid, traceid)))
|> List.concat
in
htraces @ uftraces)
in
let t3, result =
time "3-to-frontend" (fun _ -> Analysis.to_all_traces_result traces)
in
respond
~execution_id
~resp_headers:(server_timing [t1; t2; t3])
parent
`OK
result
with e -> Libexecution.Exception.reraise_as_pageable e
let initial_load
~(execution_id : Types.id)
~(user : Account.user_info)
~(canvas : string)
~(permission : Authorization.permission option)
(parent : Span.t)
body : (Cohttp.Response.t * Cohttp_lwt__.Body.t) Lwt.t =
try
let t1, (c, op_ctrs) =
time "1-load-saved-ops" (fun _ ->
let c =
C.load_all_from_cache canvas
|> Result.map_error ~f:(String.concat ~sep:", ")
|> Prelude.Result.ok_or_internal_exception "Failed to load canvas"
in
let op_ctrs =
Db.fetch
~name:"fetch_op_ctrs_for_canvas"
"SELECT browser_id, ctr FROM op_ctrs WHERE canvas_id = $1"
~params:[Db.Uuid !c.id]
|> List.map ~f:(function
| [clientOpCtr_id; op_ctr] ->
(clientOpCtr_id, op_ctr |> int_of_string)
| _ ->
Exception.internal
"wrong record shape from fetch_op_Ctrs_for_canvas")
in
(c, op_ctrs))
in
let t2, unlocked =
time "2-analyze-unlocked-dbs" (fun _ ->
Analysis.unlocked ~canvas_id:!c.id ~account_id:!c.owner)
in
let t3, assets =
time "3-static-assets" (fun _ -> SA.all_deploys_in_canvas !c.id)
in
let t5, canvas_list =
time "5-canvas-list" (fun _ -> Serialize.hosts_for user.username)
in
let t6, org_canvas_list =
time "6-org-list" (fun _ -> Serialize.orgs_for user.username)
in
let t7, orgs = time "7-orgs" (fun _ -> Serialize.orgs user.username) in
let t8, worker_schedules =
time "8-worker-schedules" (fun _ ->
Event_queue.get_worker_schedules_for_canvas !c.id)
in
let t9, secrets =
time "9-secrets" (fun _ -> Secret.secrets_in_canvas !c.id)
in
let t10, result =
time "10-to-frontend" (fun _ ->
Analysis.to_initial_load_rpc_result
!c
op_ctrs
permission
unlocked
assets
user
canvas_list
orgs
org_canvas_list
worker_schedules
secrets)
in
respond
~execution_id
~resp_headers:(server_timing [t1; t2; t3; t5; t6; t7; t8; t9; t10])
parent
`OK
result
with e -> Libexecution.Exception.reraise_as_pageable e
let execute_function
~(execution_id : Types.id) (parent : Span.t) (host : string) body :
(Cohttp.Response.t * Cohttp_lwt__.Body.t) Lwt.t =
let t1, params =
time "1-read-api-ops" (fun _ -> Api.to_execute_function_rpc_params body)
in
let t2, c =
time "2-load-saved-ops" (fun _ ->
C.load_tlids_with_context_from_cache ~tlids:[params.tlid] host
|> Result.map_error ~f:(String.concat ~sep:", ")
|> Prelude.Result.ok_or_internal_exception "Failed to load canvas")
in
let t3, (result, tlids) =
time "3-execute" (fun _ ->
Analysis.execute_function
!c
params.fnname
~execution_id
~tlid:params.tlid
~trace_id:params.trace_id
~caller_id:params.caller_id
~args:params.args)
in