"
6 | authors: "Andrew J"
7 | license: "MIT"
8 | homepage: "https://github.com/jeffa5/mirage-xmpp"
9 | bug-reports: "https://github.com/jeffa5/mirage-xmpp/issues"
10 | depends: [
11 | "ocaml" {>= "4.03.0"}
12 | "dune" {build}
13 | "lwt"
14 | "sexplib"
15 | "markup-lwt"
16 | "ppx_expect"
17 | "ppx_deriving"
18 | "lwt_ppx"
19 | "bisect_ppx"
20 | "astring"
21 | "asetmap"
22 | "uuidm"
23 | "base64" {>= "3.0.0"}
24 | ]
25 | build: [
26 | ["dune" "build" "-p" name "-j" jobs]
27 | ]
28 | run-test: [
29 | ["dune" "build" "@src/runtest"]
30 | ]
31 |
--------------------------------------------------------------------------------
/mirage/build-unikernel:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # build
4 | mirage configure -t unix --net socket
5 |
6 | # continue building
7 | if [ -z ${TRAVIS_BUILD} ]; then
8 | OPAMVAR_os_family=arch make depend
9 | else
10 | make depend
11 | fi
12 | make
13 |
--------------------------------------------------------------------------------
/mirage/config.ml:
--------------------------------------------------------------------------------
1 | open Mirage
2 |
3 | let port =
4 | let doc =
5 | Key.Arg.info ~doc:"The TCP port on which to listen for incoming connections." ["port"]
6 | in
7 | Key.(create "port" Arg.(opt int 5222 doc))
8 | ;;
9 |
10 | let hostname =
11 | let doc = Key.Arg.info ~doc:"The hostname for the server." ["hostname"] in
12 | Key.(create "hostname" Arg.(opt string "localhost" doc))
13 | ;;
14 |
15 | let packages = [package "lwt_ppx"; package "mirage-xmpp"]
16 |
17 | let main =
18 | foreign
19 | ~keys:[Key.abstract port; Key.abstract hostname]
20 | ~packages
21 | "Unikernel.Main"
22 | (stackv4 @-> job)
23 | ;;
24 |
25 | let stack = generic_stackv4 default_network
26 | let () = register "xmpp" [main $ stack]
27 |
--------------------------------------------------------------------------------
/mirage/dune:
--------------------------------------------------------------------------------
1 | (alias
2 | (name mirage)
3 | (deps (source_tree .) (package mirage-xmpp))
4 | (action (system "./build-unikernel")))
5 |
--------------------------------------------------------------------------------
/mirage/dune-project:
--------------------------------------------------------------------------------
1 | (lang dune 1.6)
2 |
--------------------------------------------------------------------------------
/mirage/unikernel.ml:
--------------------------------------------------------------------------------
1 | module Main (S : Mirage_stack_lwt.V4) = struct
2 | let write_string flow s =
3 | let s = String.trim s ^ "\n" in
4 | let b = Cstruct.of_string s in
5 | match%lwt S.TCPV4.write flow b with
6 | | Ok () -> Lwt.return_unit
7 | | Error e ->
8 | Logs.warn (fun f ->
9 | f "Error occurred from writing to connection: %a" S.TCPV4.pp_write_error e );
10 | Lwt.return_unit
11 | ;;
12 |
13 | let read flow pushf =
14 | let dst, dst_port = S.TCPV4.dst flow in
15 | let dst = Ipaddr.V4.to_string dst in
16 | let rec aux () =
17 | match%lwt S.TCPV4.read flow with
18 | | Ok `Eof | Error _ -> Lwt.return_unit
19 | | Ok (`Data b) ->
20 | let s = Cstruct.to_string b in
21 | Logs.debug (fun f -> f "Read <- %s:%d : %s" dst dst_port s);
22 | String.iter (fun c -> pushf (Some c)) s;
23 | aux ()
24 | in
25 | aux ()
26 | ;;
27 |
28 | let mvar = Lwt_mvar.create_empty ()
29 |
30 | let write flow out_stream =
31 | let dst, dst_port = S.TCPV4.dst flow in
32 | let dst = Ipaddr.V4.to_string dst in
33 | let rec aux () =
34 | match%lwt Lwt_stream.get out_stream with
35 | | Some s ->
36 | Logs.debug (fun f -> f "Send -> %s:%d : %s" dst dst_port s);
37 | let%lwt () = write_string flow s in
38 | aux ()
39 | | None ->
40 | let%lwt () = Lwt_mvar.put mvar true in
41 | Lwt.return_unit
42 | in
43 | aux ()
44 | ;;
45 |
46 | let on_connect hostname flow =
47 | let dst, dst_port = S.TCPV4.dst flow in
48 | Logs.info (fun f ->
49 | f "New tcp connection from IP %s on port %d" (Ipaddr.V4.to_string dst) dst_port
50 | );
51 | let instream, infun = Lwt_stream.create () in
52 | Lwt.async (fun () -> read flow infun);
53 | let outstream, outfun = Lwt_stream.create () in
54 | Lwt.async (fun () -> write flow outstream);
55 | let handler =
56 | Mirage_xmpp.Handler.create ~stream:instream ~callback:outfun ~hostname
57 | in
58 | let%lwt () = Mirage_xmpp.Handler.handle handler in
59 | let%lwt _ = Lwt_mvar.take mvar in
60 | Logs.info (fun f ->
61 | f
62 | "Closing tcp connection from IP %s on port %d"
63 | (Ipaddr.V4.to_string dst)
64 | dst_port );
65 | S.TCPV4.close flow
66 | ;;
67 |
68 | let start stack =
69 | Logs.info (fun f -> f "Started Unikernel");
70 | let port = Key_gen.port () in
71 | let hostname = Key_gen.hostname () in
72 | Logs.info (fun f -> f "Port is: %d" port);
73 | Logs.info (fun f -> f "Hostname is: %s" hostname);
74 | S.listen_tcpv4 stack ~port (on_connect hostname);
75 | S.listen_tcpv4 stack ~port:8081 (fun _flow ->
76 | Logs.info (fun f -> f "Received exit signal");
77 | exit 0 );
78 | Logs.info (fun f -> f "Started listening");
79 | S.listen stack
80 | ;;
81 | end
82 |
--------------------------------------------------------------------------------
/pages/_config.yml:
--------------------------------------------------------------------------------
1 | # Welcome to Jekyll!
2 | #
3 | # This config file is meant for settings that affect your whole blog, values
4 | # which you are expected to set up once and rarely edit after that. If you find
5 | # yourself editing this file very often, consider using Jekyll's data files
6 | # feature for the data you need to update frequently.
7 | #
8 | # For technical reasons, this file is *NOT* reloaded automatically when you use
9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process.
10 |
11 | # Site settings
12 | # These are used to personalize your new site. If you look in the HTML files,
13 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
14 | # You can create any custom variable you would like, and they will be accessible
15 | # in the templates via {{ site.myvariable }}.
16 | title: Jeffas | Mirage XMPP
17 | description: >- # this means to ignore newlines until "baseurl:"
18 | XMPP server implementation in OCaml for MirageOS
19 | baseurl: "/mirage-xmpp" # the subpath of your site, e.g. /blog
20 | url: "" # the base hostname & protocol for your site, e.g. http://example.com
21 | github_username: Jeffa5
22 |
23 | permalink: pretty
24 |
25 | # Build settings
26 | markdown: kramdown
27 | theme: minima
28 |
29 | # Exclude from processing.
30 | # The following items will not be processed, by default. Create a custom list
31 | # to override the default setting.
32 | # exclude:
33 | # - Gemfile
34 | # - Gemfile.lock
35 | # - node_modules
36 | # - vendor/bundle/
37 | # - vendor/cache/
38 | # - vendor/gems/
39 | # - vendor/ruby/
40 |
--------------------------------------------------------------------------------
/pages/_includes/footer.html:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/pages/_layouts/home.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
6 | {%- if page.title -%}
7 |
{{ page.title }}
8 | {%- endif -%}
9 |
10 | {{ content }}
11 |
12 |
--------------------------------------------------------------------------------
/pages/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 | title: Links
4 | ---
5 |
6 | # Check out the [docs!](docs)
7 |
8 | # And don't forget the [coverage!](coverage)
9 |
--------------------------------------------------------------------------------
/src/actions.ml:
--------------------------------------------------------------------------------
1 | open Sexplib.Std
2 |
3 | type handler_actions =
4 | | RESET_PARSER
5 | | EXIT
6 | [@@deriving sexp]
7 |
8 | type error_type =
9 | | Auth
10 | | Cancel
11 | | Continue
12 | | Modify
13 | | Wait
14 | [@@deriving sexp]
15 |
16 | type t =
17 | | SEND_STREAM_HEADER
18 | | SEND_STREAM_FEATURES_SASL
19 | | SEND_SASL_SUCCESS
20 | | SEND_STREAM_FEATURES
21 | | SESSION_START_SUCCESS of string
22 | | CLOSE
23 | | ERROR of string
24 | | SET_USER of string
25 | | SET_USER_ANON
26 | | SET_JID_RESOURCE of {id : string; resource : string option}
27 | | GET_ROSTER of string
28 | | SET_ROSTER of
29 | { id : string
30 | ; target : Jid.Bare.t
31 | ; handle : string
32 | ; groups : string list }
33 | | PUSH_ROSTER of {ato : Jid.t option; contact : Jid.t}
34 | | ADD_TO_CONNECTIONS
35 | | REMOVE_FROM_CONNECTIONS
36 | | SUBSCRIPTION_REQUEST of {ato : Jid.t; xml : Xml.t; from : Jid.t option}
37 | | UPDATE_PRESENCE of {status : Rosters.Presence.t; xml : Xml.t option}
38 | | SEND_PRESENCE_UPDATE of {from : Jid.t; xml : Xml.t option}
39 | | SEND_CURRENT_PRESENCE of Jid.t
40 | | IQ_ERROR of {error_type : error_type; error_tag : string; id : string}
41 | | MESSAGE of {ato : Jid.t; message : Xml.t}
42 | | ROSTER_REMOVE of {id : string; target : Jid.t}
43 | | SUBSCRIPTION_APPROVAL of {ato : Jid.t; xml : Xml.t; from : Jid.t option}
44 | | ROSTER_SET_FROM of Jid.t
45 | | PROBE_PRESENCE
46 | | SUBSCRIPTION_CANCELLATION of {user : Jid.t; force : bool}
47 | | SUBSCRIPTION_REMOVAL of {contact : Jid.t}
48 | | SEND_DATA of string
49 | [@@deriving sexp]
50 |
51 | let error_type_to_string = function
52 | | Auth -> "auth"
53 | | Cancel -> "cancel"
54 | | Continue -> "continue"
55 | | Modify -> "modify"
56 | | Wait -> "wait"
57 | ;;
58 |
59 | let to_string t = Sexplib.Sexp.to_string_hum @@ sexp_of_t t
60 |
--------------------------------------------------------------------------------
/src/actions.mli:
--------------------------------------------------------------------------------
1 | (** The module for actions generated by the state machine *)
2 |
3 | type handler_actions =
4 | | RESET_PARSER
5 | | EXIT
6 | [@@deriving sexp]
7 |
8 | type error_type =
9 | | Auth
10 | | Cancel
11 | | Continue
12 | | Modify
13 | | Wait
14 | [@@deriving sexp]
15 |
16 | val error_type_to_string : error_type -> string
17 |
18 | (** The type of actions, examples for now *)
19 | type t =
20 | | SEND_STREAM_HEADER
21 | | SEND_STREAM_FEATURES_SASL
22 | | SEND_SASL_SUCCESS
23 | | SEND_STREAM_FEATURES
24 | | SESSION_START_SUCCESS of string
25 | | CLOSE
26 | | ERROR of string
27 | | SET_USER of string
28 | | SET_USER_ANON
29 | | SET_JID_RESOURCE of {id : string; resource : string option}
30 | | GET_ROSTER of string
31 | | SET_ROSTER of
32 | { id : string
33 | ; target : Jid.Bare.t
34 | ; handle : string
35 | ; groups : string list }
36 | | PUSH_ROSTER of {ato : Jid.t option; contact : Jid.t}
37 | | ADD_TO_CONNECTIONS
38 | | REMOVE_FROM_CONNECTIONS
39 | | SUBSCRIPTION_REQUEST of {ato : Jid.t; xml : Xml.t; from : Jid.t option}
40 | | UPDATE_PRESENCE of {status : Rosters.Presence.t; xml : Xml.t option}
41 | | SEND_PRESENCE_UPDATE of {from : Jid.t; xml : Xml.t option}
42 | | SEND_CURRENT_PRESENCE of Jid.t
43 | | IQ_ERROR of {error_type : error_type; error_tag : string; id : string}
44 | | MESSAGE of {ato : Jid.t; message : Xml.t}
45 | | ROSTER_REMOVE of {id : string; target : Jid.t}
46 | | SUBSCRIPTION_APPROVAL of {ato : Jid.t; xml : Xml.t; from : Jid.t option}
47 | | ROSTER_SET_FROM of Jid.t
48 | | PROBE_PRESENCE
49 | | SUBSCRIPTION_CANCELLATION of {user : Jid.t; force : bool}
50 | | SUBSCRIPTION_REMOVAL of {contact : Jid.t}
51 | | SEND_DATA of string
52 | [@@deriving sexp]
53 |
54 | (** [to_string t] takes an action and returns its string representation *)
55 | val to_string : t -> string
56 |
--------------------------------------------------------------------------------
/src/connections.ml:
--------------------------------------------------------------------------------
1 | open Asetmap
2 | module Jid_map = Map.Make (Jid.Full)
3 |
4 | let mutex = Lwt_mutex.create ()
5 | let with_mutex f = Lwt_mutex.with_lock mutex f
6 | let t = ref Jid_map.empty
7 |
8 | let add jid (f : Actions.t option -> unit) =
9 | with_mutex (fun () ->
10 | t := Jid_map.add jid f !t;
11 | Lwt.return_unit )
12 | ;;
13 |
14 | let find jid = with_mutex (fun () -> Jid_map.find jid !t |> Lwt.return)
15 |
16 | let find_all bare_jid =
17 | with_mutex (fun () ->
18 | Jid_map.filter (fun fjid _ -> bare_jid = Jid.Full.to_bare fjid) !t
19 | |> Jid_map.to_list
20 | |> Lwt.return )
21 | ;;
22 |
23 | let remove jid =
24 | with_mutex (fun () ->
25 | t := Jid_map.remove jid !t;
26 | Lwt.return_unit )
27 | ;;
28 |
29 | let clear () =
30 | with_mutex (fun () ->
31 | t := Jid_map.empty;
32 | Lwt.return_unit )
33 | ;;
34 |
35 | let to_string () =
36 | with_mutex (fun () ->
37 | Jid_map.to_list !t
38 | |> Sexplib.Conv.sexp_of_list (fun jid_push ->
39 | Sexplib.Conv.sexp_of_pair
40 | Jid.Full.sexp_of_t
41 | Sexplib.Conv.sexp_of_fun
42 | jid_push )
43 | |> Sexplib.Sexp.to_string_hum
44 | |> Lwt.return )
45 | ;;
46 |
47 | let test_connections actions =
48 | let test =
49 | let%lwt () = clear () in
50 | let%lwt _ = actions () in
51 | let%lwt s = to_string () in
52 | let%lwt () = Lwt_io.printl s in
53 | Lwt_io.flush_all ()
54 | in
55 | Lwt_main.run test
56 | ;;
57 |
58 | let%expect_test "empty initially" =
59 | test_connections (fun () -> Lwt.return_unit);
60 | [%expect {| () |}]
61 | ;;
62 |
63 | let%expect_test "add one connection" =
64 | ( test_connections
65 | @@ fun () -> add (Jid.Full.of_string "juliet@im.example.com/balcony") (fun _ -> ()) );
66 | [%expect {| ((((juliet im.example.com) balcony) )) |}]
67 | ;;
68 |
69 | let%expect_test "add two connection" =
70 | ( test_connections
71 | @@ fun () ->
72 | let%lwt () = add (Jid.Full.of_string "juliet@im.example.com/balcony") (fun _ -> ()) in
73 | add (Jid.Full.of_string "romeo@home.elsewhere.com/ground") (fun _ -> ()) );
74 | [%expect
75 | {|
76 | ((((juliet im.example.com) balcony) )
77 | (((romeo home.elsewhere.com) ground) )) |}]
78 | ;;
79 |
80 | let%expect_test "find all matches bare jid" =
81 | ( test_connections
82 | @@ fun () ->
83 | let%lwt () = add (Jid.Full.of_string "juliet@im.example.com/balcony") (fun _ -> ()) in
84 | let%lwt connected_resources = find_all (Jid.Bare.of_string "juliet@im.example.com") in
85 | Lwt_list.iter_s
86 | (fun (target_jid, _) -> Lwt_io.printl (Jid.Full.to_string target_jid))
87 | connected_resources );
88 | [%expect
89 | {|
90 | juliet@im.example.com/balcony
91 | ((((juliet im.example.com) balcony) )) |}]
92 | ;;
93 |
--------------------------------------------------------------------------------
/src/connections.mli:
--------------------------------------------------------------------------------
1 | (** Need to store active connections in order to be able to send data to them from other users *)
2 |
3 | (** [add t j f] adds [j] and [f] to [t] and returns a new [t] with them added. [f] is the push function to the stream for that user *)
4 | val add : Jid.Full.t -> (Actions.t option -> unit) -> unit Lwt.t
5 |
6 | (** [find t j] returns the push function associated with the [j] in the connections map if it is present *)
7 | val find : Jid.Full.t -> (Actions.t option -> unit) option Lwt.t
8 |
9 | (** [find_all j] returns the list of jid * actions_push function pairs which correspond to the same bare jid as [j] *)
10 | val find_all : Jid.Bare.t -> (Jid.Full.t * (Actions.t option -> unit)) list Lwt.t
11 |
12 | (** [remove t j] removes the jid [j] entry from the table if present *)
13 | val remove : Jid.Full.t -> unit Lwt.t
14 |
15 | val to_string : unit -> string Lwt.t
16 | val clear : unit -> unit Lwt.t
17 |
--------------------------------------------------------------------------------
/src/dune:
--------------------------------------------------------------------------------
1 | (library
2 | (name mirage_xmpp)
3 | (public_name mirage-xmpp)
4 | (libraries lwt lwt.unix markup-lwt astring asetmap uuidm base64 sexplib)
5 | (inline_tests
6 | (flags -show-counts -strict))
7 | (preprocess
8 | (pps ppx_expect lwt_ppx ppx_deriving.std ppx_sexp_conv bisect_ppx
9 | -conditional)))
10 |
--------------------------------------------------------------------------------
/src/events.ml:
--------------------------------------------------------------------------------
1 | open Astring
2 | open Sexplib.Std
3 |
4 | type t =
5 | | STREAM_HEADER of {version : string}
6 | | SASL_AUTH of {user : string; password : string}
7 | | ANONYMOUS_SASL_AUTH
8 | | RESOURCE_BIND_SERVER_GEN of {id : string}
9 | | RESOURCE_BIND_CLIENT_GEN of {id : string; resource : string}
10 | | SESSION_START of string
11 | | STREAM_CLOSE
12 | | ERROR of string
13 | | ROSTER_GET of string
14 | | ROSTER_SET of {id : string; target : Jid.t; handle : string; groups : string list}
15 | | ROSTER_REMOVE of {id : string; target : Jid.t}
16 | | PRESENCE_UPDATE of {status : Rosters.Presence.t; xml : Xml.t option}
17 | | IQ_ERROR of {error_type : Actions.error_type; error_tag : string; id : string}
18 | | MESSAGE of {ato : Jid.t; message : Xml.t}
19 | | LOG_OUT
20 | | NOOP
21 | | SUBSCRIPTION_REQUEST of {ato : Jid.t; xml : Xml.t}
22 | | SUBSCRIPTION_APPROVAL of {ato : Jid.t; xml : Xml.t}
23 | | SUBSCRIPTION_CANCELLATION of {user : Jid.t}
24 | | SUBSCRIPTION_REMOVAL of {contact : Jid.t}
25 | [@@deriving sexp]
26 |
27 | let to_string t = Sexplib.Sexp.to_string_hum @@ sexp_of_t t
28 | let not_implemented = ERROR "not implemented"
29 |
30 | let lift_iq = function
31 | | Xml.Element (((_prefix, _name), attributes), children) ->
32 | (match Stanza.get_type attributes with
33 | | Some "set" ->
34 | (match children with
35 | | [Xml.Element (((_p, "bind"), _attrs), [])] ->
36 | (* resource bind with server-generated resource identifier (7.6) *)
37 | RESOURCE_BIND_SERVER_GEN {id = Stanza.get_id_exn attributes}
38 | | [Xml.Element (((_p, "bind"), _attrs), [child])] ->
39 | (match child with
40 | | Xml.Element (((_, "resource"), []), [Xml.Text resource]) ->
41 | RESOURCE_BIND_CLIENT_GEN {id = Stanza.get_id_exn attributes; resource}
42 | | _ -> ERROR "Unexpected child of resource bind")
43 | | [ Xml.Element
44 | ( ((_, "query"), [(_, Xml.Xmlns "jabber:iq:roster")])
45 | , [Xml.Element (((_, "item"), attrs), group_elements)] ) ] ->
46 | (match Stanza.get_subscription attrs with
47 | | Some "remove" ->
48 | ROSTER_REMOVE {id = Stanza.get_id_exn attributes; target = Stanza.get_jid attrs}
49 | | _ ->
50 | let groups =
51 | List.map
52 | (fun element ->
53 | match element with
54 | | Xml.Element (((_, "group"), _), [Xml.Text group]) -> group
55 | | _ -> assert false )
56 | group_elements
57 | in
58 | let jid = Stanza.get_jid attrs in
59 | let handle =
60 | match Stanza.get_name attrs with Some name -> name | None -> ""
61 | in
62 | ROSTER_SET {id = Stanza.get_id_exn attributes; target = jid; handle; groups})
63 | | [Xml.Element (((_, "query"), [(_, Xml.Xmlns "jabber:iq:register")]), _)] ->
64 | let id = Stanza.get_id_exn attributes in
65 | IQ_ERROR {error_type = Actions.Cancel; error_tag = "feature-not-implemented"; id}
66 | | [Xml.Element (((_, "session"), _), [])] ->
67 | SESSION_START (Stanza.get_id_exn attributes)
68 | | _ ->
69 | ERROR
70 | ( "No children matched for iq of type set\n"
71 | ^ String.concat ~sep:"\nnext xml: "
72 | @@ List.map
73 | (function
74 | | Xml.Element _ as element -> "Element: " ^ Xml.to_string element
75 | | Xml.Text _ as text -> "Text: " ^ Xml.to_string text)
76 | children ))
77 | | Some "get" ->
78 | (match children with
79 | | [Xml.Element (((_, "query"), [(_, Xml.Xmlns "jabber:iq:roster")]), _)] ->
80 | (* roster get query *)
81 | ROSTER_GET (Stanza.get_id_exn attributes)
82 | | _ ->
83 | let id = Stanza.get_id_exn attributes in
84 | IQ_ERROR {error_type = Actions.Cancel; error_tag = "feature-not-implemented"; id})
85 | | Some "result" -> NOOP
86 | | _ -> ERROR "Type of iq expected to be 'set' or 'get'")
87 | | Xml.Text _t -> ERROR "Expected an iq stanza, not text"
88 | ;;
89 |
90 | let lift_presence = function
91 | | Xml.Element (((namespace, name), attributes), children) ->
92 | (match Stanza.get_type attributes with
93 | | Some "subscribe" ->
94 | let rec modify_to = function
95 | | [] -> []
96 | | (ns, Xml.To jid) :: attrs -> (ns, Xml.To (Jid.to_bare jid)) :: attrs
97 | | a :: attrs -> a :: modify_to attrs
98 | in
99 | let ato = Stanza.get_to attributes |> Jid.to_bare in
100 | SUBSCRIPTION_REQUEST
101 | {ato; xml = Xml.Element (((namespace, name), modify_to attributes), children)}
102 | | Some "subscribed" ->
103 | let rec modify_to = function
104 | | [] -> []
105 | | (ns, Xml.To jid) :: attrs -> (ns, Xml.To (Jid.to_bare jid)) :: attrs
106 | | a :: attrs -> a :: modify_to attrs
107 | in
108 | let ato = Stanza.get_to attributes |> Jid.to_bare in
109 | SUBSCRIPTION_APPROVAL
110 | {ato; xml = Xml.Element (((namespace, name), modify_to attributes), children)}
111 | | Some "unavailable" ->
112 | PRESENCE_UPDATE
113 | { status = Offline
114 | ; xml = Some (Xml.Element (((namespace, name), attributes), children)) }
115 | | Some "unsubscribed" ->
116 | SUBSCRIPTION_CANCELLATION {user = Stanza.get_to attributes |> Jid.to_bare}
117 | | Some "unsubscribe" ->
118 | SUBSCRIPTION_REMOVAL {contact = Stanza.get_to attributes |> Jid.to_bare}
119 | | None ->
120 | PRESENCE_UPDATE
121 | { status = Online
122 | ; xml = Some (Xml.Element (((namespace, name), attributes), children)) }
123 | | _ -> not_implemented)
124 | | Xml.Text _t -> ERROR "Expected a presence stanza, not text"
125 | ;;
126 |
127 | let lift_message = function
128 | | Xml.Element (((_prefix, _name), attributes), _children) as message ->
129 | let ato = Stanza.get_to attributes in
130 | let message = Xml.remove_prefixes message in
131 | MESSAGE {ato; message}
132 | | Xml.Text _t -> ERROR "Expected a message stanza, not text"
133 | ;;
134 |
135 | let lift parse_result =
136 | let open Parser in
137 | match parse_result with
138 | | Stanza stanza ->
139 | (match stanza with
140 | | Stanza.Iq element -> lift_iq element
141 | | Stanza.Presence element -> lift_presence element
142 | | Stanza.Message element -> lift_message element)
143 | | Sasl_auth xml ->
144 | let rec get_mechanism = function
145 | | [] -> raise Not_found
146 | | (_, Xml.Mechanism mechanism) :: _ -> mechanism
147 | | _ :: attrs -> get_mechanism attrs
148 | in
149 | let invalid_mechanism () =
150 | ERROR
151 | ( Xml.to_string
152 | @@ Xml.create
153 | ~children:[Xml.create (("", "invalid-mechanism"), [])]
154 | (("", "failure"), ["", Xml.Xmlns "urn:ietf:params:xml:ns:xmpp-sasl"]) )
155 | in
156 | (match xml with
157 | | Element ((_name, attributes), [Text b64_string]) ->
158 | if get_mechanism attributes = "PLAIN"
159 | then
160 | match Base64.decode b64_string with
161 | | Ok decoded_string ->
162 | (match String.cut ~sep:"\000" (String.trim decoded_string) with
163 | | Some (_userdom, userpass) ->
164 | (match String.cut ~sep:"\000" userpass with
165 | | Some (user, pass) -> SASL_AUTH {user; password = pass}
166 | | None -> ERROR "SASL: couldn't find second 0 byte")
167 | | _ -> ERROR "SASL: couldn't find first 0 byte")
168 | | Error e -> (match e with `Msg e -> ERROR e)
169 | else invalid_mechanism ()
170 | | Element ((_, attributes), []) ->
171 | if get_mechanism attributes = "ANONYMOUS"
172 | then ANONYMOUS_SASL_AUTH
173 | else invalid_mechanism ()
174 | | _ -> invalid_mechanism ())
175 | | Stream_Element stream_element ->
176 | (match stream_element with
177 | | Header (_name, attributes) ->
178 | let version = Stanza.get_version attributes in
179 | STREAM_HEADER {version}
180 | | Features -> not_implemented
181 | | Error -> ERROR "Stream level error"
182 | | Close -> STREAM_CLOSE)
183 | | Error e -> ERROR e
184 | ;;
185 |
186 | let%expect_test "lift error gives error" =
187 | let event = lift (Error "some error") in
188 | print_endline (to_string event);
189 | [%expect {| (ERROR "some error") |}]
190 | ;;
191 |
192 | let%expect_test "iq get" =
193 | let event =
194 | lift
195 | (Stanza
196 | (Stanza.Iq
197 | (Element
198 | ( ( ("", "iq")
199 | , [ "", Xml.From (Jid.of_string "juliet@capulet.com/balcony")
200 | ; "", Xml.Id "h83vxa4c"
201 | ; "", Xml.Type "get" ] )
202 | , [Xml.Element ((("", "query"), ["", Xml.Xmlns "jabber:iq:roster"]), [])]
203 | ))))
204 | in
205 | print_endline (to_string event);
206 | [%expect {| (ROSTER_GET h83vxa4c) |}]
207 | ;;
208 |
209 | let%expect_test "iq set" =
210 | let event =
211 | lift
212 | (Stanza
213 | (Stanza.Iq
214 | (Element
215 | ( (("", "iq"), ["", Xml.Id "l3b1vs75"; "", Xml.Type "set"])
216 | , [Xml.Element ((("", "bind"), []), [])] ))))
217 | in
218 | print_endline (to_string event);
219 | [%expect {| (RESOURCE_BIND_SERVER_GEN (id l3b1vs75)) |}]
220 | ;;
221 |
222 | let%expect_test "roster get" =
223 | let event =
224 | lift
225 | (Stanza
226 | (Stanza.Iq
227 | (Element
228 | ( ( ("", "iq")
229 | , [ "", Xml.From (Jid.of_string "juliet@example.com/balony")
230 | ; "", Xml.Id "bv1bs71f"
231 | ; "", Xml.Type "get" ] )
232 | , [Xml.Element ((("", "query"), ["", Xml.Xmlns "jabber:iq:roster"]), [])]
233 | ))))
234 | in
235 | print_endline (to_string event);
236 | [%expect {| (ROSTER_GET bv1bs71f) |}]
237 | ;;
238 |
239 | let%expect_test "roster set" =
240 | let event =
241 | lift
242 | (Stanza
243 | (Stanza.Iq
244 | (Element
245 | ( ( ("", "iq")
246 | , [ "", Xml.From (Jid.of_string "juliet@example.com/balony")
247 | ; "", Xml.Id "rs1"
248 | ; "", Xml.Type "set" ] )
249 | , [ Xml.Element
250 | ( (("", "query"), ["", Xml.Xmlns "jabber:iq:roster"])
251 | , [ Xml.Element
252 | ( ( ("", "item")
253 | , [ "", Xml.Jid (Jid.of_string "nurse@example.com")
254 | ; "", Xml.Name "Nurse" ] )
255 | , [] ) ] ) ] ))))
256 | in
257 | print_endline (to_string event);
258 | [%expect
259 | {|
260 | (ROSTER_SET (id rs1) (target (Bare_JID (nurse example.com))) (handle Nurse)
261 | (groups ())) |}]
262 | ;;
263 |
--------------------------------------------------------------------------------
/src/events.mli:
--------------------------------------------------------------------------------
1 | (** The module to handle conversion of stanzas into events for the state machine *)
2 |
3 | (** The type of events, examples for now *)
4 | type t =
5 | | STREAM_HEADER of {version : string}
6 | | SASL_AUTH of {user : string; password : string}
7 | | ANONYMOUS_SASL_AUTH
8 | | RESOURCE_BIND_SERVER_GEN of {id : string}
9 | | RESOURCE_BIND_CLIENT_GEN of {id : string; resource : string}
10 | | SESSION_START of string
11 | | STREAM_CLOSE
12 | | ERROR of string
13 | | ROSTER_GET of string
14 | | ROSTER_SET of {id : string; target : Jid.t; handle : string; groups : string list}
15 | | ROSTER_REMOVE of {id : string; target : Jid.t}
16 | | PRESENCE_UPDATE of {status : Rosters.Presence.t; xml : Xml.t option}
17 | | IQ_ERROR of {error_type : Actions.error_type; error_tag : string; id : string}
18 | | MESSAGE of {ato : Jid.t; message : Xml.t}
19 | | LOG_OUT
20 | | NOOP
21 | | SUBSCRIPTION_REQUEST of {ato : Jid.t; xml : Xml.t}
22 | | SUBSCRIPTION_APPROVAL of {ato : Jid.t; xml : Xml.t}
23 | | SUBSCRIPTION_CANCELLATION of {user : Jid.t}
24 | | SUBSCRIPTION_REMOVAL of {contact : Jid.t}
25 | [@@deriving sexp]
26 |
27 | (** [to_string t] takes an event and returns it's string representation *)
28 | val to_string : t -> string
29 |
30 | (** [lift pr] converts the parse_result [pr] into an event type suitable for sending to the state machine *)
31 | val lift : Parser.parse_result -> t
32 |
--------------------------------------------------------------------------------
/src/handler.mli:
--------------------------------------------------------------------------------
1 | (* This will contain the parser type, roster type and connections type *)
2 |
3 | (** The type of an XMPP handler. *)
4 | type t
5 | (* The idea is that this will be called when the on_connect triggers in the unikernel so this will then handle *)
6 |
7 | (* When called this has a few jobs:
8 | - create the parser with the given stream
9 | - initialise the new handler type with the parameters
10 | *)
11 |
12 | (** [create c r s f] creates a new handler. [c] is a connections table of the currently active connections to the server. [r] is the roster for the server. [s] is a stream to receive the incoming data on. [f] is a callback function which can be used to send data back to the user *)
13 | val create :
14 | stream:char Lwt_stream.t -> callback:(string option -> unit) -> hostname:string -> t
15 |
16 | (** [handle t] takes the handler and starts handling the XMPP connection with the client.
17 |
18 | This controls the main operation of the server:
19 | - call parse_stanza on the parser which will return a new stanza
20 | - translates the received stanza into an event type and pass this to the fsm to get a new fsm
21 | - take the actions from the fsm and push necessary data to the callback function, handle roster events and perform lookups on the connections in order to send data to other users
22 | - call parse_stanza again and repeat
23 | *)
24 | val handle : t -> unit Lwt.t
25 |
26 | val to_string : t -> string
27 |
--------------------------------------------------------------------------------
/src/jid.ml:
--------------------------------------------------------------------------------
1 | open Astring
2 | open Sexplib.Std
3 |
4 | exception MalformedJID of string
5 |
6 | module Bare = struct
7 | type t = string * string [@@deriving sexp, ord]
8 |
9 | let set_resource resource bare_jid = bare_jid, resource
10 |
11 | exception MalformedBareJID of string
12 |
13 | let of_string str =
14 | match String.cut ~sep:"@" str with
15 | | Some (user, domres) ->
16 | (match String.cut ~sep:"/" domres with
17 | | Some _ -> raise @@ MalformedBareJID str
18 | | None -> user, domres)
19 | | None -> raise @@ MalformedJID str
20 | ;;
21 | end
22 |
23 | module Full = struct
24 | type t = (string * string) * string [@@deriving sexp, ord]
25 |
26 | let to_bare (bare_jid, _) = bare_jid
27 | let set_resource resource (bare_jid, _) = bare_jid, resource
28 |
29 | exception MalformedFullJID of string
30 |
31 | let of_string str =
32 | match String.cut ~sep:"@" str with
33 | | Some (user, domres) ->
34 | (match String.cut ~sep:"/" domres with
35 | | Some (domain, resource) -> (user, domain), resource
36 | | None -> raise @@ MalformedFullJID str)
37 | | None -> raise @@ MalformedJID str
38 | ;;
39 |
40 | let to_string ((user, domain), resource) = user ^ "@" ^ domain ^ "/" ^ resource
41 | end
42 |
43 | module Domain = struct
44 | type t = string [@@deriving sexp]
45 | end
46 |
47 | type t =
48 | | Full_JID of Full.t
49 | | Bare_JID of Bare.t
50 | | Domain of Domain.t
51 | [@@deriving sexp]
52 |
53 | let to_bare_raw = function
54 | | Full_JID (bare_jid, _) -> bare_jid
55 | | Bare_JID bare_jid -> bare_jid
56 | | Domain dom ->
57 | raise @@ MalformedJID ("Not allowed to convert a Domain to a raw jid: " ^ dom)
58 | ;;
59 |
60 | let to_bare = function
61 | | Full_JID (bare_jid, _) -> Bare_JID bare_jid
62 | | Bare_JID bare_jid -> Bare_JID bare_jid
63 | | Domain dom ->
64 | raise @@ MalformedJID ("Not allowed to convert a Domain to a raw jid: " ^ dom)
65 | ;;
66 |
67 | let anon () = "anon-" ^ Uuidm.(to_string (create `V4))
68 |
69 | let of_string str =
70 | match String.cut ~sep:"@" str with
71 | | Some (user, domres) ->
72 | (match String.cut ~sep:"/" domres with
73 | | Some (domain, resource) -> Full_JID ((user, domain), resource)
74 | | None -> Bare_JID (user, domres))
75 | | None -> Domain str
76 | ;;
77 |
78 | let create_resource () = Uuidm.(to_string (create `V4))
79 |
80 | let set_resource resource = function
81 | | Full_JID fjid -> Full_JID (Full.set_resource resource fjid)
82 | | Bare_JID bjid -> Full_JID (Bare.set_resource resource bjid)
83 | | Domain dom ->
84 | raise @@ MalformedJID ("Not allowed to set resource on a Domain: " ^ dom)
85 | ;;
86 |
87 | let to_string = function
88 | | Full_JID ((user, domain), resource) -> user ^ "@" ^ domain ^ "/" ^ resource
89 | | Bare_JID (user, domain) -> user ^ "@" ^ domain
90 | | Domain dom -> dom
91 | ;;
92 |
93 | let%expect_test "make jid" =
94 | let jid = of_string "user@domain/resource" in
95 | print_endline (to_string jid);
96 | [%expect {| user@domain/resource |}]
97 | ;;
98 |
99 | let%expect_test "no resource in jid" =
100 | let jid = of_string "user@domain" in
101 | print_endline (to_string jid);
102 | [%expect {| user@domain |}]
103 | ;;
104 |
--------------------------------------------------------------------------------
/src/jid.mli:
--------------------------------------------------------------------------------
1 | (** The type of a Jabber ID *)
2 |
3 | module Bare : sig
4 | type t [@@deriving sexp, ord]
5 |
6 | val of_string : string -> t
7 | end
8 |
9 | module Full : sig
10 | type t [@@deriving sexp, ord]
11 |
12 | val to_bare : t -> Bare.t
13 | val of_string : string -> t
14 | val to_string : t -> string
15 | val set_resource : string -> t -> t
16 | end
17 |
18 | module Domain : sig
19 | type t [@@deriving sexp]
20 | end
21 |
22 | type t =
23 | | Full_JID of Full.t
24 | | Bare_JID of Bare.t
25 | | Domain of Domain.t
26 | [@@deriving sexp]
27 |
28 | val set_resource : string -> t -> t
29 | val anon : unit -> string
30 | val to_bare_raw : t -> Bare.t
31 | val to_bare : t -> t
32 |
33 | (** [of_string s] creates a new jid from the string, splitting it appropriately *)
34 | val of_string : string -> t
35 |
36 | (** [to_string t] returns the string representation of t *)
37 | val to_string : t -> string
38 |
39 | val create_resource : unit -> string
40 |
--------------------------------------------------------------------------------
/src/parser.ml:
--------------------------------------------------------------------------------
1 | open Ppx_sexp_conv_lib
2 | open Conv
3 |
4 | type t =
5 | { raw_stream : char Lwt_stream.t sexp_opaque
6 | ; stream : (Markup.signal, Markup.async) Markup.stream sexp_opaque
7 | ; mutable depth : int }
8 | [@@deriving sexp]
9 |
10 | type parse_result =
11 | | Stanza of Stanza.t
12 | | Sasl_auth of Xml.t
13 | | Stream_Element of Stream.t
14 | | Error of string
15 |
16 | exception ParsingError of string
17 |
18 | let make_parser stream =
19 | Markup_lwt.parse_xml
20 | ~report:(fun _ e ->
21 | let error_string = Markup.Error.to_string e in
22 | Lwt.fail (ParsingError error_string) )
23 | stream
24 | ;;
25 |
26 | let create raw_stream =
27 | let stream = Markup_lwt.lwt_stream raw_stream |> make_parser |> Markup.signals in
28 | {raw_stream; stream; depth = 0}
29 | ;;
30 |
31 | let reset parser =
32 | { raw_stream = parser.raw_stream
33 | ; stream = Markup_lwt.lwt_stream parser.raw_stream |> make_parser |> Markup.signals
34 | ; depth = 0 }
35 | ;;
36 |
37 | let convert_attribute ((namespace, name), value) =
38 | let open Xml in
39 | ( namespace
40 | , match name with
41 | | "from" -> From (Jid.of_string value)
42 | | "to" -> To (Jid.of_string value)
43 | | "id" -> Id value
44 | | "jid" -> Jid (Jid.of_string value)
45 | | "xmlns" -> Xmlns value
46 | | "type" -> Type value
47 | | "ver" -> Ver value
48 | | "version" -> Version value
49 | | "lang" -> Lang value
50 | | "stream" -> Stream value
51 | | "name" -> Name value
52 | | "subscription" -> Subscription value
53 | | "mechanism" -> Mechanism value
54 | | _ -> Other (name, value) )
55 | ;;
56 |
57 | let convert_attributes attributes =
58 | List.map (fun attr -> convert_attribute attr) attributes
59 | ;;
60 |
61 | let rec parse_children parser =
62 | match%lwt Markup_lwt.next parser.stream with
63 | | exception ParsingError e -> Lwt.return_error e
64 | | Some signal ->
65 | (match signal with
66 | | `Start_element (name, attributes) ->
67 | let tag = name, convert_attributes attributes in
68 | (match%lwt parse_children parser with
69 | | Ok children ->
70 | let element = Xml.Element (tag, children) in
71 | (match%lwt parse_children parser with
72 | | Ok element_list -> Lwt.return_ok (element :: element_list)
73 | | Error e -> Lwt.return_error e)
74 | | Error e -> Lwt.return_error e)
75 | | `End_element -> Lwt.return_ok []
76 | | `Text ss ->
77 | (match String.trim (String.concat "\n" ss) with
78 | | "" -> parse_children parser
79 | | _ ->
80 | let text = Xml.Text (String.concat "\n" ss) in
81 | (match%lwt parse_children parser with
82 | | Ok element_list -> Lwt.return_ok (text :: element_list)
83 | | Error e -> Lwt.return_error e))
84 | | _ -> assert false)
85 | | None -> Lwt.return_error "End of parsing stream"
86 | ;;
87 |
88 | let rec parse parser =
89 | match%lwt Markup_lwt.next parser.stream with
90 | | exception ParsingError e -> Lwt.return (Error e)
91 | | Some signal ->
92 | (match signal with
93 | | `Start_element ((namespace, name), attrs) ->
94 | let tag = (namespace, name), convert_attributes attrs in
95 | (match parser.depth with
96 | | 0 ->
97 | (* start of stream *)
98 | (* check it actually is a stream tag *)
99 | if name = "stream"
100 | then (
101 | parser.depth <- 1;
102 | Lwt.return (Stream_Element (Stream.Header tag)) )
103 | else
104 | Lwt.return
105 | (Error
106 | ("Invalid initial stanza with name " ^ name ^ ", expected stream header."))
107 | | 1 ->
108 | (* parse stanza / error / feature *)
109 | (match name with
110 | | "iq" ->
111 | (match%lwt parse_children parser with
112 | | Ok children -> Lwt.return (Stanza (Stanza.Iq (Xml.Element (tag, children))))
113 | | Error e -> Lwt.return (Error e))
114 | | "message" ->
115 | (match%lwt parse_children parser with
116 | | Ok children ->
117 | Lwt.return (Stanza (Stanza.Message (Xml.Element (tag, children))))
118 | | Error e -> Lwt.return (Error e))
119 | | "presence" ->
120 | (match%lwt parse_children parser with
121 | | Ok children ->
122 | Lwt.return (Stanza (Stanza.Presence (Xml.Element (tag, children))))
123 | | Error e -> Lwt.return (Error e))
124 | | "auth" ->
125 | (match%lwt parse_children parser with
126 | | Ok children -> Lwt.return (Sasl_auth (Xml.Element (tag, children)))
127 | | Error e -> Lwt.return (Error e))
128 | | "stream" ->
129 | parser.depth <- 1;
130 | Lwt.return (Stream_Element (Stream.Header tag))
131 | | "error" -> Lwt.return (Stream_Element Stream.Error)
132 | | s -> Lwt.return (Error ("Unexpected tag with name: " ^ s)))
133 | | _ -> assert false)
134 | | `End_element ->
135 | (match parser.depth with
136 | | 1 -> (* End of the stream *)
137 | Lwt.return (Stream_Element Stream.Close)
138 | | _ -> Lwt.return (Error "Unexpected end element in parser"))
139 | | `Text ss ->
140 | (match String.trim (String.concat "" ss) with
141 | | "" -> parse parser
142 | | _ -> Lwt.return (Error ("Unexpected Text: " ^ String.concat "\n" ss)))
143 | | `Xml _declaration ->
144 | (* Xml declaration is optional so we can just ignore it as there is nothing to do with it *)
145 | parse parser
146 | | `Doctype _doctype -> Lwt.return (Error "Unexpected Doctype")
147 | | `PI (s1, s2) -> Lwt.return (Error ("Unexpected PI: " ^ s1 ^ ", " ^ s2))
148 | | `Comment s -> Lwt.return (Error ("Unexpected Comment: " ^ s)))
149 | | None -> Lwt.return (Error "End of parsing stream")
150 | ;;
151 |
152 | let parse_string s =
153 | let parser = create (Lwt_stream.of_string s) in
154 | let out () =
155 | match%lwt parse parser with
156 | | Stanza s ->
157 | print_endline (Stanza.to_string s);
158 | Lwt.return_unit
159 | | Sasl_auth xml ->
160 | print_endline ("Sasl_auth\n" ^ Xml.to_string xml);
161 | Lwt.return_unit
162 | | Stream_Element stream_element ->
163 | print_endline ("Stream_Element\n" ^ Stream.to_string stream_element);
164 | Lwt.return_unit
165 | | Error e ->
166 | print_endline e;
167 | Lwt.return_unit
168 | in
169 | fun () -> Lwt_main.run (out ())
170 | ;;
171 |
172 | let%expect_test "initial stanza gets returned" =
173 | let pf = parse_string "" in
174 | pf ();
175 | [%expect {|
176 | Stream_Element
177 | |}];
178 | pf ();
179 | [%expect {|
180 | Stream_Element
181 | |}]
182 | ;;
183 |
184 | let%expect_test "non empty stanza is ok" =
185 | let pf = parse_string "A message!" in
186 | pf ();
187 | [%expect {|
188 | Stream_Element
189 | |}];
190 | pf ();
191 | [%expect {|
192 | A message! |}];
193 | pf ();
194 | [%expect {|
195 | Stream_Element
196 | |}]
197 | ;;
198 |
199 | let%expect_test "start end full" =
200 | let pf =
201 | parse_string
202 | "text"
205 | in
206 | pf ();
207 | [%expect
208 | {|
209 | Stream_Element
210 | |}];
211 | pf ();
212 | [%expect
213 | {|
214 | text |}];
215 | pf ();
216 | [%expect {|
217 | Stream_Element
218 | |}];
219 | pf ();
220 | [%expect {| End of parsing stream |}]
221 | ;;
222 |
223 | let%expect_test "resource binding" =
224 | let pf =
225 | parse_string
226 | "balcony"
231 | in
232 | pf ();
233 | [%expect
234 | {|
235 | Stream_Element
236 | |}];
237 | pf ();
238 | [%expect
239 | {|
240 | balcony |}];
241 | pf ();
242 | [%expect {|
243 | Stream_Element
244 | |}]
245 | ;;
246 |
247 | let%expect_test "invalid xml" =
248 | let pf = parse_string "" in
249 | pf ();
250 | [%expect {|
251 | Stream_Element
252 | |}];
253 | pf ();
254 | [%expect {| unmatched start tag 'iq' |}]
255 | ;;
256 |
257 | let%expect_test "whitespace between elements" =
258 | let pf = parse_string " \n " in
259 | pf ();
260 | [%expect {|
261 | Stream_Element
262 | |}];
263 | pf ();
264 | [%expect {|
265 | |}]
266 | ;;
267 |
268 | let%expect_test "non-whitespace between elements" =
269 | let pf =
270 | parse_string "invalid string n more invalid stuff"
271 | in
272 | pf ();
273 | [%expect {|
274 | Stream_Element
275 | |}];
276 | pf ();
277 | [%expect {|
278 | Unexpected Text: invalid string |}]
279 | ;;
280 |
--------------------------------------------------------------------------------
/src/parser.mli:
--------------------------------------------------------------------------------
1 | (** The type of a parser *)
2 | type t [@@deriving sexp]
3 |
4 | type parse_result =
5 | | Stanza of Stanza.t
6 | | Sasl_auth of Xml.t
7 | | Stream_Element of Stream.t
8 | | Error of string
9 |
10 | (** [create s] creates a new parser from the given stream of input characters *)
11 | val create : char Lwt_stream.t -> t
12 |
13 | val reset : t -> t
14 |
15 | (** [parse_stanza t] will act similarly to parse_xml apart from that it returns a full stanza or in the case of a start of stream, it returns a near-complete stanza *)
16 | val parse : t -> parse_result Lwt.t
17 |
--------------------------------------------------------------------------------
/src/rosters.mli:
--------------------------------------------------------------------------------
1 | (** A roster stores information about contacts for a particular user, it also stores information about subscriptions to the user's presence. *)
2 |
3 | module Subscription : sig
4 | type t =
5 | | None
6 | | To
7 | | From
8 | | Both
9 | | Remove
10 |
11 | val to_string : t -> string
12 | end
13 |
14 | module Presence : sig
15 | type t =
16 | | Online
17 | | Offline
18 | [@@deriving sexp]
19 | end
20 |
21 | module Item : sig
22 | type t =
23 | { handle : string
24 | ; subscription : Subscription.t [@default (None : Subscription.t)]
25 | ; ask : bool [@default false]
26 | ; groups : string list }
27 | [@@deriving sexp, make]
28 |
29 | val to_tuple : t -> string * Subscription.t * bool * string list
30 | end
31 |
32 | (** [lock_user user] attempts to acquire a basic lock on the bare user to prevent others from using it. It returns [true] if it is successful in acquiring the lock and [false] otherwise. *)
33 | val lock_user : Jid.Bare.t -> bool Lwt.t
34 |
35 | (** [unlock_user user] unlocks the lock set by [lock_user]. *)
36 | val unlock_user : Jid.Bare.t -> unit Lwt.t
37 |
38 | (** [remove_item user contact] removes the [contact] from the [user]'s roster. *)
39 | val remove_item : Jid.Bare.t -> Jid.Bare.t -> unit Lwt.t
40 |
41 | (** [downgrade_subscription_to user contact] removes the {e to} part of the presence subscription from [user] to [contact]. *)
42 | val downgrade_subscription_to : Jid.Bare.t -> Jid.Bare.t -> unit Lwt.t
43 |
44 | (** [downgrade_subscription_from user contact] removes the {e from} part of the presence subscription from [user] to [contact]. *)
45 | val downgrade_subscription_from : Jid.Bare.t -> Jid.Bare.t -> unit Lwt.t
46 |
47 | (** [upgrade_subscription_to user contact] adds the {e to} part of the presence subscription from [user] to [contact]. *)
48 | val upgrade_subscription_to : Jid.Bare.t -> Jid.Bare.t -> unit Lwt.t
49 |
50 | (** [upgrade_subscription_from user contact] adds the {e from} part of the presence subscription from [user] to [contact]. *)
51 | val upgrade_subscription_from : Jid.Bare.t -> Jid.Bare.t -> unit Lwt.t
52 |
53 | (** [unset_ask user contact] sets the [ask] value to false for the [contact] in the [user]'s roster. *)
54 | val unset_ask : Jid.Bare.t -> Jid.Bare.t -> unit Lwt.t
55 |
56 | (** [set_ask user contact] sets the [ask] value to true for the [contact] in the [user]'s roster. *)
57 | val set_ask : Jid.Bare.t -> Jid.Bare.t -> unit Lwt.t
58 |
59 | (** [set_item ~subscription ~handle ~groups user contact] sets the item for [contact] in the [user]'s roster either by creating a new item and inserting it or updating an existing item. The default for [subscription] is [None], for [handle] is [""] and for [groups] is [[]]. *)
60 | val set_item :
61 | ?subscription:Subscription.t
62 | -> ?handle:string
63 | -> ?groups:string list
64 | -> Jid.Bare.t
65 | -> Jid.Bare.t
66 | -> Item.t Lwt.t
67 |
68 | (** [get_presence user] gets the current presence status for the [user]. *)
69 | val get_presence : Jid.Bare.t -> Presence.t Lwt.t
70 |
71 | val set_presence : Jid.Bare.t -> Presence.t -> unit Lwt.t
72 |
73 | (** [get_ask user contact] gets the current status of a presence subscription ask from [user] to [contact]. *)
74 | val get_ask : Jid.Bare.t -> Jid.Bare.t -> bool option Lwt.t
75 |
76 | (** [get_subscription user contact] gets the subscription from [user] to [contact] if there is one. *)
77 | val get_subscription : Jid.Bare.t -> Jid.Bare.t -> Subscription.t option Lwt.t
78 |
79 | (** [get_item user contact] get the item associated with the [contact] in the [user]'s roster if there is one. *)
80 | val get_item : Jid.Bare.t -> Jid.Bare.t -> Item.t option Lwt.t
81 |
82 | (** [get_items user] returns a list of [(contact, item)] pairs from the [user]'s roster. *)
83 | val get_items : Jid.Bare.t -> (Jid.Bare.t * Item.t) list Lwt.t
84 |
85 | (** [get_subscriptions user] gets the list of [contact]s where the [user] has a subscription to the [contact]. *)
86 | val get_subscriptions : Jid.Bare.t -> Jid.Bare.t list Lwt.t
87 |
88 | (** [get_subscribers user] gets the list of [contact]s where the [contact] has a subscription to the [user]. *)
89 | val get_subscribers : Jid.Bare.t -> Jid.Bare.t list Lwt.t
90 |
91 | (** [to_string ()] returns the string representation of the rosters. *)
92 | val to_string : unit -> string Lwt.t
93 |
94 | (** [clear ()] clears the roster. *)
95 | val clear : unit -> unit Lwt.t
96 |
--------------------------------------------------------------------------------
/src/stanza.ml:
--------------------------------------------------------------------------------
1 | type t =
2 | | Message of Xml.t
3 | | Presence of Xml.t
4 | | Iq of Xml.t
5 | [@@deriving sexp]
6 |
7 | let to_xml = function Message xml -> xml | Presence xml -> xml | Iq xml -> xml
8 | let gen_id () = Uuidm.(to_string (create `V4))
9 |
10 | let create_presence ?(attributes = []) ?atype ?ato ~id ~from children =
11 | let attributes =
12 | match atype with Some t -> ("", Xml.Type t) :: attributes | None -> attributes
13 | in
14 | let attributes =
15 | match id with Some i -> ("", Xml.Id i) :: attributes | None -> attributes
16 | in
17 | let attributes =
18 | match ato with Some ato -> ("", Xml.To ato) :: attributes | None -> attributes
19 | in
20 | Presence (Element ((("", "presence"), ["", Xml.From from] @ attributes), children))
21 | ;;
22 |
23 | let create_iq ?(attributes = []) ?ato ~atype ~id children =
24 | let attributes =
25 | match ato with Some ato -> ("", Xml.To ato) :: attributes | None -> attributes
26 | in
27 | Iq (Element ((("", "iq"), ["", Xml.Id id; "", Xml.Type atype] @ attributes), children))
28 | ;;
29 |
30 | let create_iq_error ~from ?ato ~id ~error_type ~error_tag () =
31 | let attributes = match ato with Some target -> ["", Xml.To target] | None -> [] in
32 | Iq
33 | (Element
34 | ( ( ("", "iq")
35 | , ["", Xml.From from; "", Xml.Id id; "", Xml.Type "error"] @ attributes )
36 | , [ Element
37 | ( (("", "error"), ["", Xml.Type (Actions.error_type_to_string error_type)])
38 | , [ Element
39 | ( ( ("", error_tag)
40 | , ["", Xml.Xmlns "urn:ietf:params:xml:ns:xmpp-stanzas"] )
41 | , [] ) ] ) ] ))
42 | ;;
43 |
44 | let create_bind ?(attributes = []) children =
45 | Xml.create
46 | (("", "bind"), ["", Xml.Xmlns "urn:ietf:params:xml:ns:xmpp-bind"] @ attributes)
47 | ~children
48 | ;;
49 |
50 | let create_query children =
51 | Xml.create (("", "query"), ["", Xml.Xmlns "jabber:iq:roster"]) ~children
52 | ;;
53 |
54 | let create_resource ?(attributes = []) children =
55 | Xml.create (("", "resource"), attributes) ~children
56 | ;;
57 |
58 | let create_bind_result ~id ~jid () =
59 | create_iq
60 | ~id
61 | ~atype:"result"
62 | [create_bind [Xml.create (("", "jid"), []) ~children:[Xml.Text (Jid.to_string jid)]]]
63 | ;;
64 |
65 | let create_roster_get_result ~id ~ato items =
66 | create_iq
67 | ~id
68 | ~atype:"result"
69 | ~ato
70 | [ create_query
71 | (List.map
72 | (fun (jid, item) ->
73 | let handle, subscription, _ask, groups = Rosters.Item.to_tuple item in
74 | Xml.create
75 | ( ("", "item")
76 | , [ "", Xml.Jid (Bare_JID jid)
77 | ; "", Xml.Name handle
78 | ; "", Xml.Subscription (Rosters.Subscription.to_string subscription) ]
79 | )
80 | ~children:
81 | (List.map
82 | (fun group ->
83 | Xml.create (("", "group"), []) ~children:[Xml.Text group] )
84 | groups) )
85 | items) ]
86 | ;;
87 |
88 | let create_roster_set_result ~id ~ato = create_iq ~id ~atype:"result" ~ato []
89 |
90 | let create_roster_push ~id ~ato (jid, item) =
91 | let handle, subscription, _ask, groups = Rosters.Item.to_tuple item in
92 | let attributes = [] in
93 | let attributes =
94 | match handle with "" -> attributes | h -> ("", Xml.Name h) :: attributes
95 | in
96 | let attributes =
97 | ("", Xml.Subscription (Rosters.Subscription.to_string subscription)) :: attributes
98 | in
99 | create_iq
100 | ~id
101 | ~ato
102 | ~atype:"set"
103 | [ create_query
104 | [ Xml.create
105 | (("", "item"), ["", Xml.Jid jid] @ attributes)
106 | ~children:
107 | (List.map
108 | (fun group -> Xml.create (("", "group"), []) ~children:[Xml.Text group])
109 | groups) ] ]
110 | ;;
111 |
112 | let rec get_subscription = function
113 | | [] -> None
114 | | (_, Xml.Subscription sub) :: _ -> Some sub
115 | | _ :: attrs -> get_subscription attrs
116 | ;;
117 |
118 | let rec get_id_exn = function
119 | | [] -> raise Not_found
120 | | (_, Xml.Id id) :: _ -> id
121 | | _ :: attrs -> get_id_exn attrs
122 | ;;
123 |
124 | let rec get_id = function
125 | | [] -> None
126 | | (_, Xml.Id id) :: _ -> Some id
127 | | _ :: attrs -> get_id attrs
128 | ;;
129 |
130 | let rec get_from = function
131 | | [] -> raise Not_found
132 | | (_, Xml.From jid) :: _ -> jid
133 | | _ :: attrs -> get_from attrs
134 | ;;
135 |
136 | let rec get_to = function
137 | | [] -> raise Not_found
138 | | (_, Xml.To jid) :: _ -> jid
139 | | _ :: attrs -> get_to attrs
140 | ;;
141 |
142 | let rec get_type = function
143 | | [] -> None
144 | | (_, Xml.Type t) :: _ -> Some t
145 | | _ :: attrs -> get_type attrs
146 | ;;
147 |
148 | let rec get_version = function
149 | | [] -> raise Not_found
150 | | (_, Xml.Version v) :: _ -> v
151 | | _ :: attrs -> get_version attrs
152 | ;;
153 |
154 | let rec get_jid = function
155 | | [] -> raise Not_found
156 | | (_, Xml.Jid jid) :: _ -> jid
157 | | _ :: attrs -> get_jid attrs
158 | ;;
159 |
160 | let rec get_name = function
161 | | [] -> None
162 | | (_, Xml.Name name) :: _ -> Some name
163 | | _ :: attrs -> get_name attrs
164 | ;;
165 |
166 | let to_string = function
167 | | Message xml -> Xml.to_string xml
168 | | Presence xml -> Xml.to_string xml
169 | | Iq xml -> Xml.to_string xml
170 | ;;
171 |
--------------------------------------------------------------------------------
/src/stanza.mli:
--------------------------------------------------------------------------------
1 | (** The type of a Stanza. *)
2 | type t =
3 | | Message of Xml.t
4 | | Presence of Xml.t
5 | | Iq of Xml.t
6 |
7 | (** [to_xml t] returns the xml element contained within the stanza type [t]. *)
8 | val to_xml : t -> Xml.t
9 |
10 | (** [gen_id ()] generates a new string to use as an id. *)
11 | val gen_id : unit -> string
12 |
13 | (** [create_presence ~attributes ~atype ~ato ~id ~from children] creates a presence stanza with the given attributes and children. *)
14 | val create_presence :
15 | ?attributes:Xml.attribute list
16 | -> ?atype:string
17 | -> ?ato:Jid.t
18 | -> id:string option
19 | -> from:Jid.t
20 | -> Xml.t list
21 | -> t
22 |
23 | (** [create_iq ~attributes ~ato ~atype ~id children] creates an iq stanza with the given attributes and children. *)
24 | val create_iq :
25 | ?attributes:Xml.attribute list
26 | -> ?ato:Jid.t
27 | -> atype:string
28 | -> id:string
29 | -> Xml.t list
30 | -> t
31 |
32 | (** [create_iq_error ~from ~ato ~id ~error_type ~error_tag] creates an iq error stanza with the given attributes. *)
33 | val create_iq_error :
34 | from:Jid.t
35 | -> ?ato:Jid.t
36 | -> id:string
37 | -> error_type:Actions.error_type
38 | -> error_tag:string
39 | -> unit
40 | -> t
41 |
42 | (** [create_bind ~attributes children] creates an iq bind stanza with the given attributes and children. *)
43 | val create_bind : ?attributes:Xml.attribute list -> Xml.t list -> Xml.t
44 |
45 | (** [create_resource ~attributes children] creates a resource bind xml element with the given attributes and children. *)
46 | val create_resource : ?attributes:Xml.attribute list -> Xml.t list -> Xml.t
47 |
48 | val create_bind_result : id:string -> jid:Jid.t -> unit -> t
49 |
50 | val create_roster_get_result :
51 | id:string -> ato:Jid.t -> (Jid.Bare.t * Rosters.Item.t) list -> t
52 |
53 | val create_roster_set_result : id:string -> ato:Jid.t -> t
54 | val create_roster_push : id:string -> ato:Jid.t -> Jid.t * Rosters.Item.t -> t
55 |
56 | (** [to_string t] takes a stanza [t] and returns the string representation of it *)
57 | val to_string : t -> string
58 |
59 | val get_subscription : Xml.attribute list -> string option
60 | val get_id_exn : Xml.attribute list -> string
61 | val get_id : Xml.attribute list -> string option
62 | val get_from : Xml.attribute list -> Jid.t
63 | val get_to : Xml.attribute list -> Jid.t
64 | val get_type : Xml.attribute list -> string option
65 | val get_version : Xml.attribute list -> string
66 | val get_jid : Xml.attribute list -> Jid.t
67 | val get_name : Xml.attribute list -> string option
68 |
--------------------------------------------------------------------------------
/src/state.ml:
--------------------------------------------------------------------------------
1 | (* The state representing the current status of the connection *)
2 | open Events
3 |
4 | type state =
5 | | IDLE
6 | | SASL_NEGOTIATION
7 | | NEGOTIATING
8 | | CONNECTED
9 | | CLOSED
10 | [@@deriving sexp]
11 |
12 | type t = {state : state} [@@deriving sexp]
13 |
14 | let initial = {state = IDLE}
15 | let to_string t = Sexplib.Sexp.to_string_hum @@ sexp_of_t t
16 |
17 | let closed =
18 | ( {state = CLOSED}
19 | , [ Actions.UPDATE_PRESENCE {status = Rosters.Presence.Offline; xml = None}
20 | ; Actions.REMOVE_FROM_CONNECTIONS
21 | ; Actions.CLOSE ]
22 | , [Actions.EXIT] )
23 | ;;
24 |
25 | let closed_with_error e =
26 | ( {state = CLOSED}
27 | , [ Actions.UPDATE_PRESENCE {status = Rosters.Presence.Offline; xml = None}
28 | ; Actions.REMOVE_FROM_CONNECTIONS
29 | ; Actions.ERROR e ]
30 | , [Actions.EXIT] )
31 | ;;
32 |
33 | let handle_idle t = function
34 | | STREAM_HEADER {version} ->
35 | if float_of_string version >= 1.0
36 | then
37 | ( {state = SASL_NEGOTIATION}
38 | , [Actions.SEND_STREAM_HEADER; Actions.SEND_STREAM_FEATURES_SASL]
39 | , [] )
40 | else closed_with_error "Must use version >= 1.0"
41 | | SASL_AUTH _ -> closed_with_error "No stream"
42 | | ANONYMOUS_SASL_AUTH -> closed_with_error "No stream"
43 | | RESOURCE_BIND_SERVER_GEN _ -> closed_with_error "No stream"
44 | | RESOURCE_BIND_CLIENT_GEN _ -> closed_with_error "No stream"
45 | | SESSION_START _id -> closed_with_error "No stream"
46 | | STREAM_CLOSE -> closed_with_error "No stream"
47 | | ERROR e -> closed_with_error e
48 | | ROSTER_GET _ -> closed_with_error "No stream"
49 | | ROSTER_SET _ -> closed_with_error "No stream"
50 | | ROSTER_REMOVE _ -> closed_with_error "No stream"
51 | | SUBSCRIPTION_REQUEST _ -> closed_with_error "No stream"
52 | | PRESENCE_UPDATE _ -> closed_with_error "No stream"
53 | | IQ_ERROR _ -> closed_with_error "No stream"
54 | | MESSAGE _ -> closed_with_error "No stream"
55 | | LOG_OUT -> closed_with_error "No stream"
56 | | NOOP -> t, [], []
57 | | SUBSCRIPTION_APPROVAL _ -> closed_with_error "No stream"
58 | | SUBSCRIPTION_CANCELLATION _ -> closed_with_error "No stream"
59 | | SUBSCRIPTION_REMOVAL _ -> closed_with_error "No stream"
60 | ;;
61 |
62 | let handle_sasl_negotiation t = function
63 | | STREAM_HEADER _ ->
64 | closed_with_error "Unexpected stream header during sasl negotiation"
65 | | SASL_AUTH {user; _} ->
66 | ( {state = NEGOTIATING}
67 | , [Actions.SET_USER user; Actions.SEND_SASL_SUCCESS]
68 | , [Actions.RESET_PARSER] )
69 | | ANONYMOUS_SASL_AUTH ->
70 | ( {state = NEGOTIATING}
71 | , [Actions.SET_USER_ANON; Actions.SEND_SASL_SUCCESS]
72 | , [Actions.RESET_PARSER] )
73 | | RESOURCE_BIND_SERVER_GEN _ -> closed_with_error "Not finished SASL"
74 | | RESOURCE_BIND_CLIENT_GEN _ -> closed_with_error "Not finished SASL"
75 | | SESSION_START _id ->
76 | closed_with_error "Unexpected session start stanza during sasl negotiation"
77 | | STREAM_CLOSE -> closed_with_error "Unexpected stream close during sasl negotiation"
78 | | ERROR e -> closed_with_error e
79 | | ROSTER_GET _ -> closed_with_error "Unexpected roster get during sasl negotiation"
80 | | ROSTER_SET _ -> closed_with_error "Unexpected roster set during sasl negotiation"
81 | | ROSTER_REMOVE _ ->
82 | closed_with_error "Unexpected roster remove during sasl negotiation"
83 | | SUBSCRIPTION_REQUEST _ ->
84 | closed_with_error "Unexpected subscription request during sasl negotiation"
85 | | PRESENCE_UPDATE _ ->
86 | closed_with_error "Unexpected presence update during sasl negotiation"
87 | | IQ_ERROR {error_type; error_tag; id} ->
88 | {state = SASL_NEGOTIATION}, [Actions.IQ_ERROR {error_type; error_tag; id}], []
89 | | MESSAGE _ -> closed_with_error "Unexpected message during sasl negotiation"
90 | | LOG_OUT ->
91 | closed_with_error "Unexpected presence for log out during sasl negotiation"
92 | | NOOP -> t, [], []
93 | | SUBSCRIPTION_APPROVAL _ ->
94 | closed_with_error "Unexpected subscription approval during sasl negotiation"
95 | | SUBSCRIPTION_CANCELLATION _ ->
96 | closed_with_error "Unexpected subscription cancellation during sasl negotiation"
97 | | SUBSCRIPTION_REMOVAL _ ->
98 | closed_with_error "Unexpected subscription removal during sasl negotiation"
99 | ;;
100 |
101 | let just_connected actions =
102 | {state = CONNECTED}, [Actions.PROBE_PRESENCE; Actions.ADD_TO_CONNECTIONS] @ actions, []
103 | ;;
104 |
105 | let handle_negotiating t = function
106 | | STREAM_HEADER {version} ->
107 | if float_of_string version >= 1.0
108 | then
109 | ( {state = NEGOTIATING}
110 | , [Actions.SEND_STREAM_HEADER; Actions.SEND_STREAM_FEATURES]
111 | , [] )
112 | else closed_with_error "Must use version >= 1.0"
113 | | SASL_AUTH _ -> closed_with_error "Already negotiated sasl"
114 | | ANONYMOUS_SASL_AUTH -> closed_with_error "Already negotiated sasl"
115 | | RESOURCE_BIND_SERVER_GEN {id} ->
116 | {state = NEGOTIATING}, [Actions.SET_JID_RESOURCE {id; resource = None}], []
117 | | RESOURCE_BIND_CLIENT_GEN {id; resource} ->
118 | {state = NEGOTIATING}, [Actions.SET_JID_RESOURCE {id; resource = Some resource}], []
119 | | SESSION_START id -> just_connected [Actions.SESSION_START_SUCCESS id]
120 | | STREAM_CLOSE ->
121 | (* the stream can close during negotiation so close our direction too *)
122 | closed
123 | | ERROR e -> closed_with_error e
124 | | ROSTER_GET id -> just_connected [Actions.GET_ROSTER id]
125 | | ROSTER_SET {id; target; handle; groups} ->
126 | just_connected
127 | [Actions.SET_ROSTER {id; target = Jid.to_bare_raw target; handle; groups}]
128 | | ROSTER_REMOVE {id; target} ->
129 | just_connected
130 | [ Actions.ROSTER_REMOVE {id; target}
131 | ; Actions.PUSH_ROSTER {ato = None; contact = target} ]
132 | | SUBSCRIPTION_REQUEST {ato; xml} ->
133 | just_connected
134 | [ Actions.SUBSCRIPTION_REQUEST {ato; xml; from = None}
135 | ; Actions.PUSH_ROSTER {ato = None; contact = ato} ]
136 | | PRESENCE_UPDATE {status; xml} ->
137 | just_connected [Actions.UPDATE_PRESENCE {status; xml}]
138 | | IQ_ERROR {error_type; error_tag; id} ->
139 | {state = NEGOTIATING}, [Actions.IQ_ERROR {error_type; error_tag; id}], []
140 | | MESSAGE {ato; message} -> just_connected [Actions.MESSAGE {ato; message}]
141 | | LOG_OUT -> closed
142 | | NOOP -> t, [], []
143 | | SUBSCRIPTION_APPROVAL {ato; xml} ->
144 | just_connected
145 | [ Actions.SUBSCRIPTION_APPROVAL {ato; xml; from = None}
146 | ; Actions.ROSTER_SET_FROM ato
147 | ; Actions.PUSH_ROSTER {ato = None; contact = ato}
148 | ; Actions.SEND_CURRENT_PRESENCE ato ]
149 | | SUBSCRIPTION_CANCELLATION {user} ->
150 | just_connected [Actions.SUBSCRIPTION_CANCELLATION {user; force = false}]
151 | | SUBSCRIPTION_REMOVAL {contact} ->
152 | just_connected [Actions.SUBSCRIPTION_REMOVAL {contact}]
153 | ;;
154 |
155 | let handle_connected t = function
156 | | STREAM_HEADER _ -> closed_with_error "Not expecting stream header"
157 | | SASL_AUTH _ -> closed_with_error "Already negotiated sasl"
158 | | ANONYMOUS_SASL_AUTH -> closed_with_error "Already negotiated sasl"
159 | | RESOURCE_BIND_SERVER_GEN _ -> closed_with_error "Already connected"
160 | | RESOURCE_BIND_CLIENT_GEN _ -> closed_with_error "Already connected"
161 | | SESSION_START id -> {state = CONNECTED}, [Actions.SESSION_START_SUCCESS id], []
162 | | STREAM_CLOSE -> closed
163 | | ERROR e -> closed_with_error e
164 | | ROSTER_GET id -> {state = CONNECTED}, [Actions.GET_ROSTER id], []
165 | | ROSTER_SET {id; target; handle; groups} ->
166 | ( {state = CONNECTED}
167 | , [Actions.SET_ROSTER {id; target = Jid.to_bare_raw target; handle; groups}]
168 | , [] )
169 | | ROSTER_REMOVE {id; target} ->
170 | ( {state = CONNECTED}
171 | , [ Actions.ROSTER_REMOVE {id; target}
172 | ; Actions.PUSH_ROSTER {ato = None; contact = target} ]
173 | , [] )
174 | | SUBSCRIPTION_REQUEST {ato; xml} ->
175 | ( {state = CONNECTED}
176 | , [ Actions.SUBSCRIPTION_REQUEST {ato; xml; from = None}
177 | ; Actions.PUSH_ROSTER {ato = None; contact = ato} ]
178 | , [] )
179 | | PRESENCE_UPDATE {status; xml} ->
180 | {state = CONNECTED}, [Actions.UPDATE_PRESENCE {status; xml}], []
181 | | IQ_ERROR {error_type; error_tag; id} ->
182 | {state = CONNECTED}, [Actions.IQ_ERROR {error_type; error_tag; id}], []
183 | | MESSAGE {ato; message} -> {state = CONNECTED}, [Actions.MESSAGE {ato; message}], []
184 | | LOG_OUT -> closed
185 | | NOOP -> t, [], []
186 | | SUBSCRIPTION_APPROVAL {ato; xml} ->
187 | ( {state = CONNECTED}
188 | , [ Actions.SUBSCRIPTION_APPROVAL {ato; xml; from = None}
189 | ; Actions.ROSTER_SET_FROM ato
190 | ; Actions.PUSH_ROSTER {ato = None; contact = ato}
191 | ; Actions.SEND_CURRENT_PRESENCE ato ]
192 | , [] )
193 | | SUBSCRIPTION_CANCELLATION {user} ->
194 | {state = CONNECTED}, [Actions.SUBSCRIPTION_CANCELLATION {user; force = false}], []
195 | | SUBSCRIPTION_REMOVAL {contact} ->
196 | {state = CONNECTED}, [Actions.SUBSCRIPTION_REMOVAL {contact}], []
197 | ;;
198 |
199 | let handle_closed t = function
200 | | STREAM_HEADER _s -> closed_with_error "Not expecting stream header"
201 | | SASL_AUTH _ -> closed_with_error "Already negotiated sasl"
202 | | ANONYMOUS_SASL_AUTH -> closed_with_error "Already negotiated sasl"
203 | | RESOURCE_BIND_SERVER_GEN _ -> closed_with_error "Connection closed"
204 | | RESOURCE_BIND_CLIENT_GEN _ -> closed_with_error "Connection closed"
205 | | SESSION_START _id -> closed_with_error "Not expecting session start"
206 | | STREAM_CLOSE ->
207 | (* shouldn't receive another close after being closed *)
208 | closed_with_error "Not expecting a close"
209 | | ERROR e -> closed_with_error e
210 | | ROSTER_GET _ -> closed_with_error "already closed"
211 | | ROSTER_SET _ -> closed_with_error "already closed"
212 | | ROSTER_REMOVE _ -> closed_with_error "already closed"
213 | | SUBSCRIPTION_REQUEST _ -> closed_with_error "already closed"
214 | | PRESENCE_UPDATE _ -> closed_with_error "already closed"
215 | | IQ_ERROR _ -> closed_with_error "already closed"
216 | | MESSAGE _ -> closed_with_error "already closed"
217 | | LOG_OUT -> closed
218 | | NOOP -> t, [], []
219 | | SUBSCRIPTION_APPROVAL _ -> closed_with_error "already closed"
220 | | SUBSCRIPTION_CANCELLATION _ -> closed_with_error "already closed"
221 | | SUBSCRIPTION_REMOVAL _ -> closed_with_error "already closed"
222 | ;;
223 |
224 | let handle t event =
225 | match t.state with
226 | | IDLE -> handle_idle t event
227 | | SASL_NEGOTIATION -> handle_sasl_negotiation t event
228 | | NEGOTIATING -> handle_negotiating t event
229 | | CONNECTED -> handle_connected t event
230 | | CLOSED -> handle_closed t event
231 | ;;
232 |
233 | let%expect_test "create" =
234 | let fsm = initial in
235 | print_endline (to_string fsm);
236 | [%expect {| ((state IDLE)) |}]
237 | ;;
238 |
239 | let%expect_test "idle to negotiating" =
240 | let fsm = initial in
241 | let fsm, actions, _handler_actions =
242 | handle fsm (Events.STREAM_HEADER {version = "1.0"})
243 | in
244 | print_endline (to_string fsm);
245 | [%expect {| ((state SASL_NEGOTIATION)) |}];
246 | let strings = List.map (fun a -> Utils.mask_id @@ Actions.to_string a) actions in
247 | List.iter (Printf.printf "%s\n") strings;
248 | [%expect {|
249 | SEND_STREAM_HEADER
250 | SEND_STREAM_FEATURES_SASL |}]
251 | ;;
252 |
253 | let%expect_test "idle to negotiating with > 1.0" =
254 | let fsm = initial in
255 | let fsm, actions, _handler_actions =
256 | handle fsm (Events.STREAM_HEADER {version = "2.0"})
257 | in
258 | print_endline (to_string fsm);
259 | [%expect {| ((state SASL_NEGOTIATION)) |}];
260 | let strings = List.map (fun a -> Utils.mask_id @@ Actions.to_string a) actions in
261 | List.iter (Printf.printf "%s\n") strings;
262 | [%expect {|
263 | SEND_STREAM_HEADER
264 | SEND_STREAM_FEATURES_SASL |}]
265 | ;;
266 |
267 | let%expect_test "negotiating to closing" =
268 | let fsm = initial in
269 | let fsm, actions, _handler_actions =
270 | handle fsm (Events.STREAM_HEADER {version = "1.0"})
271 | in
272 | print_endline (to_string fsm);
273 | [%expect {| ((state SASL_NEGOTIATION)) |}];
274 | let strings = List.map (fun a -> Utils.mask_id @@ Actions.to_string a) actions in
275 | List.iter (Printf.printf "%s\n") strings;
276 | [%expect {|
277 | SEND_STREAM_HEADER
278 | SEND_STREAM_FEATURES_SASL |}];
279 | let fsm, actions, _handler_actions = handle fsm Events.STREAM_CLOSE in
280 | print_endline (to_string fsm);
281 | [%expect {| ((state CLOSED)) |}];
282 | let strings = List.map (fun a -> Actions.to_string a) actions in
283 | List.iter (Printf.printf "%s\n") strings;
284 | [%expect
285 | {|
286 | (UPDATE_PRESENCE (status Offline) (xml ()))
287 | REMOVE_FROM_CONNECTIONS
288 | (ERROR "Unexpected stream close during sasl negotiation") |}]
289 | ;;
290 |
291 | let%expect_test "sasl negotiation" =
292 | let fsm = initial in
293 | let fsm, actions, _handler_actions =
294 | handle fsm (Events.STREAM_HEADER {version = "1.0"})
295 | in
296 | print_endline (to_string fsm);
297 | [%expect {| ((state SASL_NEGOTIATION)) |}];
298 | let strings = List.map (fun a -> Actions.to_string a) actions in
299 | List.iter (Printf.printf "%s\n") strings;
300 | [%expect {|
301 | SEND_STREAM_HEADER
302 | SEND_STREAM_FEATURES_SASL |}];
303 | let fsm, actions, _handler_actions =
304 | handle fsm (Events.SASL_AUTH {user = "juliet"; password = ""})
305 | in
306 | print_endline (to_string fsm);
307 | [%expect {| ((state NEGOTIATING)) |}];
308 | let strings = List.map (fun a -> Actions.to_string a) actions in
309 | List.iter (Printf.printf "%s\n") strings;
310 | [%expect {|
311 | (SET_USER juliet)
312 | SEND_SASL_SUCCESS |}];
313 | let fsm, actions, _handler_actions =
314 | handle fsm (Events.RESOURCE_BIND_SERVER_GEN {id = "id"})
315 | in
316 | print_endline (to_string fsm);
317 | [%expect {| ((state NEGOTIATING)) |}];
318 | let strings = List.map (fun a -> Actions.to_string a) actions in
319 | List.iter (fun s -> print_endline s) strings;
320 | [%expect {|
321 | (SET_JID_RESOURCE (id id) (resource ())) |}];
322 | let fsm, actions, _handler_actions = handle fsm Events.STREAM_CLOSE in
323 | print_endline (to_string fsm);
324 | [%expect {| ((state CLOSED)) |}];
325 | let strings = List.map (fun a -> Actions.to_string a) actions in
326 | List.iter (Printf.printf "%s\n") strings;
327 | [%expect
328 | {|
329 | (UPDATE_PRESENCE (status Offline) (xml ()))
330 | REMOVE_FROM_CONNECTIONS
331 | CLOSE |}]
332 | ;;
333 |
334 | let%expect_test "bind resource" =
335 | let fsm = initial in
336 | let fsm, actions, _handler_actions =
337 | handle fsm (Events.STREAM_HEADER {version = "1.0"})
338 | in
339 | print_endline (to_string fsm);
340 | [%expect {| ((state SASL_NEGOTIATION)) |}];
341 | let strings = List.map (fun a -> Actions.to_string a) actions in
342 | List.iter (Printf.printf "%s\n") strings;
343 | [%expect {|
344 | SEND_STREAM_HEADER
345 | SEND_STREAM_FEATURES_SASL |}];
346 | let fsm, actions, _handler_actions =
347 | handle fsm (Events.SASL_AUTH {user = "juliet"; password = ""})
348 | in
349 | print_endline (to_string fsm);
350 | [%expect {| ((state NEGOTIATING)) |}];
351 | let strings = List.map (fun a -> Actions.to_string a) actions in
352 | List.iter (Printf.printf "%s\n") strings;
353 | [%expect {|
354 | (SET_USER juliet)
355 | SEND_SASL_SUCCESS |}];
356 | let fsm, actions, _handler_actions =
357 | handle fsm (Events.RESOURCE_BIND_SERVER_GEN {id = "id"})
358 | in
359 | print_endline (to_string fsm);
360 | [%expect {| ((state NEGOTIATING)) |}];
361 | let strings = List.map (fun a -> Actions.to_string a) actions in
362 | List.iter (fun s -> print_endline s) strings;
363 | [%expect {|
364 | (SET_JID_RESOURCE (id id) (resource ())) |}];
365 | let fsm, actions, _handler_actions = handle fsm Events.STREAM_CLOSE in
366 | print_endline (to_string fsm);
367 | [%expect {| ((state CLOSED)) |}];
368 | let strings = List.map (fun a -> Actions.to_string a) actions in
369 | List.iter (Printf.printf "%s\n") strings;
370 | [%expect
371 | {|
372 | (UPDATE_PRESENCE (status Offline) (xml ()))
373 | REMOVE_FROM_CONNECTIONS
374 | CLOSE |}]
375 | ;;
376 |
377 | let%expect_test "bind resource client" =
378 | let fsm = initial in
379 | let fsm, actions, _handler_actions =
380 | handle fsm (Events.STREAM_HEADER {version = "1.0"})
381 | in
382 | print_endline (to_string fsm);
383 | [%expect {| ((state SASL_NEGOTIATION)) |}];
384 | let strings = List.map (fun a -> Actions.to_string a) actions in
385 | List.iter (Printf.printf "%s\n") strings;
386 | [%expect {|
387 | SEND_STREAM_HEADER
388 | SEND_STREAM_FEATURES_SASL |}];
389 | let fsm, actions, _handler_actions =
390 | handle fsm (Events.SASL_AUTH {user = "juliet"; password = ""})
391 | in
392 | print_endline (to_string fsm);
393 | [%expect {| ((state NEGOTIATING)) |}];
394 | let strings = List.map (fun a -> Actions.to_string a) actions in
395 | List.iter (Printf.printf "%s\n") strings;
396 | [%expect {|
397 | (SET_USER juliet)
398 | SEND_SASL_SUCCESS |}];
399 | let fsm, actions, _handler_actions =
400 | handle fsm (Events.RESOURCE_BIND_CLIENT_GEN {id = "id"; resource = "client-res"})
401 | in
402 | print_endline (to_string fsm);
403 | [%expect {| ((state NEGOTIATING)) |}];
404 | let strings = List.map (fun a -> Actions.to_string a) actions in
405 | List.iter (fun s -> print_endline s) strings;
406 | [%expect {|
407 | (SET_JID_RESOURCE (id id) (resource (client-res))) |}];
408 | let fsm, actions, _handler_actions = handle fsm Events.STREAM_CLOSE in
409 | print_endline (to_string fsm);
410 | [%expect {| ((state CLOSED)) |}];
411 | let strings = List.map (fun a -> Actions.to_string a) actions in
412 | List.iter (Printf.printf "%s\n") strings;
413 | [%expect
414 | {|
415 | (UPDATE_PRESENCE (status Offline) (xml ()))
416 | REMOVE_FROM_CONNECTIONS
417 | CLOSE |}]
418 | ;;
419 |
420 | let%expect_test "roster get" =
421 | let fsm = initial in
422 | let fsm, actions, _handler_actions =
423 | handle fsm (Events.STREAM_HEADER {version = "1.0"})
424 | in
425 | print_endline (to_string fsm);
426 | [%expect {| ((state SASL_NEGOTIATION)) |}];
427 | List.map (fun a -> Actions.to_string a) actions |> List.iter (Printf.printf "%s\n");
428 | [%expect {|
429 | SEND_STREAM_HEADER
430 | SEND_STREAM_FEATURES_SASL |}];
431 | let fsm, actions, _handler_actions =
432 | handle fsm (Events.SASL_AUTH {user = "juliet"; password = ""})
433 | in
434 | print_endline (to_string fsm);
435 | [%expect {| ((state NEGOTIATING)) |}];
436 | let strings = List.map (fun a -> Actions.to_string a) actions in
437 | List.iter (Printf.printf "%s\n") strings;
438 | [%expect {|
439 | (SET_USER juliet)
440 | SEND_SASL_SUCCESS |}];
441 | let fsm, actions, _handler_actions =
442 | handle fsm (Events.RESOURCE_BIND_CLIENT_GEN {id = "id"; resource = "client-res"})
443 | in
444 | print_endline (to_string fsm);
445 | [%expect {| ((state NEGOTIATING)) |}];
446 | List.map (fun a -> Actions.to_string a) actions |> List.iter (fun s -> print_endline s);
447 | [%expect {| (SET_JID_RESOURCE (id id) (resource (client-res))) |}];
448 | let fsm, actions, _handler_actions = handle fsm (Events.ROSTER_GET "some_id") in
449 | print_endline (to_string fsm);
450 | [%expect {| ((state CONNECTED)) |}];
451 | List.map (fun a -> Actions.to_string a) actions |> List.iter (fun s -> print_endline s);
452 | [%expect {|
453 | PROBE_PRESENCE
454 | ADD_TO_CONNECTIONS
455 | (GET_ROSTER some_id) |}];
456 | let fsm, actions, _handler_actions = handle fsm Events.STREAM_CLOSE in
457 | print_endline (to_string fsm);
458 | [%expect {| ((state CLOSED)) |}];
459 | List.map (fun a -> Actions.to_string a) actions |> List.iter (Printf.printf "%s\n");
460 | [%expect
461 | {|
462 | (UPDATE_PRESENCE (status Offline) (xml ()))
463 | REMOVE_FROM_CONNECTIONS
464 | CLOSE |}]
465 | ;;
466 |
467 | let%expect_test "roster set" =
468 | let fsm = initial in
469 | let fsm, actions, _handler_actions =
470 | handle fsm (Events.STREAM_HEADER {version = "1.0"})
471 | in
472 | print_endline (to_string fsm);
473 | [%expect {| ((state SASL_NEGOTIATION)) |}];
474 | List.map (fun a -> Actions.to_string a) actions |> List.iter (Printf.printf "%s\n");
475 | [%expect {|
476 | SEND_STREAM_HEADER
477 | SEND_STREAM_FEATURES_SASL |}];
478 | let fsm, actions, _handler_actions =
479 | handle fsm (Events.SASL_AUTH {user = "juliet"; password = ""})
480 | in
481 | print_endline (to_string fsm);
482 | [%expect {| ((state NEGOTIATING)) |}];
483 | let strings = List.map (fun a -> Actions.to_string a) actions in
484 | List.iter (Printf.printf "%s\n") strings;
485 | [%expect {|
486 | (SET_USER juliet)
487 | SEND_SASL_SUCCESS |}];
488 | let fsm, actions, _handler_actions =
489 | handle fsm (Events.RESOURCE_BIND_CLIENT_GEN {id = "id"; resource = "client-res"})
490 | in
491 | print_endline (to_string fsm);
492 | [%expect {| ((state NEGOTIATING)) |}];
493 | List.map (fun a -> Actions.to_string a) actions |> List.iter (fun s -> print_endline s);
494 | [%expect {| (SET_JID_RESOURCE (id id) (resource (client-res))) |}];
495 | let fsm, actions, _handler_actions = handle fsm (Events.ROSTER_GET "some_id") in
496 | print_endline (to_string fsm);
497 | [%expect {| ((state CONNECTED)) |}];
498 | List.map (fun a -> Actions.to_string a) actions |> List.iter (fun s -> print_endline s);
499 | [%expect {|
500 | PROBE_PRESENCE
501 | ADD_TO_CONNECTIONS
502 | (GET_ROSTER some_id) |}];
503 | let fsm, actions, _handler_actions =
504 | handle
505 | fsm
506 | (Events.ROSTER_SET
507 | { id = "some_id"
508 | ; target = Jid.of_string "nurse@example.com"
509 | ; handle = "Nurse"
510 | ; groups = ["Servants"] })
511 | in
512 | print_endline (to_string fsm);
513 | [%expect {| ((state CONNECTED)) |}];
514 | List.map (fun a -> Actions.to_string a) actions |> List.iter (fun s -> print_endline s);
515 | [%expect
516 | {|
517 | (SET_ROSTER (id some_id) (target (nurse example.com)) (handle Nurse)
518 | (groups (Servants))) |}];
519 | let fsm, actions, _handler_actions = handle fsm Events.STREAM_CLOSE in
520 | print_endline (to_string fsm);
521 | [%expect {| ((state CLOSED)) |}];
522 | List.map (fun a -> Actions.to_string a) actions |> List.iter (Printf.printf "%s\n");
523 | [%expect
524 | {|
525 | (UPDATE_PRESENCE (status Offline) (xml ()))
526 | REMOVE_FROM_CONNECTIONS
527 | CLOSE |}]
528 | ;;
529 |
--------------------------------------------------------------------------------
/src/state.mli:
--------------------------------------------------------------------------------
1 | (** State machine representing the transitions for the XMPP input events. The events drive the new states of the state machine and it returns actions to be taken, typically of the form of writing data back to the user. *)
2 |
3 | (** The type of a state machine *)
4 | type t [@@deriving sexp]
5 |
6 | (** Create a state machine in the initial state *)
7 | val initial : t
8 |
9 | (** [handle t e] updates the state machine [t] with the event [e] to give the resulting state machine in a new state and the list of actions to be performed *)
10 | val handle : t -> Events.t -> t * Actions.t list * Actions.handler_actions list
11 |
12 | val to_string : t -> string
13 |
--------------------------------------------------------------------------------
/src/stream.ml:
--------------------------------------------------------------------------------
1 | type t =
2 | | Header of Xml.tag
3 | | Features
4 | | Error
5 | | Close
6 | [@@deriving sexp]
7 |
8 | let to_string = function
9 | | Header tag -> Xml.tag_to_string ~empty:false tag
10 | | Features -> "features"
11 | | Error -> "error"
12 | | Close -> ""
13 | ;;
14 |
15 | let features_sasl_mechanisms =
16 | Xml.create
17 | (("stream", "features"), [])
18 | ~children:
19 | [ Xml.create
20 | (("", "mechanisms"), ["", Xml.Xmlns "urn:ietf:params:xml:ns:xmpp-sasl"])
21 | ~children:
22 | [ Xml.create (("", "mechanism"), []) ~children:[Xml.Text "PLAIN"]
23 | ; Xml.create (("", "mechanism"), []) ~children:[Xml.Text "ANONYMOUS"] ] ]
24 | ;;
25 |
26 | let features =
27 | Xml.create
28 | (("stream", "features"), [])
29 | ~children:
30 | [Xml.create (("", "bind"), ["", Xml.Xmlns "urn:ietf:params:xml:ns:xmpp-bind"])]
31 | ;;
32 |
33 | let create_header
34 | ?(version = "1.0")
35 | ?(lang = "en")
36 | ?(xmlns = "jabber:client")
37 | ?(stream_ns = "http://etherx.jabber.org/streams")
38 | ?(attributes = [])
39 | ?ato
40 | ?from
41 | () =
42 | let attributes =
43 | match ato with Some v -> ("", Xml.To v) :: attributes | None -> attributes
44 | in
45 | let attributes =
46 | match from with Some v -> ("", Xml.From v) :: attributes | None -> attributes
47 | in
48 | ( ("stream", "stream")
49 | , [ "", Xml.Id (Stanza.gen_id ())
50 | ; "", Xml.Version version
51 | ; "xml", Xml.Lang lang
52 | ; "", Xml.Xmlns xmlns
53 | ; "xmlns", Xml.Stream stream_ns ]
54 | @ attributes )
55 | ;;
56 |
--------------------------------------------------------------------------------
/src/stream.mli:
--------------------------------------------------------------------------------
1 | type t =
2 | | Header of Xml.tag
3 | | Features
4 | | Error
5 | | Close
6 | [@@deriving sexp]
7 |
8 | val to_string : t -> string
9 | val features_sasl_mechanisms : Xml.t
10 | val features : Xml.t
11 |
12 | val create_header :
13 | ?version:string
14 | -> ?lang:string
15 | -> ?xmlns:string
16 | -> ?stream_ns:string
17 | -> ?attributes:Xml.attribute list
18 | -> ?ato:Jid.t
19 | -> ?from:Jid.t
20 | -> unit
21 | -> Xml.tag
22 |
--------------------------------------------------------------------------------
/src/utils.ml:
--------------------------------------------------------------------------------
1 | let mask_id s =
2 | match Astring.String.find_sub ~sub:"id='" s with
3 | | Some i ->
4 | (match Astring.String.find_sub ~start:(i + 4) ~sub:"'" s with
5 | | Some j ->
6 | Astring.String.with_index_range ~first:0 ~last:(i + 3) s
7 | ^ ""
8 | ^ Astring.String.with_index_range ~first:j s
9 | | None -> assert false)
10 | | None -> s
11 | ;;
12 |
13 | let option_to_string string_func = function
14 | | Some thing -> "Some: " ^ string_func thing
15 | | None -> "None"
16 | ;;
17 |
--------------------------------------------------------------------------------
/src/utils.mli:
--------------------------------------------------------------------------------
1 | val mask_id : string -> string
2 | val option_to_string : ('a -> string) -> 'a option -> string
3 |
--------------------------------------------------------------------------------
/src/xml.ml:
--------------------------------------------------------------------------------
1 | open Sexplib.Std
2 |
3 | type name = string * string [@@deriving sexp]
4 |
5 | type attribute_value =
6 | | From of Jid.t
7 | | To of Jid.t
8 | | Id of string
9 | | Jid of Jid.t
10 | | Xmlns of string
11 | | Type of string
12 | | Ver of string
13 | | Version of string
14 | | Lang of string
15 | | Stream of string
16 | | Name of string
17 | | Subscription of string
18 | | Mechanism of string
19 | | Other of string * string
20 | [@@deriving sexp]
21 |
22 | type attribute = string * attribute_value [@@deriving sexp]
23 | type tag = name * attribute list [@@deriving sexp]
24 |
25 | type t =
26 | | Text of string
27 | | Element of tag * t list
28 | [@@deriving sexp]
29 |
30 | let remove_prefixes_attribute (_prefix, value) = "", value
31 |
32 | let rec remove_prefixes = function
33 | | Element (((_prefix, name), attributes), children) ->
34 | Element
35 | ( (("", name), List.map remove_prefixes_attribute attributes)
36 | , List.map remove_prefixes children )
37 | | Text _t as text -> text
38 | ;;
39 |
40 | let name_to_string (prefix, name) = if prefix <> "" then prefix ^ ":" ^ name else name
41 |
42 | let attribute_to_string (namespace, nameval) =
43 | (if namespace <> "" then namespace ^ ":" else "")
44 | ^
45 | match nameval with
46 | | From jid -> "from='" ^ Jid.to_string jid ^ "'"
47 | | To jid -> "to='" ^ Jid.to_string jid ^ "'"
48 | | Id s -> "id='" ^ s ^ "'"
49 | | Jid jid -> "jid='" ^ Jid.to_string jid ^ "'"
50 | | Xmlns s -> "xmlns='" ^ s ^ "'"
51 | | Type s -> "type='" ^ s ^ "'"
52 | | Ver s -> "ver='" ^ s ^ "'"
53 | | Version s -> "version='" ^ s ^ "'"
54 | | Lang s -> "lang='" ^ s ^ "'"
55 | | Stream s -> "stream='" ^ s ^ "'"
56 | | Name s -> "name='" ^ s ^ "'"
57 | | Subscription s -> "subscription='" ^ s ^ "'"
58 | | Mechanism s -> "mechanism='" ^ s ^ "'"
59 | | Other (name, value) -> name ^ "='" ^ value ^ "'"
60 | ;;
61 |
62 | let tag_to_string ~empty (name, attributes) =
63 | let sep = " " in
64 | let attr_string =
65 | String.concat sep (List.map (fun a -> attribute_to_string a) attributes)
66 | in
67 | let name_string = name_to_string name in
68 | "<"
69 | ^ name_string
70 | ^ (if attr_string <> "" then sep ^ attr_string else "")
71 | ^ if empty then "/>" else ">"
72 | ;;
73 |
74 | let rec to_string = function
75 | | Text s -> s
76 | | Element (((name, _attributes) as tag), children) ->
77 | (match children with
78 | | [] -> tag_to_string ~empty:true tag
79 | | cs ->
80 | tag_to_string ~empty:false tag
81 | ^ String.concat "" (List.map (fun c -> to_string c) cs)
82 | ^ ""
83 | ^ name_to_string name
84 | ^ ">")
85 | ;;
86 |
87 | let create ?(children = []) tag = Element (tag, children)
88 |
--------------------------------------------------------------------------------
/src/xml.mli:
--------------------------------------------------------------------------------
1 | type name = string * string [@@deriving sexp]
2 |
3 | type attribute_value =
4 | | From of Jid.t
5 | | To of Jid.t
6 | | Id of string
7 | | Jid of Jid.t
8 | | Xmlns of string
9 | | Type of string
10 | | Ver of string
11 | | Version of string
12 | | Lang of string
13 | | Stream of string
14 | | Name of string
15 | | Subscription of string
16 | | Mechanism of string
17 | | Other of string * string
18 | [@@deriving sexp]
19 |
20 | type attribute = string * attribute_value [@@deriving sexp]
21 | type tag = name * attribute list [@@deriving sexp]
22 |
23 | type t =
24 | | Text of string
25 | | Element of tag * t list
26 | [@@deriving sexp]
27 |
28 | val remove_prefixes : t -> t
29 | val to_string : t -> string
30 | val tag_to_string : empty:bool -> tag -> string
31 | val create : ?children:t list -> tag -> t
32 |
--------------------------------------------------------------------------------
/test/integration/dune:
--------------------------------------------------------------------------------
1 | (library
2 | (name integration)
3 | (libraries lwt lwt.unix mirage-xmpp)
4 | (inline_tests
5 | (flags -show-counts -strict))
6 | (preprocess
7 | (pps ppx_expect lwt_ppx bisect_ppx -conditional)))
8 |
--------------------------------------------------------------------------------
/test/integration/integration.ml:
--------------------------------------------------------------------------------
1 | let send ?(timeout = 10.) ?(host = "127.0.0.1") ?(port = 5222) str =
2 | let timeout_t =
3 | let%lwt () = Lwt_unix.sleep timeout in
4 | Lwt.return "Timeout"
5 | in
6 | let request =
7 | let addr = Unix.ADDR_INET (Unix.inet_addr_of_string host, port) in
8 | Lwt_io.(
9 | with_connection addr (fun (_i, o) ->
10 | let%lwt () = write o str in
11 | Lwt.return "Success" ))
12 | in
13 | let s = Lwt_main.run (Lwt.pick [request; timeout_t]) in
14 | print_endline s
15 | ;;
16 |
17 | let send_recv ?(timeout = 10.) ?(host = "127.0.0.1") ?(port = 5222) str_list =
18 | let timeout_t =
19 | let%lwt () = Lwt_unix.sleep timeout in
20 | Lwt.return "Timeout"
21 | in
22 | let request =
23 | let mask_id s =
24 | match Astring.String.find_sub ~sub:"id='" s with
25 | | Some i ->
26 | (match Astring.String.find_sub ~start:(i + 4) ~sub:"'" s with
27 | | Some j ->
28 | Astring.String.with_index_range ~first:0 ~last:(i + 3) s
29 | ^ "redacted_for_testing"
30 | ^ Astring.String.with_index_range ~first:j s
31 | | None -> assert false)
32 | | None -> s
33 | in
34 | let addr = Unix.ADDR_INET (Unix.inet_addr_of_string host, port) in
35 | Lwt_io.(
36 | with_connection addr (fun (i, o) ->
37 | let rec reader () =
38 | (* Repeatedly read data from the connection and print it *)
39 | try%lwt
40 | let%lwt s = read_line i in
41 | print_endline ("Receive:\n" ^ mask_id s);
42 | if s = "" then Lwt.return "Finished" else reader ()
43 | with End_of_file -> Lwt.return "Didn't close the stream before exiting"
44 | in
45 | let rec writer = function
46 | (* Send all the data in the list to the server *)
47 | | [] -> Lwt.return "Finished"
48 | | x :: xs ->
49 | print_endline ("Send:\n" ^ x);
50 | let%lwt () = write o x in
51 | let%lwt () = Lwt_unix.sleep 0.1 in
52 | writer xs
53 | in
54 | Lwt.async (fun () -> writer str_list);
55 | reader () ))
56 | in
57 | let s = Lwt_main.run (Lwt.pick [request; timeout_t]) in
58 | print_endline s
59 | ;;
60 |
61 | let start_unikernel () =
62 | print_endline "Starting unikernel";
63 | let command =
64 | Lwt_process.shell
65 | "cd ../../../../; mirage/xmpp --hostname=\"im.example.com\" -l \"debug\" > \
66 | unikernel.log 2>&1"
67 | in
68 | let _process = Lwt_process.open_process_none command in
69 | Unix.sleepf 0.1
70 | ;;
71 |
72 | let stop_unikernel () =
73 | print_endline "Stopping unikernel";
74 | send ~port:8081 "exit";
75 | Unix.sleepf 0.2
76 | ;;
77 |
78 | let test_unikernel f =
79 | start_unikernel ();
80 | f ();
81 | stop_unikernel ()
82 | ;;
83 |
84 | let%expect_test "start stop" =
85 | test_unikernel (fun () -> ());
86 | [%expect {|
87 | Starting unikernel
88 | Stopping unikernel
89 | Success |}]
90 | ;;
91 |
92 | let%expect_test "open and close stream" =
93 | test_unikernel (fun () ->
94 | send_recv
95 | [ ""
98 | ; "" ] );
99 | [%expect
100 | {|
101 | Starting unikernel
102 | Send:
103 |
104 | Receive:
105 |
106 | Receive:
107 | PLAINANONYMOUS
108 | Send:
109 |
110 | Receive:
111 | Unexpected stream close during sasl negotiation
112 | Didn't close the stream before exiting
113 | Stopping unikernel
114 | Success |}]
115 | ;;
116 |
117 | let%expect_test "open stream with iq bind" =
118 | test_unikernel (fun () ->
119 | send_recv
120 | [ ""
123 | ; "AGp1bGlldABwYXNzd29yZA=="
125 | ; ""
128 | ; "balcony"
130 | ; "" ] );
131 | [%expect
132 | {|
133 | Starting unikernel
134 | Send:
135 |
136 | Receive:
137 |
138 | Receive:
139 | PLAINANONYMOUS
140 | Send:
141 | AGp1bGlldABwYXNzd29yZA==
142 | Receive:
143 |
144 | Send:
145 |
146 | Receive:
147 |
148 | Receive:
149 |
150 | Send:
151 | balcony
152 | Receive:
153 | juliet@im.example.com/balcony
154 | Send:
155 |
156 | Receive:
157 |
158 | Finished
159 | Stopping unikernel
160 | Success |}]
161 | ;;
162 |
163 | let%expect_test "open stream with iq bind and roster get without contacts" =
164 | test_unikernel (fun () ->
165 | send_recv
166 | [ ""
169 | ; "AGp1bGlldABwYXNzd29yZA=="
171 | ; ""
174 | ; "balcony"
176 | ; ""
178 | ; "" ] );
179 | [%expect
180 | {|
181 | Starting unikernel
182 | Send:
183 |
184 | Receive:
185 |
186 | Receive:
187 | PLAINANONYMOUS
188 | Send:
189 | AGp1bGlldABwYXNzd29yZA==
190 | Receive:
191 |
192 | Send:
193 |
194 | Receive:
195 |
196 | Receive:
197 |
198 | Send:
199 | balcony
200 | Receive:
201 | juliet@im.example.com/balcony
202 | Send:
203 |
204 | Receive:
205 |
206 | Send:
207 |
208 | Receive:
209 |
210 | Finished
211 | Stopping unikernel
212 | Success |}]
213 | ;;
214 |
215 | let%expect_test "open stream with iq bind and roster get with contacts" =
216 | test_unikernel (fun () ->
217 | send_recv
218 | [ ""
221 | ; "AGp1bGlldABwYXNzd29yZA=="
223 | ; ""
226 | ; "balcony"
228 | ; "- Servants
"
231 | ; ""
233 | ; "" ] );
234 | [%expect
235 | {|
236 | Starting unikernel
237 | Send:
238 |
239 | Receive:
240 |
241 | Receive:
242 | PLAINANONYMOUS
243 | Send:
244 | AGp1bGlldABwYXNzd29yZA==
245 | Receive:
246 |
247 | Send:
248 |
249 | Receive:
250 |
251 | Receive:
252 |
253 | Send:
254 | balcony
255 | Receive:
256 | juliet@im.example.com/balcony
257 | Send:
258 | - Servants
259 | Receive:
260 |
261 | Receive:
262 | - Servants
263 | Send:
264 |
265 | Receive:
266 | - Servants
267 | Send:
268 |
269 | Receive:
270 |
271 | Finished
272 | Stopping unikernel
273 | Success |}]
274 | ;;
275 |
--------------------------------------------------------------------------------
/test/performance/dune:
--------------------------------------------------------------------------------
1 | (executable
2 | (name performance)
3 | (libraries lwt lwt.unix cmdliner astring jingoo re)
4 | (preprocess
5 | (pps lwt_ppx)))
6 |
--------------------------------------------------------------------------------
/test/performance/performance.ml:
--------------------------------------------------------------------------------
1 | open Cmdliner
2 |
3 | let run_python_stats file =
4 | let%lwt () = Lwt_io.printl @@ "PYTHON: Running stats on file: " ^ file in
5 | let%lwt _ = Lwt_unix.system @@ "python test/performance/stats.py --save " ^ file in
6 | Lwt_io.printl "PYTHON: Finished stats"
7 | ;;
8 |
9 | let run_command_with_output command =
10 | let command = Lwt_process.shell command in
11 | let process = Lwt_process.open_process_in command in
12 | let rec get_lines () =
13 | match%lwt Lwt_io.read_line_opt process#stdout with
14 | | Some l ->
15 | let%lwt lines_after = get_lines () in
16 | Lwt.return ((string_of_float (Unix.gettimeofday ()) ^ " " ^ l) :: lines_after)
17 | | None -> Lwt.return_nil
18 | in
19 | get_lines ()
20 | ;;
21 |
22 | let get_cpu_mem_docker container_name =
23 | let command =
24 | "docker stats --no-stream "
25 | ^ container_name
26 | ^ " | sed -n '2p' | awk '{gsub(/%/, \"\", $3); gsub(/%/, \"\", $7); printf \"%s \
27 | %s\", $3, $7}'"
28 | in
29 | run_command_with_output command
30 | ;;
31 |
32 | let tsung file server_name =
33 | let tsung_command = Lwt_process.shell "tsung -f test/performance/tsung.xml start" in
34 | let tsung_process = Lwt_process.open_process_in tsung_command in
35 | let rec tsung_get_lines () =
36 | match%lwt Lwt_io.read_line_opt tsung_process#stdout with
37 | | Some l ->
38 | let%lwt () = Lwt_io.printl @@ "TSUNG: " ^ l in
39 | let%lwt lines_after = tsung_get_lines () in
40 | Lwt.return (l :: lines_after)
41 | | None -> Lwt.return_nil
42 | in
43 | let tsung_lines = ref [] in
44 | Lwt.async (fun () ->
45 | let%lwt tsung_output = tsung_get_lines () in
46 | tsung_lines := tsung_output;
47 | Lwt.return_unit );
48 | let cpumem_lines = ref [] in
49 | let rec main_loop () =
50 | match tsung_process#state with
51 | | Running ->
52 | let%lwt logged_lines = get_cpu_mem_docker server_name in
53 | cpumem_lines := !cpumem_lines @ logged_lines;
54 | let%lwt () = Lwt_unix.sleep 1. in
55 | main_loop ()
56 | | Exited _ -> Lwt.return_unit
57 | in
58 | let%lwt () = main_loop () in
59 | match%lwt tsung_process#status with
60 | | Unix.WEXITED 0 ->
61 | (* get the log file location *)
62 | let dump_time, dump_file =
63 | match
64 | List.filter
65 | (fun line -> Astring.String.is_prefix ~affix:"Log directory is:" line)
66 | !tsung_lines
67 | with
68 | | [line] ->
69 | (match Astring.String.cut ~sep:"/" line with
70 | | Some (_, path) ->
71 | ( (match Astring.String.cut ~rev:true ~sep:"/" path with
72 | | Some (_, datetime) -> datetime
73 | | None -> "")
74 | , "/" ^ path ^ "/tsung.dump" )
75 | | None -> "", "")
76 | | _ -> "", ""
77 | in
78 | (* separate the input file to the name of the xml file *)
79 | let config_name =
80 | match Astring.String.cut ~rev:true ~sep:"/" file with
81 | | Some (_, filename) ->
82 | (* remove the file extension *)
83 | (match Astring.String.cut ~rev:true ~sep:"." filename with
84 | | Some (fname, _) -> fname
85 | | None -> filename)
86 | | None -> file
87 | in
88 | (* copy the dump file to a new location with servername, xml file and time to identify it: servername-xmlfile-time.dump *)
89 | let results_dir =
90 | "test/performance/results/"
91 | ^ String.concat "-" [server_name; config_name; dump_time]
92 | in
93 | let%lwt () =
94 | try%lwt Lwt_unix.mkdir "test/performance/results" 0o755 with Unix.Unix_error _ ->
95 | Lwt.return_unit
96 | in
97 | let%lwt () =
98 | try%lwt Lwt_unix.mkdir results_dir 0o755 with Unix.Unix_error _ -> Lwt.return_unit
99 | in
100 | let%lwt cpumem_file = Lwt_io.open_file ~mode:Output (results_dir ^ "/cpumem") in
101 | let%lwt () =
102 | Lwt_list.iter_s (fun line -> Lwt_io.write_line cpumem_file line) !cpumem_lines
103 | in
104 | let%lwt () = Lwt_io.close cpumem_file in
105 | let copied_dump = results_dir ^ "/dump" in
106 | let%lwt () = Lwt_unix.rename dump_file copied_dump in
107 | let%lwt () = Lwt_io.printl "TSUNG: Finished Tsung" in
108 | Lwt.return results_dir
109 | | _ -> Lwt.return ""
110 | ;;
111 |
112 | let test_docker image volume server_name file =
113 | let command =
114 | Lwt_process.shell
115 | @@ "docker run --rm --name "
116 | ^ server_name
117 | ^ " "
118 | ^ (if volume <> "" then "-v " ^ volume else "")
119 | ^ " -p 5222:5222 "
120 | ^ image
121 | in
122 | let%lwt () = Lwt_io.printl "DOCKER: Starting container" in
123 | let process = Lwt_process.open_process_in command in
124 | let rec get_lines () =
125 | match%lwt Lwt_io.read_line_opt process#stdout with
126 | | Some l ->
127 | let%lwt () = Lwt_io.printl @@ "DOCKER: " ^ l in
128 | get_lines ()
129 | | None -> Lwt.return_unit
130 | in
131 | Lwt.async get_lines;
132 | let%lwt () = Lwt_unix.sleep 20. in
133 | let%lwt results_dir = tsung file server_name in
134 | let%lwt () = Lwt_io.printl "DOCKER: Stopping container" in
135 | let%lwt _ = Lwt_unix.system @@ "docker stop " ^ server_name in
136 | Lwt.return results_dir
137 | ;;
138 |
139 | let test_none server_name file = tsung file server_name
140 |
141 | type server =
142 | | Mirage
143 | | Ejabberd
144 | | Tigase
145 | | Prosody
146 | | None
147 |
148 | let server_to_string = function
149 | | Mirage -> "mirage"
150 | | Ejabberd -> "ejabberd"
151 | | Tigase -> "tigase"
152 | | Prosody -> "prosody"
153 | | None -> "none"
154 | ;;
155 |
156 | let mirage = "jeffas/mirage-xmpp"
157 | let ejabberd = "ejabberd/ecs"
158 | let tigase = "dictcp/tigase"
159 | let prosody = "prosody/prosody"
160 |
161 | let performance
162 | servers files load_duration load_duration_unit load_arrivalrate load_arrivalrate_unit
163 | =
164 | Lwt_main.run
165 | (let%lwt () =
166 | Lwt_io.printl
167 | @@ "Servers: "
168 | ^ String.concat ", " (List.map (fun server -> server_to_string server) servers)
169 | in
170 | let%lwt () = Lwt_io.printl @@ "Files: " ^ String.concat ", " files in
171 | let%lwt () =
172 | Lwt_io.printl
173 | @@ "Load duration: "
174 | ^ string_of_int load_duration
175 | ^ " "
176 | ^ load_duration_unit
177 | in
178 | let%lwt () =
179 | Lwt_io.printl
180 | @@ "Load arrivalrate: "
181 | ^ string_of_int load_arrivalrate
182 | ^ " per "
183 | ^ load_arrivalrate_unit
184 | in
185 | let files_length = List.length files in
186 | let run test_fn =
187 | Lwt_list.iteri_s
188 | (fun i file ->
189 | let%lwt () =
190 | Templates.make_template
191 | file
192 | load_duration
193 | load_duration_unit
194 | load_arrivalrate
195 | load_arrivalrate_unit
196 | in
197 | let%lwt results_dir = test_fn file in
198 | let%lwt () = run_python_stats results_dir in
199 | if i + 1 <> files_length then Lwt_unix.sleep 30. else Lwt.return_unit )
200 | files
201 | in
202 | Lwt_list.iter_s
203 | (fun server ->
204 | match server with
205 | | Mirage -> run @@ test_docker mirage "" @@ server_to_string Mirage
206 | | Ejabberd ->
207 | run
208 | @@ test_docker
209 | ejabberd
210 | "$(pwd)/docker/ejabberd/ejabberd.yml:/home/ejabberd/conf/ejabberd.yml"
211 | @@ server_to_string Ejabberd
212 | | Tigase ->
213 | run
214 | @@ test_docker
215 | tigase
216 | "$(pwd)/docker/tigase/init.properties:/opt/tigase-server/etc/init.properties"
217 | @@ server_to_string Tigase
218 | | Prosody ->
219 | run
220 | @@ test_docker
221 | prosody
222 | "$(pwd)/docker/prosody/prosody.cfg.lua:/etc/prosody/prosody.cfg.lua"
223 | @@ server_to_string Prosody
224 | | None -> run @@ test_none @@ server_to_string None )
225 | servers)
226 | ;;
227 |
228 | (* Command line parsing *)
229 |
230 | let servers =
231 | let doc = "Run performance tests against the MirageOS unikernel" in
232 | let mirage = Mirage, Arg.info ["m"; "mirage"] ~doc in
233 | let doc = "Run performance tests against the Ejabberd server" in
234 | let ejabberd = Ejabberd, Arg.info ["e"; "ejabberd"] ~doc in
235 | let doc = "Run performance tests against the Tigase server" in
236 | let tigase = Tigase, Arg.info ["t"; "tigase"] ~doc in
237 | let doc = "Run performance tests against the Prosody server" in
238 | let prosody = Prosody, Arg.info ["p"; "prosody"] ~doc in
239 | let doc = "No automated server creation, just run tsung" in
240 | let none = None, Arg.info ["n"; "none"] ~doc in
241 | Arg.(value & vflag_all [] [mirage; ejabberd; tigase; prosody; none])
242 | ;;
243 |
244 | let files =
245 | let doc = "The xml files to run tsung with." in
246 | Arg.(value & pos_all file [] & info [] ~doc)
247 | ;;
248 |
249 | let load_duration =
250 | let doc = "The duration to run the tests for" in
251 | Arg.(value & opt int 1 & info ["ld"; "load-duration"] ~doc)
252 | ;;
253 |
254 | let load_duration_unit =
255 | let doc = "The unit (minute or second) for the load duration." in
256 | Arg.(value & opt string "minute" & info ["ldu"; "load-duration-unit"] ~doc)
257 | ;;
258 |
259 | let load_arrivalrate =
260 | let doc = "The arrivalrate of the users for the scenario." in
261 | Arg.(value & opt int 10 & info ["la"; "load-arrivalrate"] ~doc)
262 | ;;
263 |
264 | let load_arrivalrate_unit =
265 | let doc = "The unit (minute or second) for the arrivalrate of users." in
266 | Arg.(value & opt string "second" & info ["lau"; "load-arrivalrate-unit"] ~doc)
267 | ;;
268 |
269 | let cmd =
270 | let doc = "Run performance tests against a given target." in
271 | ( Term.(
272 | const performance
273 | $ servers
274 | $ files
275 | $ load_duration
276 | $ load_duration_unit
277 | $ load_arrivalrate
278 | $ load_arrivalrate_unit)
279 | , Term.info "performance" ~doc ~exits:Term.default_exits )
280 | ;;
281 |
282 | let info =
283 | let doc = "Run the performance tests against a given target." in
284 | Term.info "performance" ~doc ~exits:Term.default_exits
285 | ;;
286 |
287 | let () = Term.exit @@ Term.eval cmd
288 |
--------------------------------------------------------------------------------
/test/performance/stats.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import math
3 | import re
4 | import xml.etree.ElementTree as ET
5 | from collections import defaultdict
6 | from enum import Enum
7 | from pprint import pprint
8 |
9 | import matplotlib.pyplot as plt
10 | import numpy as np
11 |
12 |
13 | def parse_newclient(line):
14 | """NewClient: