aboutsummaryrefslogtreecommitdiff
path: root/portal/lib
diff options
context:
space:
mode:
Diffstat (limited to 'portal/lib')
-rw-r--r--portal/lib/dune6
-rw-r--r--portal/lib/portal_ws.ml62
2 files changed, 68 insertions, 0 deletions
diff --git a/portal/lib/dune b/portal/lib/dune
new file mode 100644
index 0000000..8ce0232
--- /dev/null
+++ b/portal/lib/dune
@@ -0,0 +1,6 @@
+(library
+ (name portal_ws)
+ (modules portal_ws)
+ (public_name flesh_websockets)
+ (libraries lwt js_of_ocaml js_of_ocaml-lwt xmlm)
+ (preprocess (pps js_of_ocaml-ppx)))
diff --git a/portal/lib/portal_ws.ml b/portal/lib/portal_ws.ml
new file mode 100644
index 0000000..1570a9e
--- /dev/null
+++ b/portal/lib/portal_ws.ml
@@ -0,0 +1,62 @@
+open Lwt.Syntax
+open Js_of_ocaml
+let jss = Js.string
+let sjs = Js.to_string
+
+(* sic. XEP-0156: "host-meta files MUST be fetched only over HTTPS". I don't make the rules. *)
+let well_known_of (domain : string) = "https://" ^ domain ^ "/.well-known/host-meta"
+
+let ws_endpoint (domain : string) =
+ (** [ws_endpoint domain] is a promise containing the XMPP websocket endpoint associated with [domain], by using the
+ domain's Web-host Metadata.
+
+ This function uses XMLHttpRequest, so while it should work fine in the browser, in environments that don't provide
+ this constructor (Node.js), there should be some sort of polyfill.
+
+ Lastly, if [domain] doesn't provide a well-formed Web-host Metadata file, the function throws an exception. *)
+
+ let+ host_meta = Js_of_ocaml_lwt.XmlHttpRequest.perform_raw_url (well_known_of domain)
+ in let i = Xmlm.make_input (`String (0, host_meta.content))
+ (* This ugly function extracts the href element from a Link tag's attributes if it's a websocket. *)
+ and link_websocket attrs =
+ match attrs with
+ | ((_, "href"), href) :: ((_, "rel"), "urn:xmpp:alt-connections:websocket") :: _ -> Some href
+ | ((_, "rel"), "urn:xmpp:alt-connections:websocket") :: ((_, "href"), href) :: _ -> Some href
+ | _ -> None
+ in let parse_tree = (* Parse a single XRD tree. *)
+ Xmlm.input_tree
+ ~el:(fun tag children
+ -> match tag with
+ | ((_, "Link"), attributes) -> link_websocket attributes
+ | ((_, "XRD"), _) -> List.find_map (fun x -> x) children
+ | _ -> None)
+ ~data:(fun x -> Some x)
+ in ignore (Xmlm.input i); (* DTD *)
+ match parse_tree i with
+ | Some uri -> uri
+ | None -> failwith (domain ^ "doesn't advertise a WebSocket endpoint via Web-host Metadata.")
+
+let ws_stream (url : string) =
+ (** [ws_stream url] returns a stream (and its push function) that talk with the websocket located at [url].
+
+ Pushing [None] closes the websocket.
+
+ If the websocket is closed server-side, it's still up to the caller to close the stream. *)
+ let open Lwt_stream in
+ let handle ws incoming () =
+ let+ _ = iter (fun msg -> ws##send (jss msg)) incoming
+ in (ws##close)
+ in let stream, message = create () (* websocket -> user *)
+ and incoming, push = create () (* user -> websocket *)
+ in let (ws : WebSockets.webSocket Js.t) = new%js WebSockets.webSocket (jss url)
+ in ws##.onmessage :=
+ Dom.handler (fun x -> Some (sjs x##.data) |> message; Js._false);
+ ws##.onopen :=
+ Dom.handler (fun _ -> Lwt.async @@ handle ws incoming; Js._false);
+ ws##.onclose :=
+ Dom.handler (fun _ -> message None; Js._true);
+ stream, push
+
+(* let connect domain = *)
+(* let+ url = server_ws domain *)
+(* in ws_stream url *)