View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2014-2015, VU University Amsterdam
    7    All rights reserved.
    8
    9    Redistribution and use in source and binary forms, with or without
   10    modification, are permitted provided that the following conditions
   11    are met:
   12
   13    1. Redistributions of source code must retain the above copyright
   14       notice, this list of conditions and the following disclaimer.
   15
   16    2. Redistributions in binary form must reproduce the above copyright
   17       notice, this list of conditions and the following disclaimer in
   18       the documentation and/or other materials provided with the
   19       distribution.
   20
   21    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   22    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   23    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   24    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   25    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   26    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   27    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   28    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   29    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   30    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   31    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   32    POSSIBILITY OF SUCH DAMAGE.
   33*/
   34
   35:- module(websocket,
   36          [ http_open_websocket/3,      % +URL, -WebSocket, +Options
   37            http_upgrade_to_websocket/3, % :Goal, +Options, +Request
   38            ws_send/2,                  % +WebSocket, +Message
   39            ws_receive/2,               % +WebSocket, -Message
   40            ws_receive/3,               % +WebSocket, -Message, +Options
   41            ws_close/3,                 % +WebSocket, +Code, +Message
   42                                        % Low level interface
   43            ws_open/3,                  % +Stream, -WebSocket, +Options
   44            ws_property/2               % +WebSocket, ?Property
   45          ]).   46:- use_module(library(debug),[debug/3]).   47:- use_module(library(http/http_dispatch),[http_switch_protocol/2]).   48
   49:- autoload(library(base64),[base64//1]).   50:- autoload(library(error),
   51	    [permission_error/3,must_be/2,type_error/2,domain_error/2]).   52:- autoload(library(lists),[member/2]).   53:- autoload(library(option),[select_option/3,option/2,option/3]).   54:- autoload(library(sha),[sha_hash/3]).   55:- autoload(library(http/http_open),[http_open/3]).   56:- autoload(library(http/json),[json_write_dict/2,json_read_dict/3]).   57
   58:- meta_predicate
   59    http_upgrade_to_websocket(1, +, +).   60
   61:- predicate_options(http_open_websocket/3, 3,
   62                     [ subprotocols(list(atom)),
   63                       pass_to(http_open:http_open/3, 3)
   64                     ]).   65:- predicate_options(http_upgrade_to_websocket/3, 2,
   66                     [ guarded(boolean),
   67                       subprotocols(list(atom))
   68                     ]).   69
   70:- use_foreign_library(foreign(websocket)).   71
   72/** <module> WebSocket support
   73
   74WebSocket is a lightweight message oriented   protocol  on top of TCP/IP
   75streams. It is typically used as an   _upgrade_ of an HTTP connection to
   76provide bi-directional communication, but can also  be used in isolation
   77over arbitrary (Prolog) streams.
   78
   79The SWI-Prolog interface is based on _streams_ and provides ws_open/3 to
   80create a _websocket stream_ from any   Prolog stream. Typically, both an
   81input and output stream are wrapped  and   then  combined  into a single
   82object using stream_pair/3.
   83
   84The high-level interface provides http_upgrade_to_websocket/3 to realise
   85a   websocket   inside   the    HTTP     server    infrastructure    and
   86http_open_websocket/3 as a layer over http_open/3   to  realise a client
   87connection. After establishing a connection,  ws_send/2 and ws_receive/2
   88can be used to send and receive   messages.  The predicate ws_close/3 is
   89provided to perform the closing  handshake   and  dispose  of the stream
   90objects.
   91
   92@see    RFC 6455, http://tools.ietf.org/html/rfc6455
   93@tbd    Deal with protocol extensions.
   94*/
   95
   96
   97
   98                 /*******************************
   99                 *         HTTP SUPPORT         *
  100                 *******************************/
  101
  102%!  http_open_websocket(+URL, -WebSocket, +Options) is det.
  103%
  104%   Establish  a  client  websocket  connection.  This  predicate  calls
  105%   http_open/3  with  additional  headers  to   negotiate  a  websocket
  106%   connection. In addition to the options processed by http_open/3, the
  107%   following options are recognised:
  108%
  109%     - subprotocols(+List)
  110%     List of subprotocols that are acceptable. The selected
  111%     protocol is available as ws_property(WebSocket,
  112%     subprotocol(Protocol).
  113%
  114%   Note that clients often provide an  `Origin` header and some servers
  115%   require this field. See  RFC  6455   for  details.  By  default this
  116%   predicate  does  not  set  `Origin`.  It    may  be  set  using  the
  117%   `request_header` option of http_open/3, e.g. by  passing this in the
  118%   Options list:
  119%
  120%       request_header('Origin' = 'https://www.swi-prolog.org')
  121%
  122%   The   following   example   exchanges    a     message    with   the
  123%   html5rocks.websocket.org echo service:
  124%
  125%     ```
  126%     ?- URL = 'ws://html5rocks.websocket.org/echo',
  127%        http_open_websocket(URL, WS, []),
  128%        ws_send(WS, text('Hello World!')),
  129%        ws_receive(WS, Reply),
  130%        ws_close(WS, 1000, "Goodbye").
  131%     URL = 'ws://html5rocks.websocket.org/echo',
  132%     WS = <stream>(0xe4a440,0xe4a610),
  133%     Reply = websocket{data:"Hello World!", opcode:text}.
  134%     ```
  135%
  136%   @arg WebSocket is a stream pair (see stream_pair/3)
  137
  138http_open_websocket(URL, WebSocket, Options) :-
  139    phrase(base64(`___SWI-Prolog___`), Bytes),
  140    string_codes(Key, Bytes),
  141    add_subprotocols(Options, Options1),
  142    http_open(URL, In,
  143              [ status_code(Status),
  144                output(Out),
  145                header(sec_websocket_protocol, Selected),
  146                header(sec_websocket_accept, AcceptedKey),
  147                connection('Keep-alive, Upgrade'),
  148                request_header('Upgrade' = websocket),
  149                request_header('Sec-WebSocket-Key' = Key),
  150                request_header('Sec-WebSocket-Version' = 13)
  151              | Options1
  152              ]),
  153    (   Status == 101,
  154        sec_websocket_accept(_{key:Key}, AcceptedKey)
  155    ->  ws_client_options(Selected, WsOptions),
  156        stream_pair(In,  Read, Write),      % Old API: In and Out
  157        stream_pair(Out, Read, Write),      % New API: In == Out (= pair)
  158        ws_open(Read,  WsIn,  WsOptions),
  159        ws_open(Write, WsOut, WsOptions),
  160        stream_pair(WebSocket, WsIn, WsOut)
  161    ;   close(Out),
  162        close(In),
  163        permission_error(open, websocket, URL)
  164    ).
  165
  166ws_client_options('',          [mode(client)]) :- !.
  167ws_client_options(null,        [mode(client)]) :- !.
  168ws_client_options(Subprotocol, [mode(client), subprotocol(Subprotocol)]).
  169
  170add_subprotocols(OptionsIn, OptionsOut) :-
  171    select_option(subprotocols(Subprotocols), OptionsIn, Options1),
  172    !,
  173    must_be(list(atom), Subprotocols),
  174    atomic_list_concat(Subprotocols, ', ', Value),
  175    OptionsOut = [ request_header('Sec-WebSocket-Protocol' = Value)
  176                 | Options1
  177                 ].
  178add_subprotocols(Options, Options).
  179
  180
  181%!  http_upgrade_to_websocket(:Goal, +Options, +Request)
  182%
  183%   Create a websocket connection running call(Goal, WebSocket),
  184%   where WebSocket is a socket-pair.  Options:
  185%
  186%     * guarded(+Boolean)
  187%     If `true` (default), guard the execution of Goal and close
  188%     the websocket on both normal and abnormal termination of Goal.
  189%     If `false`, Goal itself is responsible for the created
  190%     websocket if Goal succeeds. The websocket is closed if Goal
  191%     fails or raises an exception.  This can be used to create a single
  192%     thread that manages multiple websockets using I/O multiplexing.
  193%     See library(http/hub).
  194%
  195%     * subprotocols(+List)
  196%     List of acceptable subprotocols.
  197%
  198%     * timeout(+TimeOut)
  199%     Timeout to apply to the input stream.  Default is =infinite=.
  200%
  201%   Note that the Request argument is  the last for cooperation with
  202%   http_handler/3. A simple _echo_ server that   can be accessed at
  203%   =/ws/= can be implemented as:
  204%
  205%     ==
  206%     :- use_module(library(http/websocket)).
  207%     :- use_module(library(http/thread_httpd)).
  208%     :- use_module(library(http/http_dispatch)).
  209%
  210%     :- http_handler(root(ws),
  211%                     http_upgrade_to_websocket(echo, []),
  212%                     [spawn([])]).
  213%
  214%     echo(WebSocket) :-
  215%         ws_receive(WebSocket, Message),
  216%         (   Message.opcode == close
  217%         ->  true
  218%         ;   ws_send(WebSocket, Message),
  219%             echo(WebSocket)
  220%         ).
  221%     ==
  222%
  223%   @see http_switch_protocol/2.
  224%   @throws switching_protocols(Goal, Options).  The recovery from
  225%           this exception causes the HTTP infrastructure to call
  226%           call(Goal, WebSocket).
  227
  228http_upgrade_to_websocket(Goal, Options, Request) :-
  229    request_websocket_info(Request, Info),
  230    debug(websocket(open), 'Websocket request: ~p', [Info]),
  231    sec_websocket_accept(Info, AcceptKey),
  232    choose_subprotocol(Info, Options, SubProtocol, ExtraHeaders),
  233    debug(websocket(open), 'Subprotocol: ~p', [SubProtocol]),
  234    http_switch_protocol(
  235        open_websocket(Goal, SubProtocol, Options),
  236        [ headers([ upgrade(websocket),
  237                    connection('Upgrade'),
  238                    sec_websocket_accept(AcceptKey)
  239                  | ExtraHeaders
  240                  ])
  241        ]).
  242
  243choose_subprotocol(Info, Options, SubProtocol, ExtraHeaders) :-
  244    HdrValue = Info.get(subprotocols),
  245    option(subprotocols(ServerProtocols), Options),
  246    split_string(HdrValue, ",", " ", RequestProtocols),
  247    member(Protocol, RequestProtocols),
  248    member(SubProtocol, ServerProtocols),
  249    atom_string(SubProtocol, Protocol),
  250    !,
  251    ExtraHeaders = [ 'Sec-WebSocket-Protocol'(SubProtocol) ].
  252choose_subprotocol(_, _, null, []).
  253
  254open_websocket(Goal, SubProtocol, Options, HTTPIn, HTTPOut) :-
  255    option(timeout(TimeOut), Options, infinite),
  256    set_stream(HTTPIn, timeout(TimeOut)),
  257    WsOptions = [mode(server), subprotocol(SubProtocol)],
  258    ws_open(HTTPIn, WsIn, WsOptions),
  259    ws_open(HTTPOut, WsOut, WsOptions),
  260    stream_pair(WebSocket, WsIn, WsOut),
  261    guard_websocket_server(Goal, WebSocket, Options).
  262
  263guard_websocket_server(Goal, WebSocket, Options) :-
  264    (   catch(call(Goal, WebSocket), E, true)
  265    ->  (   var(E)
  266        ->  (   option(guarded(false), Options, true)
  267            ->  Close = false
  268            ;   Msg = bye, Code = 1000
  269            )
  270        ;   message_to_string(E, Msg),
  271            Code = 1011
  272        )
  273    ;   Msg = "goal failed", Code = 1011
  274    ),
  275    (   Close == false
  276    ->  true
  277    ;   catch(ws_close(WebSocket, Code, Msg), Error,
  278              print_message(error, Error))
  279    ).
  280
  281
  282request_websocket_info(Request, Info) :-
  283    option(upgrade(Websocket), Request),
  284    downcase_atom(Websocket, websocket),
  285    option(connection(Connection), Request),
  286    connection_contains_upgrade(Connection),
  287    option(sec_websocket_key(ClientKey), Request),
  288    option(sec_websocket_version(Version), Request),
  289    Info0 = _{key:ClientKey, version:Version},
  290    add_option(origin,                   Request, origin,       Info0, Info1),
  291    add_option(sec_websocket_protocol,   Request, subprotocols, Info1, Info2),
  292    add_option(sec_websocket_extensions, Request, extensions,   Info2, Info).
  293
  294connection_contains_upgrade(Connection) :-
  295    split_string(Connection, ",", " ", Tokens),
  296    member(Token, Tokens),
  297    string_lower(Token, "upgrade"),
  298    !.
  299
  300add_option(OptionName, Request, Key, Dict0, Dict) :-
  301    Option =.. [OptionName,Value],
  302    option(Option, Request),
  303    !,
  304    Dict = Dict0.put(Key,Value).
  305add_option(_, _, _, Dict, Dict).
  306
  307%!  sec_websocket_accept(+Info, -AcceptKey) is det.
  308%
  309%   Compute the accept key as per 4.2.2., point 5.4
  310
  311sec_websocket_accept(Info, AcceptKey) :-
  312    string_concat(Info.key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", Str),
  313    sha_hash(Str, Hash, [ algorithm(sha1) ]),
  314    phrase(base64(Hash), Encoded),
  315    string_codes(AcceptKey, Encoded).
  316
  317
  318                 /*******************************
  319                 *     HIGH LEVEL INTERFACE     *
  320                 *******************************/
  321
  322%!  ws_send(+WebSocket, +Message) is det.
  323%
  324%   Send a message over a websocket. The following terms are allowed
  325%   for Message:
  326%
  327%     - text(+Text)
  328%       Send a text message.  Text is serialized using write/1.
  329%     - binary(+Content)
  330%       As text(+Text), but all character codes produced by Content
  331%       must be in the range [0..255].  Typically, Content will be
  332%       an atom or string holding binary data.
  333%     - prolog(+Term)
  334%       Send a Prolog term as a text message. Text is serialized
  335%       using write_canonical/1.
  336%     - json(+JSON)
  337%       Send the Prolog representation of a JSON term using
  338%       json_write_dict/2.
  339%     - string(+Text)
  340%       Same as text(+Text), provided for consistency.
  341%     - close(+Code, +Text)
  342%       Send a close message.  Code is 1000 for normal close.  See
  343%       websocket documentation for other values.
  344%     - Dict
  345%       A dict that minimally contains an =opcode= key.  Other keys
  346%       used are:
  347%
  348%       - format:Format
  349%         Serialization format used for Message.data. Format is
  350%         one of =string=, =prolog= or =json=.  See ws_receive/3.
  351%
  352%       - data:Term
  353%         If this key is present, it is serialized according
  354%         to Message.format.  Otherwise it is serialized using
  355%         write/1, which implies that string and atoms are just
  356%         sent verbatim.
  357%
  358%   Note that ws_start_message/3 does not unlock the stream. This is
  359%   done by ws_send/1. This implies that   multiple  threads can use
  360%   ws_send/2 and the messages are properly serialized.
  361%
  362%   @tbd    Provide serialization details using options.
  363
  364ws_send(WsStream, Message) :-
  365    message_opcode(Message, OpCode),
  366    setup_call_cleanup(
  367        ws_start_message(WsStream, OpCode, 0),
  368        write_message_data(WsStream, Message),
  369        ws_send(WsStream)).
  370
  371message_opcode(Message, OpCode) :-
  372    is_dict(Message),
  373    !,
  374    to_opcode(Message.opcode, OpCode).
  375message_opcode(Message, OpCode) :-
  376    functor(Message, Name, _),
  377    (   text_functor(Name)
  378    ->  to_opcode(text, OpCode)
  379    ;   to_opcode(Name, OpCode)
  380    ).
  381
  382text_functor(json).
  383text_functor(string).
  384text_functor(prolog).
  385
  386write_message_data(Stream, Message) :-
  387    is_dict(Message),
  388    !,
  389    (   _{code:Code, data:Data} :< Message
  390    ->  write_message_data(Stream, close(Code, Data))
  391    ;   _{format:prolog, data:Data} :< Message
  392    ->  format(Stream, '~k .~n', [Data])
  393    ;   _{format:json, data:Data} :< Message
  394    ->  json_write_dict(Stream, Data)
  395    ;   _{data:Data} :< Message
  396    ->  format(Stream, '~w', Data)
  397    ;   true
  398    ).
  399write_message_data(Stream, Message) :-
  400    functor(Message, Format, 1),
  401    !,
  402    arg(1, Message, Data),
  403    (   text_functor(Format)
  404    ->  write_text_message(Format, Stream, Data)
  405    ;   format(Stream, '~w', [Data])
  406    ).
  407write_message_data(_, Message) :-
  408    atom(Message),
  409    !.
  410write_message_data(Stream, close(Code, Data)) :-
  411    !,
  412    High is (Code >> 8) /\ 0xff,
  413    Low  is Code /\ 0xff,
  414    put_byte(Stream, High),
  415    put_byte(Stream, Low),
  416    stream_pair(Stream, _, Out),
  417    set_stream(Out, encoding(utf8)),
  418    format(Stream, '~w', [Data]).
  419write_message_data(_, Message) :-
  420    type_error(websocket_message, Message).
  421
  422write_text_message(json, Stream, Data) :-
  423    !,
  424    json_write_dict(Stream, Data).
  425write_text_message(prolog, Stream, Data) :-
  426    !,
  427    format(Stream, '~k .', [Data]).
  428write_text_message(_, Stream, Data) :-
  429    format(Stream, '~w', [Data]).
  430
  431
  432
  433%!  ws_receive(+WebSocket, -Message:dict) is det.
  434%!  ws_receive(+WebSocket, -Message:dict, +Options) is det.
  435%
  436%   Receive the next message  from  WebSocket.   Message  is  a dict
  437%   containing the following keys:
  438%
  439%     - opcode:OpCode
  440%       OpCode of the message.  This is an atom for known opcodes
  441%       and an integer for unknown ones.  If the peer closed the
  442%       stream, OpCode is bound to =close= and data to the atom
  443%       =end_of_file=.
  444%     - data:String
  445%       The data, represented as a string.  This field is always
  446%       present.  String is the empty string if there is no data
  447%       in the message.
  448%     - rsv:RSV
  449%       Present if the WebSocket RSV header is not 0. RSV is an
  450%       integer in the range [1..7].
  451%
  452%   If =ping= message is received and   WebSocket  is a stream pair,
  453%   ws_receive/1 replies with a  =pong=  and   waits  for  the  next
  454%   message.
  455%
  456%   The predicate ws_receive/3 processes the following options:
  457%
  458%     - format(+Format)
  459%     Defines how _text_ messages are parsed.  Format is one of
  460%       - string
  461%       Data is returned as a Prolog string (default)
  462%       - json
  463%       Data is parsed using json_read_dict/3, which also receives
  464%       Options.
  465%       - prolog
  466%       Data is parsed using read_term/3, which also receives
  467%       Options.
  468%
  469%   @tbd    Add a hook to allow for more data formats?
  470
  471ws_receive(WsStream, Message) :-
  472    ws_receive(WsStream, Message, []).
  473
  474ws_receive(WsStream, Message, Options) :-
  475    ws_read_header(WsStream, Code, RSV),
  476    debug(websocket, 'ws_receive(~p): OpCode=~w, RSV=~w',
  477          [WsStream, Code, RSV]),
  478    (   Code == end_of_file
  479    ->  Message = websocket{opcode:close, data:end_of_file}
  480    ;   (   ws_opcode(OpCode, Code)
  481        ->  true
  482        ;   OpCode = Code
  483        ),
  484        read_data(OpCode, WsStream, Data, Options),
  485        (   OpCode == ping,
  486            reply_pong(WsStream, Data.data)
  487        ->  ws_receive(WsStream, Message, Options)
  488        ;   (   RSV == 0
  489            ->  Message = Data
  490            ;   Message = Data.put(rsv, RSV)
  491            )
  492        )
  493    ),
  494    debug(websocket, 'ws_receive(~p) --> ~p', [WsStream, Message]).
  495
  496read_data(close, WsStream,
  497          websocket{opcode:close, code:Code, format:string, data:Data}, _Options) :-
  498    !,
  499    get_byte(WsStream, High),
  500    (   High == -1
  501    ->  Code = 1000,
  502        Data = ""
  503    ;   get_byte(WsStream, Low),
  504        Code is High<<8 \/ Low,
  505        stream_pair(WsStream, In, _),
  506        set_stream(In, encoding(utf8)),
  507        read_string(WsStream, _Len, Data)
  508    ).
  509read_data(text, WsStream, Data, Options) :-
  510    !,
  511    option(format(Format), Options, string),
  512    read_text_data(Format, WsStream, Data, Options).
  513read_data(OpCode, WsStream, websocket{opcode:OpCode, format:string, data:Data}, _Options) :-
  514    read_string(WsStream, _Len, Data).
  515
  516%!  read_text_data(+Format, +WsStream, -Dict, +Options) is det.
  517%
  518%   Read a websocket message into   a  dict websocket{opcode:OpCode,
  519%   data:Data}, where Data is parsed according to Format.
  520
  521read_text_data(string, WsStream,
  522          websocket{opcode:text, format:string, data:Data}, _Options) :-
  523    !,
  524    read_string(WsStream, _Len, Data).
  525read_text_data(json, WsStream,
  526          websocket{opcode:text, format:json,   data:Data}, Options) :-
  527    !,
  528    json_read_dict(WsStream, Data, Options).
  529read_text_data(prolog, WsStream,
  530          websocket{opcode:text, format:prolog, data:Data}, Options) :-
  531    !,
  532    read_term(WsStream, Data, Options).
  533read_text_data(Format, _, _, _) :-
  534    domain_error(format, Format).
  535
  536reply_pong(WebSocket, Data) :-
  537    stream_pair(WebSocket, _In, Out),
  538    is_stream(Out),
  539    ws_send(Out, pong(Data)).
  540
  541
  542%!  ws_close(+WebSocket:stream_pair, +Code, +Data) is det.
  543%
  544%   Close a WebSocket connection by sending a =close= message if
  545%   this was not already sent and wait for the close reply.
  546%
  547%   @arg    Code is the numerical code indicating the close status.
  548%           This is 16-bit integer.  The codes are defined in
  549%           section _|7.4.1. Defined Status Codes|_ of RFC6455.
  550%           Notably, 1000 indicates a normal closure.
  551%   @arg    Data is currently interpreted as text.
  552%   @error  websocket_error(unexpected_message, Reply) if
  553%           the other side did not send a close message in reply.
  554
  555ws_close(WebSocket, Code, Data) :-
  556    setup_call_cleanup(
  557        true,
  558        ws_close_(WebSocket, Code, Data),
  559        close(WebSocket)).
  560
  561ws_close_(WebSocket, Code, Data) :-
  562    stream_pair(WebSocket, In, Out),
  563    (   (   var(Out)
  564        ;   ws_property(Out, status, closed)
  565        )
  566    ->  debug(websocket(close),
  567              'Output stream of ~p already closed', [WebSocket])
  568    ;   ws_send(WebSocket, close(Code, Data)),
  569        close(Out),
  570        debug(websocket(close), '~p: closed output', [WebSocket]),
  571        (   (   var(In)
  572            ;   ws_property(In, status, closed)
  573            )
  574        ->  debug(websocket(close),
  575                  'Input stream of ~p already closed', [WebSocket])
  576        ;   ws_receive(WebSocket, Reply),
  577            (   Reply.opcode == close
  578            ->  debug(websocket(close), '~p: close confirmed', [WebSocket])
  579            ;   throw(error(websocket_error(unexpected_message, Reply), _))
  580            )
  581        )
  582    ).
  583
  584
  585%!  ws_open(+Stream, -WSStream, +Options) is det.
  586%
  587%   Turn a raw TCP/IP (or any other  binary stream) into a websocket
  588%   stream. Stream can be an input stream, output stream or a stream
  589%   pair. Options includes
  590%
  591%     * mode(+Mode)
  592%     One of =server= or =client=.  If =client=, messages are sent
  593%     as _masked_.
  594%
  595%     * buffer_size(+Count)
  596%     Send partial messages for each Count bytes or when flushing
  597%     the output. The default is to buffer the entire message before
  598%     it is sent.
  599%
  600%     * close_parent(+Boolean)
  601%     If =true= (default), closing WSStream also closes Stream.
  602%
  603%     * subprotocol(+Protocol)
  604%     Set the subprotocol property of WsStream.  This value can be
  605%     retrieved using ws_property/2.  Protocol is an atom.  See
  606%     also the =subprotocols= option of http_open_websocket/3 and
  607%     http_upgrade_to_websocket/3.
  608%
  609%   A typical sequence to turn a pair of streams into a WebSocket is
  610%   here:
  611%
  612%     ==
  613%         ...,
  614%         Options = [mode(server), subprotocol(chat)],
  615%         ws_open(Input, WsInput, Options),
  616%         ws_open(Output, WsOutput, Options),
  617%         stream_pair(WebSocket, WsInput, WsOutput).
  618%     ==
  619
  620%!  ws_start_message(+WSStream, +OpCode) is det.
  621%!  ws_start_message(+WSStream, +OpCode, +RSV) is det.
  622%
  623%   Prepare for sending a new  message.   OpCode  is  one of =text=,
  624%   =binary=,  =close=,  =ping=  or  =pong=.  RSV  is  reserved  for
  625%   extensions. After this call, the application usually writes data
  626%   to  WSStream  and  uses  ws_send/1   to  complete  the  message.
  627%   Depending on OpCode, the stream  is   switched  to _binary_ (for
  628%   OpCode is =binary=) or _text_ using   =utf8= encoding (all other
  629%   OpCode values). For example,  to  a   JSON  message  can be send
  630%   using:
  631%
  632%     ==
  633%     ws_send_json(WSStream, JSON) :-
  634%        ws_start_message(WSStream, text),
  635%        json_write(WSStream, JSON),
  636%        ws_send(WSStream).
  637%     ==
  638
  639%!  ws_send(+WSStream) is det.
  640%
  641%   Complete and send the WebSocket message.   If  the OpCode of the
  642%   message is =close=, close the stream.
  643
  644%!  ws_read_header(+WSStream, -OpCode, -RSV) is det.
  645%
  646%   Read the header of the WebSocket  next message. After this call,
  647%   WSStream is switched to  the   appropriate  encoding and reading
  648%   from the stream will  signal  end-of-file   at  the  end  of the
  649%   message.  Note  that  this  end-of-file  does  *not*  invalidate
  650%   WSStream.  Reading may perform various tasks on the background:
  651%
  652%     - If the message has _Fin_ is =false=, it will wait for an
  653%       additional message.
  654%     - If a =ping= is received, it will reply with a =pong= on the
  655%       matching output stream.
  656%     - If a =pong= is received, it will be ignored.
  657%     - If a =close= is received and a partial message is read,
  658%       it generates an exception (TBD: which?).  If no partial
  659%       message is received, it unified OpCode with =close= and
  660%       replies with a =close= message.
  661%
  662%   If not all data has been read  for the previous message, it will
  663%   first read the remainder of the  message. This input is silently
  664%   discarded. This allows for  trailing   white  space after proper
  665%   text messages such as JSON, Prolog or XML terms. For example, to
  666%   read a JSON message, use:
  667%
  668%     ==
  669%     ws_read_json(WSStream, JSON) :-
  670%         ws_read_header(WSStream, OpCode, RSV),
  671%         (   OpCode == text,
  672%             RSV == 0
  673%         ->  json_read(WSStream, JSON)
  674%         ;   OpCode == close
  675%         ->  JSON = end_of_file
  676%         ).
  677%     ==
  678
  679%!  ws_property(+WebSocket, ?Property) is nondet.
  680%
  681%   True if Property is  a   property  WebSocket. Defined properties
  682%   are:
  683%
  684%     * subprotocol(Protocol)
  685%     Protocol is the negotiated subprotocol. This is typically set
  686%     as a property of the websocket by ws_open/3.
  687
  688ws_property(WebSocket, Property) :-
  689    ws_property_(Property, WebSocket).
  690
  691ws_property_(subprotocol(Protocol), WebSocket) :-
  692    ws_property(WebSocket, subprotocol, Protocol).
  693
  694%!  to_opcode(+Spec, -OpCode:int) is det.
  695%
  696%   Convert a specification of an opcode into the numeric opcode.
  697
  698to_opcode(In, Code) :-
  699    integer(In),
  700    !,
  701    must_be(between(0, 15), In),
  702    Code = In.
  703to_opcode(Name, Code) :-
  704    must_be(atom, Name),
  705    (   ws_opcode(Name, Code)
  706    ->  true
  707    ;   domain_error(ws_opcode, Name)
  708    ).
  709
  710%!  ws_opcode(?Name, ?Code)
  711%
  712%   Define symbolic names for the WebSocket opcodes.
  713
  714ws_opcode(continuation, 0).
  715ws_opcode(text,         1).
  716ws_opcode(binary,       2).
  717ws_opcode(close,        8).
  718ws_opcode(ping,         9).
  719ws_opcode(pong,         10).
  720
  721
  722%!  ws_mask(-Mask)
  723%
  724%   Produce a good random number of the mask of a client message.
  725
  726:- public ws_mask/1.  727
  728ws_mask(Mask) :-
  729    Mask is 1+random(1<<32-1)