├── rebar ├── .gitignore ├── src ├── sockjs_service.erl ├── sockjs.app.src ├── sockjs_app.erl ├── sockjs_json.erl ├── sockjs_multiplex_channel.erl ├── sockjs_session_sup.erl ├── sockjs.erl ├── sockjs_internal.hrl ├── sockjs_util.erl ├── sockjs_ws_handler.erl ├── sockjs_cowboy_handler.erl ├── sockjs_multiplex.erl ├── sockjs_filters.erl ├── sockjs_http.erl ├── sockjs_handler.erl ├── mochinum_fork.erl ├── sockjs_action.erl ├── sockjs_session.erl ├── sockjs_pmod_pt.erl └── mochijson2_fork.erl ├── rebar.config ├── COPYING ├── Changelog ├── LICENSE-MIT-SockJS ├── LICENSE-MIT-Mochiweb ├── Makefile ├── examples ├── cowboy_echo.erl ├── echo.html ├── multiplex │ ├── multiplex.js │ ├── cowboy_multiplex.erl │ └── index.html └── cowboy_test_server.erl ├── README.md ├── LICENSE-APL2-Rebar └── LICENSE-EPL-OTP /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sockjs/sockjs-erlang/HEAD/rebar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | deps/* 2 | ebin/* 3 | priv/www 4 | *~ 5 | .pidfile.pid 6 | .dialyzer_sockjs.plt 7 | .dialyzer_generic.plt 8 | -------------------------------------------------------------------------------- /src/sockjs_service.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_service). 2 | 3 | -export([behaviour_info/1]). 4 | 5 | behaviour_info(callbacks) -> 6 | [ 7 | {sockjs_init, 2}, 8 | {sockjs_handle, 3}, 9 | {sockjs_terminate, 2} 10 | ]; 11 | 12 | behaviour_info(_Other) -> 13 | undefined. 14 | -------------------------------------------------------------------------------- /src/sockjs.app.src: -------------------------------------------------------------------------------- 1 | {application, sockjs, 2 | [ 3 | {description, "SockJS"}, 4 | {vsn, "0.3.4"}, 5 | {modules, []}, 6 | {registered, []}, 7 | {applications, [ 8 | kernel, 9 | stdlib, 10 | xmerl 11 | ]}, 12 | {mod, { sockjs_app, []}} 13 | ]}. 14 | -------------------------------------------------------------------------------- /src/sockjs_app.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_app). 2 | 3 | -behaviour(application). 4 | 5 | -export([start/2, stop/1]). 6 | 7 | -spec start(_, _) -> {ok, pid()}. 8 | start(_StartType, _StartArgs) -> 9 | sockjs_session:init(), 10 | sockjs_session_sup:start_link(). 11 | 12 | -spec stop(_) -> ok. 13 | stop(_State) -> 14 | ok. 15 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | %% This is the default `rebar.config` for SockJS-erlang. 3 | %% 4 | 5 | {erl_opts, [ 6 | %% fail_on_warning, 7 | %bin_opt_info, 8 | %warn_missing_spec, 9 | debug_info, 10 | warn_export_all 11 | ]}. 12 | 13 | {deps, [ 14 | {cowboy, "0.8.3",{git, "https://github.com/extend/cowboy.git", "0.8.3"}} 15 | ]}. 16 | -------------------------------------------------------------------------------- /src/sockjs_json.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_json). 2 | 3 | -export([encode/1, decode/1]). 4 | 5 | %% -------------------------------------------------------------------------- 6 | 7 | -spec encode(any()) -> iodata(). 8 | encode(Thing) -> 9 | mochijson2_fork:encode(Thing). 10 | 11 | -spec decode(iodata()) -> {ok, any()} | {error, any()}. 12 | decode(Encoded) -> 13 | try mochijson2_fork:decode(Encoded) of 14 | V -> {ok, V} 15 | catch 16 | _:E -> {error, E} 17 | end. 18 | -------------------------------------------------------------------------------- /src/sockjs_multiplex_channel.erl: -------------------------------------------------------------------------------- 1 | -compile({parse_transform,sockjs_pmod_pt}). 2 | -module(sockjs_multiplex_channel, [Conn, Topic]). 3 | 4 | -export([send/1, close/0, close/2, info/0]). 5 | 6 | send(Data) -> 7 | Conn:send(iolist_to_binary(["msg", ",", Topic, ",", Data])). 8 | 9 | close() -> 10 | close(1000, "Normal closure"). 11 | 12 | close(_Code, _Reason) -> 13 | Conn:send(iolist_to_binary(["uns", ",", Topic])). 14 | 15 | info() -> 16 | Conn:info() ++ [{topic, Topic}]. 17 | 18 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | All code is released under the MIT license (see LICENSE-MIT-SockJS) 2 | with the exception of following files: 3 | 4 | * src/mochijson2_fork.erl and src/mochinum_fork.erl, which are forked 5 | from Mochiweb project (https://github.com/mochi/mochiweb) and 6 | covered by LICENSE-MIT-Mochiweb. 7 | 8 | * src/sockjs_pmod_pt.erl, which is from OTP's release of pmod_transform 9 | (https://github.com/erlang/pmod_transform) and covered by LICENSE-EPL-OTP. 10 | 11 | * rebar, which is a compiled binary from Rebar project 12 | (https://github.com/basho/rebar) and covered by LICENSE-APL2-Rebar. 13 | -------------------------------------------------------------------------------- /src/sockjs_session_sup.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_session_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | -export([start_link/0, start_child/3]). 6 | -export([init/1]). 7 | 8 | %% -------------------------------------------------------------------------- 9 | 10 | -spec start_link() -> ignore | {'ok', pid()} | {'error', any()}. 11 | start_link() -> 12 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 13 | 14 | init([]) -> 15 | {ok, {{simple_one_for_one, 10, 10}, 16 | [{undefined, {sockjs_session, start_link, []}, 17 | transient, 5000, worker, [sockjs_session]}]}}. 18 | 19 | start_child(SessionId, Service, Info) -> 20 | supervisor:start_child(?MODULE, [SessionId, Service, Info]). 21 | -------------------------------------------------------------------------------- /src/sockjs.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs). 2 | 3 | -export([send/2, close/1, close/3, info/1]). 4 | 5 | -type(conn() :: {sockjs_session, any()}). 6 | 7 | %% Send data over a connection. 8 | -spec send(iodata(), conn()) -> ok. 9 | send(Data, Conn = {sockjs_session, _}) -> 10 | sockjs_session:send(Data, Conn). 11 | 12 | %% Initiate a close of a connection. 13 | -spec close(conn()) -> ok. 14 | close(Conn) -> 15 | close(1000, "Normal closure", Conn). 16 | 17 | -spec close(non_neg_integer(), string(), conn()) -> ok. 18 | close(Code, Reason, Conn = {sockjs_session, _}) -> 19 | sockjs_session:close(Code, Reason, Conn). 20 | 21 | -spec info(conn()) -> [{atom(), any()}]. 22 | info(Conn = {sockjs_session, _}) -> 23 | sockjs_session:info(Conn). 24 | 25 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | 0.3.4 2 | ===== 3 | 4 | * #41 - fix a traceback when websocket is too slow or too 5 | busy to prompty process incoming and outgoing data 6 | * #37 - make porting to new cowboy a tiny bit easier 7 | (allow 'method' to be a binary) 8 | 9 | 10 | 0.3.3 11 | ===== 12 | 13 | * sockjs/sockjs-protocol#56 Fix for iOS 6 caching POSTs 14 | 15 | 16 | 0.3.0 17 | ===== 18 | 19 | * Fixed {odd_info, heartbeat_triggered} exception (Isaev Ivan) 20 | * Changes to pass sockjs-protocol-0.3 21 | * Fixed sockname badmatch (Egobrain) 22 | * Updated README 23 | * Introduced parametrized module API (to get multiplexer working). 24 | * Introduced Multiplexer example. 25 | * Fixed invalid catch in sockjs_json:decode (Isaev Ivan) 26 | * Bumped Cowboy version. 27 | * Specs were moved around to make R13 happy 28 | * Dropped milsultin support. 29 | 30 | 31 | -------------------------------------------------------------------------------- /LICENSE-MIT-SockJS: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 VMware, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /LICENSE-MIT-Mochiweb: -------------------------------------------------------------------------------- 1 | This is the MIT license. 2 | 3 | Copyright (c) 2007 Mochi Media, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/sockjs_internal.hrl: -------------------------------------------------------------------------------- 1 | 2 | -type(req() :: {cowboy, any()}). 3 | 4 | -type(user_session() :: nonempty_string()). 5 | -type(emittable() :: init|closed|{recv, binary()}). 6 | -type(callback() :: fun((user_session(), emittable(), any()) -> ok)). 7 | -type(logger() :: fun((any(), req(), websocket|http) -> req())). 8 | 9 | -record(service, {prefix :: nonempty_string(), 10 | callback :: callback(), 11 | state :: any(), 12 | sockjs_url :: nonempty_string(), 13 | cookie_needed :: boolean(), 14 | websocket :: boolean(), 15 | disconnect_delay :: non_neg_integer(), 16 | heartbeat_delay :: non_neg_integer(), 17 | response_limit :: non_neg_integer(), 18 | logger :: logger() 19 | }). 20 | 21 | -type(service() :: #service{}). 22 | 23 | -type(headers() :: list({nonempty_string(), nonempty_string()})). 24 | -type(server() :: nonempty_string()). 25 | -type(session() :: nonempty_string()). 26 | 27 | -type(frame() :: {open, nil} | 28 | {close, {non_neg_integer(), string()}} | 29 | {data, list(iodata())} | 30 | {heartbeat, nil} ). 31 | 32 | -type(info() :: [{atom(), any()}]). 33 | -------------------------------------------------------------------------------- /src/sockjs_util.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_util). 2 | 3 | -export([rand32/0]). 4 | -export([encode_frame/1]). 5 | -export([url_escape/2]). 6 | 7 | -include("sockjs_internal.hrl"). 8 | 9 | %% -------------------------------------------------------------------------- 10 | 11 | -spec rand32() -> non_neg_integer(). 12 | rand32() -> 13 | case get(random_seeded) of 14 | undefined -> 15 | {MegaSecs, Secs, MicroSecs} = now(), 16 | _ = random:seed(MegaSecs, Secs, MicroSecs), 17 | put(random_seeded, true); 18 | _Else -> 19 | ok 20 | end, 21 | random:uniform(erlang:trunc(math:pow(2,32)))-1. 22 | 23 | 24 | -spec encode_frame(frame()) -> iodata(). 25 | encode_frame({open, nil}) -> 26 | <<"o">>; 27 | encode_frame({close, {Code, Reason}}) -> 28 | [<<"c">>, 29 | sockjs_json:encode([Code, list_to_binary(Reason)])]; 30 | encode_frame({data, L}) -> 31 | [<<"a">>, 32 | sockjs_json:encode([iolist_to_binary(D) || D <- L])]; 33 | encode_frame({heartbeat, nil}) -> 34 | <<"h">>. 35 | 36 | 37 | -spec url_escape(string(), string()) -> iolist(). 38 | url_escape(Str, Chars) -> 39 | [case lists:member(Char, Chars) of 40 | true -> hex(Char); 41 | false -> Char 42 | end || Char <- Str]. 43 | 44 | hex(C) -> 45 | <> = <>, 46 | High = integer_to_list(High0), 47 | Low = integer_to_list(Low0), 48 | "%" ++ High ++ Low. 49 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR=./rebar 2 | 3 | .PHONY: all clean distclean 4 | all: deps 5 | $(REBAR) compile 6 | 7 | deps: 8 | $(REBAR) get-deps 9 | 10 | clean:: 11 | $(REBAR) clean 12 | rm -rf priv/www 13 | 14 | distclean:: 15 | rm -rf deps priv ebin 16 | 17 | 18 | # **** serve **** 19 | 20 | .PHONY: serve 21 | SERVE_SCRIPT=./examples/cowboy_test_server.erl 22 | serve: 23 | @if [ -e .pidfile.pid ]; then \ 24 | kill `cat .pidfile.pid`; \ 25 | rm .pidfile.pid; \ 26 | fi 27 | 28 | @while [ 1 ]; do \ 29 | $(REBAR) compile && ( \ 30 | echo " [*] Running erlang"; \ 31 | $(SERVE_SCRIPT) & \ 32 | SRVPID=$$!; \ 33 | echo $$SRVPID > .pidfile.pid; \ 34 | echo " [*] Pid: $$SRVPID"; \ 35 | ); \ 36 | inotifywait -r -q -e modify src/*erl examples/*erl src/*hrl; \ 37 | test -e .pidfile.pid && kill `cat .pidfile.pid`; \ 38 | rm -f .pidfile.pid; \ 39 | sleep 0.1; \ 40 | done 41 | 42 | 43 | # **** dialyzer **** 44 | 45 | .dialyzer_generic.plt: 46 | dialyzer \ 47 | --build_plt \ 48 | --output_plt .dialyzer_generic.plt \ 49 | --apps erts kernel stdlib compiler sasl os_mon mnesia \ 50 | tools public_key crypto ssl 51 | 52 | .dialyzer_sockjs.plt: .dialyzer_generic.plt 53 | dialyzer \ 54 | --no_native \ 55 | --add_to_plt \ 56 | --plt .dialyzer_generic.plt \ 57 | --output_plt .dialyzer_sockjs.plt -r deps/*/ebin 58 | 59 | distclean:: 60 | rm -f .dialyzer_sockjs.plt 61 | 62 | dialyze: .dialyzer_sockjs.plt 63 | @dialyzer \ 64 | --plt .dialyzer_sockjs.plt \ 65 | --no_native \ 66 | --fullpath \ 67 | -Wrace_conditions \ 68 | -Werror_handling \ 69 | -Wunmatched_returns \ 70 | ebin 71 | 72 | .PHONY: xref 73 | xref: 74 | $(REBAR) xref | egrep -v unused 75 | 76 | 77 | # **** release **** 78 | # 1. Commit 79 | # 2. Bump version in "src/sockjs.app.src" 80 | # 3. git tag -s "vx.y.z" -m "Release vx.y.z" 81 | -------------------------------------------------------------------------------- /examples/cowboy_echo.erl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -smp disable +A1 +K true -pa ebin -env ERL_LIBS deps -input 3 | -module(cowboy_echo). 4 | -mode(compile). 5 | 6 | -export([main/1]). 7 | 8 | %% Cowboy callbacks 9 | -export([init/3, handle/2, terminate/3]). 10 | 11 | 12 | main(_) -> 13 | Port = 8081, 14 | ok = application:start(xmerl), 15 | ok = application:start(sockjs), 16 | ok = application:start(ranch), 17 | ok = application:start(crypto), 18 | ok = application:start(cowboy), 19 | 20 | SockjsState = sockjs_handler:init_state( 21 | <<"/echo">>, fun service_echo/3, state, []), 22 | 23 | VhostRoutes = [{<<"/echo/[...]">>, sockjs_cowboy_handler, SockjsState}, 24 | {'_', ?MODULE, []}], 25 | Routes = [{'_', VhostRoutes}], % any vhost 26 | Dispatch = cowboy_router:compile(Routes), 27 | 28 | io:format(" [*] Running at http://localhost:~p~n", [Port]), 29 | cowboy:start_http(cowboy_echo_http_listener, 100, 30 | [{port, Port}], 31 | [{env, [{dispatch, Dispatch}]}]), 32 | receive 33 | _ -> ok 34 | end. 35 | 36 | %% -------------------------------------------------------------------------- 37 | 38 | init({_Any, http}, Req, []) -> 39 | {ok, Req, []}. 40 | 41 | handle(Req, State) -> 42 | {ok, Data} = file:read_file("./examples/echo.html"), 43 | {ok, Req1} = cowboy_req:reply(200, [{<<"Content-Type">>, "text/html"}], 44 | Data, Req), 45 | {ok, Req1, State}. 46 | 47 | terminate(_Reason, _Req, _State) -> 48 | ok. 49 | 50 | %% -------------------------------------------------------------------------- 51 | 52 | service_echo(_Conn, init, state) -> {ok, state}; 53 | service_echo(Conn, {recv, Data}, state) -> Conn:send(Data); 54 | service_echo(_Conn, {info, _Info}, state) -> {ok, state}; 55 | service_echo(_Conn, closed, state) -> {ok, state}. 56 | -------------------------------------------------------------------------------- /src/sockjs_ws_handler.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_ws_handler). 2 | 3 | -export([received/3, reply/2, close/2]). 4 | 5 | -include("sockjs_internal.hrl"). 6 | 7 | %% -------------------------------------------------------------------------- 8 | 9 | -spec received(websocket|rawwebsocket, pid(), binary()) -> ok | shutdown. 10 | %% Ignore empty 11 | received(_RawWebsocket, _SessionPid, <<>>) -> 12 | ok; 13 | received(websocket, SessionPid, Data) -> 14 | case sockjs_json:decode(Data) of 15 | {ok, Msg} when is_binary(Msg) -> 16 | session_received([Msg], SessionPid); 17 | {ok, Messages} when is_list(Messages) -> 18 | session_received(Messages, SessionPid); 19 | _Else -> 20 | shutdown 21 | end; 22 | 23 | received(rawwebsocket, SessionPid, Data) -> 24 | session_received([Data], SessionPid). 25 | 26 | session_received(Messages, SessionPid) -> 27 | try sockjs_session:received(Messages, SessionPid) of 28 | ok -> ok 29 | catch 30 | no_session -> shutdown 31 | end. 32 | 33 | -spec reply(websocket|rawwebsocket, pid()) -> {close|open, binary()} | wait. 34 | reply(websocket, SessionPid) -> 35 | case sockjs_session:reply(SessionPid) of 36 | {W, Frame} when W =:= ok orelse W =:= close-> 37 | Frame1 = sockjs_util:encode_frame(Frame), 38 | {W, iolist_to_binary(Frame1)}; 39 | wait -> 40 | wait 41 | end; 42 | reply(rawwebsocket, SessionPid) -> 43 | case sockjs_session:reply(SessionPid, false) of 44 | {W, Frame} when W =:= ok orelse W =:= close-> 45 | case Frame of 46 | {open, nil} -> reply(rawwebsocket, SessionPid); 47 | {close, {_Code, _Reason}} -> {close, <<>>}; 48 | {data, [Msg]} -> {ok, iolist_to_binary(Msg)}; 49 | {heartbeat, nil} -> reply(rawwebsocket, SessionPid) 50 | end; 51 | wait -> 52 | wait 53 | end. 54 | 55 | -spec close(websocket|rawwebsocket, pid()) -> ok. 56 | close(_RawWebsocket, SessionPid) -> 57 | SessionPid ! force_shutdown, 58 | ok. 59 | -------------------------------------------------------------------------------- /examples/echo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 32 | 33 |

SockJS-erlang Echo example

34 |
35 | 37 |
38 |
39 | 72 | 73 | -------------------------------------------------------------------------------- /examples/multiplex/multiplex.js: -------------------------------------------------------------------------------- 1 | // **** 2 | 3 | var DumbEventTarget = function() { 4 | this._listeners = {}; 5 | }; 6 | DumbEventTarget.prototype._ensure = function(type) { 7 | if(!(type in this._listeners)) this._listeners[type] = []; 8 | }; 9 | DumbEventTarget.prototype.addEventListener = function(type, listener) { 10 | this._ensure(type); 11 | this._listeners[type].push(listener); 12 | }; 13 | DumbEventTarget.prototype.emit = function(type) { 14 | this._ensure(type); 15 | var args = Array.prototype.slice.call(arguments, 1); 16 | if(this['on' + type]) this['on' + type].apply(this, args); 17 | for(var i=0; i < this._listeners[type].length; i++) { 18 | this._listeners[type][i].apply(this, args); 19 | } 20 | }; 21 | 22 | 23 | // **** 24 | 25 | var MultiplexedWebSocket = function(ws) { 26 | var that = this; 27 | this.ws = ws; 28 | this.channels = {}; 29 | this.ws.addEventListener('message', function(e) { 30 | var t = e.data.split(','); 31 | var type = t.shift(), name = t.shift(), payload = t.join(); 32 | if(!(name in that.channels)) { 33 | return; 34 | } 35 | var sub = that.channels[name]; 36 | 37 | switch(type) { 38 | case 'uns': 39 | delete that.channels[name]; 40 | sub.emit('close', {}); 41 | break; 42 | case 'msg': 43 | sub.emit('message', {data: payload}); 44 | break 45 | } 46 | }); 47 | }; 48 | MultiplexedWebSocket.prototype.channel = function(raw_name) { 49 | return this.channels[escape(raw_name)] = 50 | new Channel(this.ws, escape(raw_name), this.channels); 51 | }; 52 | 53 | 54 | var Channel = function(ws, name, channels) { 55 | DumbEventTarget.call(this); 56 | var that = this; 57 | this.ws = ws; 58 | this.name = name; 59 | this.channels = channels; 60 | var onopen = function() { 61 | that.ws.send('sub,' + that.name); 62 | that.emit('open'); 63 | }; 64 | if(ws.readyState > 0) { 65 | setTimeout(onopen, 0); 66 | } else { 67 | this.ws.addEventListener('open', onopen); 68 | } 69 | }; 70 | Channel.prototype = new DumbEventTarget() 71 | 72 | Channel.prototype.send = function(data) { 73 | this.ws.send('msg,' + this.name + ',' + data); 74 | }; 75 | Channel.prototype.close = function() { 76 | var that = this; 77 | this.ws.send('uns,' + this.name); 78 | delete this.channels[this.name]; 79 | setTimeout(function(){that.emit('close', {})},0); 80 | }; 81 | -------------------------------------------------------------------------------- /src/sockjs_cowboy_handler.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_cowboy_handler). 2 | -behaviour(cowboy_http_handler). 3 | -behaviour(cowboy_websocket_handler). 4 | 5 | %% Cowboy http callbacks 6 | -export([init/3, handle/2, terminate/3]). 7 | 8 | %% Cowboy ws callbacks 9 | -export([websocket_init/3, websocket_handle/3, 10 | websocket_info/3, websocket_terminate/3]). 11 | 12 | -include("sockjs_internal.hrl"). 13 | 14 | %% -------------------------------------------------------------------------- 15 | 16 | init({_Any, http}, Req, Service) -> 17 | case sockjs_handler:is_valid_ws(Service, {cowboy, Req}) of 18 | {true, {cowboy, _Req1}, _Reason} -> 19 | {upgrade, protocol, cowboy_websocket}; 20 | {false, {cowboy, Req1}, _Reason} -> 21 | {ok, Req1, Service} 22 | end. 23 | 24 | handle(Req, Service) -> 25 | {cowboy, Req3} = sockjs_handler:handle_req(Service, {cowboy, Req}), 26 | {ok, Req3, Service}. 27 | 28 | terminate(_Reason, _Req, _Service) -> 29 | ok. 30 | 31 | %% -------------------------------------------------------------------------- 32 | 33 | websocket_init(_TransportName, Req, Service = #service{logger = Logger}) -> 34 | Req0 = Logger(Service, {cowboy, Req}, websocket), 35 | 36 | Service1 = Service#service{disconnect_delay = 5*60*1000}, 37 | 38 | {Info, Req1} = sockjs_handler:extract_info(Req0), 39 | SessionPid = sockjs_session:maybe_create(undefined, Service1, Info), 40 | {RawWebsocket, {cowboy, Req3}} = 41 | case sockjs_handler:get_action(Service, Req1) of 42 | {{match, WS}, Req2} when WS =:= websocket orelse 43 | WS =:= rawwebsocket -> 44 | {WS, Req2} 45 | end, 46 | self() ! go, 47 | {ok, Req3, {RawWebsocket, SessionPid}}. 48 | 49 | websocket_handle({text, Data}, Req, {RawWebsocket, SessionPid} = S) -> 50 | case sockjs_ws_handler:received(RawWebsocket, SessionPid, Data) of 51 | ok -> {ok, Req, S}; 52 | shutdown -> {shutdown, Req, S} 53 | end; 54 | websocket_handle(_Unknown, Req, S) -> 55 | {shutdown, Req, S}. 56 | 57 | websocket_info(go, Req, {RawWebsocket, SessionPid} = S) -> 58 | case sockjs_ws_handler:reply(RawWebsocket, SessionPid) of 59 | wait -> {ok, Req, S}; 60 | {ok, Data} -> self() ! go, 61 | {reply, {text, Data}, Req, S}; 62 | {close, <<>>} -> {shutdown, Req, S}; 63 | {close, Data} -> self() ! shutdown, 64 | {reply, {text, Data}, Req, S} 65 | end; 66 | websocket_info(shutdown, Req, S) -> 67 | {shutdown, Req, S}. 68 | 69 | websocket_terminate(_Reason, _Req, {RawWebsocket, SessionPid}) -> 70 | sockjs_ws_handler:close(RawWebsocket, SessionPid), 71 | ok. 72 | -------------------------------------------------------------------------------- /src/sockjs_multiplex.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_multiplex). 2 | 3 | -behaviour(sockjs_service). 4 | 5 | -export([init_state/1]). 6 | -export([sockjs_init/2, sockjs_handle/3, sockjs_terminate/2]). 7 | 8 | -record(service, {callback, state, vconn}). 9 | 10 | %% -------------------------------------------------------------------------- 11 | 12 | init_state(Services) -> 13 | L = [{Topic, #service{callback = Callback, state = State}} || 14 | {Topic, Callback, State} <- Services], 15 | {orddict:from_list(L), orddict:new()}. 16 | 17 | 18 | 19 | sockjs_init(_Conn, {_Services, _Channels} = S) -> 20 | {ok, S}. 21 | 22 | sockjs_handle(Conn, Data, {Services, Channels}) -> 23 | [Type, Topic, Payload] = split($,, binary_to_list(Data), 3), 24 | case orddict:find(Topic, Services) of 25 | {ok, Service} -> 26 | Channels1 = action(Conn, {Type, Topic, Payload}, Service, Channels), 27 | {ok, {Services, Channels1}}; 28 | _Else -> 29 | {ok, {Services, Channels}} 30 | end. 31 | 32 | sockjs_terminate(_Conn, {Services, Channels}) -> 33 | _ = [ {emit(closed, Channel)} || 34 | {_Topic, Channel} <- orddict:to_list(Channels) ], 35 | {ok, {Services, orddict:new()}}. 36 | 37 | 38 | action(Conn, {Type, Topic, Payload}, Service, Channels) -> 39 | case {Type, orddict:is_key(Topic, Channels)} of 40 | {"sub", false} -> 41 | Channel = Service#service{ 42 | vconn = sockjs_multiplex_channel:new( 43 | Conn, Topic) 44 | }, 45 | orddict:store(Topic, emit(init, Channel), Channels); 46 | {"uns", true} -> 47 | Channel = orddict:fetch(Topic, Channels), 48 | emit(closed, Channel), 49 | orddict:erase(Topic, Channels); 50 | {"msg", true} -> 51 | Channel = orddict:fetch(Topic, Channels), 52 | orddict:store(Topic, emit({recv, Payload}, Channel), Channels); 53 | _Else -> 54 | %% Ignore 55 | Channels 56 | end. 57 | 58 | 59 | emit(What, Channel = #service{callback = Callback, 60 | state = State, 61 | vconn = VConn}) -> 62 | case Callback(VConn, What, State) of 63 | {ok, State1} -> Channel#service{state = State1}; 64 | ok -> Channel 65 | end. 66 | 67 | 68 | %% -------------------------------------------------------------------------- 69 | 70 | split(Char, Str, Limit) -> 71 | Acc = split(Char, Str, Limit, []), 72 | lists:reverse(Acc). 73 | split(_Char, _Str, 0, Acc) -> Acc; 74 | split(Char, Str, Limit, Acc) -> 75 | {L, R} = case string:chr(Str, Char) of 76 | 0 -> {Str, ""}; 77 | I -> {string:substr(Str, 1, I-1), string:substr(Str, I+1)} 78 | end, 79 | split(Char, R, Limit-1, [L | Acc]). 80 | -------------------------------------------------------------------------------- /src/sockjs_filters.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_filters). 2 | 3 | -include("sockjs_internal.hrl"). 4 | 5 | -export([cache_for/2, h_sid/2, h_no_cache/2, xhr_cors/2, 6 | xhr_options_post/2, xhr_options_get/2]). 7 | 8 | -define(YEAR, 365 * 24 * 60 * 60). 9 | 10 | %% -------------------------------------------------------------------------- 11 | 12 | -spec cache_for(req(), headers()) -> {headers(), req()}. 13 | cache_for(Req, Headers) -> 14 | Expires = calendar:gregorian_seconds_to_datetime( 15 | calendar:datetime_to_gregorian_seconds( 16 | calendar:now_to_datetime(now())) + ?YEAR), 17 | H = [{"Cache-Control", "public, max-age=" ++ integer_to_list(?YEAR)}, 18 | {"Expires", httpd_util:rfc1123_date(Expires)}], 19 | {H ++ Headers, Req}. 20 | 21 | -spec h_sid(req(), headers()) -> {headers(), req()}. 22 | h_sid(Req, Headers) -> 23 | %% Some load balancers do sticky sessions, but only if there is 24 | %% a JSESSIONID cookie. If this cookie isn't yet set, we shall 25 | %% set it to a dumb value. It doesn't really matter what, as 26 | %% session information is usually added by the load balancer. 27 | {C, Req2} = sockjs_http:jsessionid(Req), 28 | H = case C of 29 | undefined -> [{"Set-Cookie", "JSESSIONID=dummy; path=/"}]; 30 | Jsid -> [{"Set-Cookie", "JSESSIONID=" ++ Jsid ++ "; path=/"}] 31 | end, 32 | {H ++ Headers, Req2}. 33 | 34 | -spec h_no_cache(req(), headers()) -> {headers(), req()}. 35 | h_no_cache(Req, Headers) -> 36 | H = [{"Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"}], 37 | {H ++ Headers, Req}. 38 | 39 | -spec xhr_cors(req(), headers()) -> {headers(), req()}. 40 | xhr_cors(Req, Headers) -> 41 | {OriginH, Req1} = sockjs_http:header('origin', Req), 42 | Origin = case OriginH of 43 | "null" -> "*"; 44 | undefined -> "*"; 45 | O -> O 46 | end, 47 | {HeadersH, Req2} = sockjs_http:header( 48 | 'access-control-request-headers', Req1), 49 | AllowHeaders = case HeadersH of 50 | undefined -> []; 51 | V -> [{"Access-Control-Allow-Headers", V}] 52 | end, 53 | H = [{"Access-Control-Allow-Origin", Origin}, 54 | {"Access-Control-Allow-Credentials", "true"}], 55 | {H ++ AllowHeaders ++ Headers, Req2}. 56 | 57 | -spec xhr_options_post(req(), headers()) -> {headers(), req()}. 58 | xhr_options_post(Req, Headers) -> 59 | xhr_options(Req, Headers, ["OPTIONS", "POST"]). 60 | 61 | -spec xhr_options_get(req(), headers()) -> {headers(), req()}. 62 | xhr_options_get(Req, Headers) -> 63 | xhr_options(Req, Headers, ["OPTIONS", "GET"]). 64 | 65 | -spec xhr_options(req(), headers(), list(string())) -> {headers(), req()}. 66 | xhr_options(Req, Headers, Methods) -> 67 | H = [{"Access-Control-Allow-Methods", string:join(Methods, ", ")}, 68 | {"Access-Control-Max-Age", integer_to_list(?YEAR)}], 69 | {H ++ Headers, Req}. 70 | -------------------------------------------------------------------------------- /examples/multiplex/cowboy_multiplex.erl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -smp disable +A1 +K true -pa ebin deps/cowboy/ebin -input 3 | -module(cowboy_multiplex). 4 | -mode(compile). 5 | 6 | -export([main/1]). 7 | 8 | %% Cowboy callbacks 9 | -export([init/3, handle/2, terminate/2]). 10 | 11 | main(_) -> 12 | Port = 8081, 13 | application:start(sockjs), 14 | application:start(cowboy), 15 | 16 | MultiplexState = sockjs_multiplex:init_state( 17 | [{"ann", fun service_ann/3, []}, 18 | {"bob", fun service_bob/3, []}, 19 | {"carl", fun service_carl/3, []}]), 20 | 21 | SockjsState = sockjs_handler:init_state( 22 | <<"/multiplex">>, sockjs_multiplex, MultiplexState, []), 23 | 24 | VhostRoutes = [{[<<"multiplex">>, '...'], sockjs_cowboy_handler, SockjsState}, 25 | {'_', ?MODULE, []}], 26 | Routes = [{'_', VhostRoutes}], % any vhost 27 | 28 | io:format(" [*] Running at http://localhost:~p~n", [Port]), 29 | cowboy:start_listener(http, 100, 30 | cowboy_tcp_transport, [{port, Port}], 31 | cowboy_http_protocol, [{dispatch, Routes}]), 32 | receive 33 | _ -> ok 34 | end. 35 | 36 | %% -------------------------------------------------------------------------- 37 | 38 | init({_Any, http}, Req, []) -> 39 | {ok, Req, []}. 40 | 41 | handle(Req, State) -> 42 | {Path, Req1} = cowboy_http_req:path(Req), 43 | {ok, Req2} = case Path of 44 | [<<"multiplex.js">>] -> 45 | {ok, Data} = file:read_file("./examples/multiplex/multiplex.js"), 46 | cowboy_http_req:reply(200, [{<<"Content-Type">>, "application/javascript"}], 47 | Data, Req1); 48 | [] -> 49 | {ok, Data} = file:read_file("./examples/multiplex/index.html"), 50 | cowboy_http_req:reply(200, [{<<"Content-Type">>, "text/html"}], 51 | Data, Req1); 52 | _ -> 53 | cowboy_http_req:reply(404, [], 54 | <<"404 - Nothing here\n">>, Req1) 55 | end, 56 | {ok, Req2, State}. 57 | 58 | terminate(_Req, _State) -> 59 | ok. 60 | 61 | %% -------------------------------------------------------------------------- 62 | 63 | service_ann(Conn, init, State) -> 64 | Conn:send("Ann says hi!"), 65 | {ok, State}; 66 | service_ann(Conn, {recv, Data}, State) -> 67 | Conn:send(["Ann nods: ", Data]), 68 | {ok, State}; 69 | service_ann(_Conn, closed, State) -> 70 | {ok, State}. 71 | 72 | service_bob(Conn, init, State) -> 73 | Conn:send("Bob doesn't agree."), 74 | {ok, State}; 75 | service_bob(Conn, {recv, Data}, State) -> 76 | Conn:send(["Bob says no to: ", Data]), 77 | {ok, State}; 78 | service_bob(_Conn, closed, State) -> 79 | {ok, State}. 80 | 81 | service_carl(Conn, init, State) -> 82 | Conn:send("Carl says goodbye!"), 83 | Conn:close(), 84 | {ok, State}; 85 | service_carl(_Conn, _, State) -> 86 | {ok, State}. 87 | -------------------------------------------------------------------------------- /examples/multiplex/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 36 | 37 |

SockJS Multiplex example

38 | 39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 | 54 | 96 | 97 | -------------------------------------------------------------------------------- /examples/cowboy_test_server.erl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %%! -smp disable +A1 +K true -pa ebin -env ERL_LIBS deps -input 3 | -module(cowboy_test_server). 4 | -mode(compile). 5 | 6 | -export([main/1]). 7 | 8 | %% Cowboy callbacks 9 | -export([init/3, handle/2, terminate/2]). 10 | 11 | 12 | main(_) -> 13 | Port = 8081, 14 | ok = application:start(xmerl), 15 | ok = application:start(sockjs), 16 | ok = application:start(ranch), 17 | ok = application:start(crypto), 18 | ok = application:start(cowboy), 19 | 20 | StateEcho = sockjs_handler:init_state( 21 | <<"/echo">>, fun service_echo/3, state, 22 | [{response_limit, 4096}]), 23 | StateClose = sockjs_handler:init_state( 24 | <<"/close">>, fun service_close/3, state, []), 25 | StateAmplify = sockjs_handler:init_state( 26 | <<"/amplify">>, fun service_amplify/3, state, []), 27 | StateBroadcast = sockjs_handler:init_state( 28 | <<"/broadcast">>, fun service_broadcast/3, state, []), 29 | StateDWSEcho = sockjs_handler:init_state( 30 | <<"/disabled_websocket_echo">>, fun service_echo/3, state, 31 | [{websocket, false}]), 32 | StateCNEcho = sockjs_handler:init_state( 33 | <<"/cookie_needed_echo">>, fun service_echo/3, state, 34 | [{cookie_needed, true}]), 35 | 36 | VRoutes = [{<<"/echo/[...]">>, sockjs_cowboy_handler, StateEcho}, 37 | {<<"/close/[...]">>, sockjs_cowboy_handler, StateClose}, 38 | {<<"/amplify/[...]">>, sockjs_cowboy_handler, StateAmplify}, 39 | {<<"/broadcast/[...]">>, sockjs_cowboy_handler, StateBroadcast}, 40 | {<<"/disabled_websocket_echo/[...]">>, sockjs_cowboy_handler, 41 | StateDWSEcho}, 42 | {<<"/cookie_needed_echo/[...]">>, sockjs_cowboy_handler, 43 | StateCNEcho}, 44 | {'_', ?MODULE, []}], 45 | Routes = [{'_', VRoutes}], % any vhost 46 | Dispatch = cowboy_router:compile(Routes), 47 | 48 | io:format(" [*] Running at http://localhost:~p~n", [Port]), 49 | 50 | cowboy:start_http(cowboy_test_server_http_listener, 100, 51 | [{port, Port}], 52 | [{env, [{dispatch, Dispatch}]}]), 53 | receive 54 | _ -> ok 55 | end. 56 | 57 | %% -------------------------------------------------------------------------- 58 | 59 | init({_Any, http}, Req, []) -> 60 | {ok, Req, []}. 61 | 62 | handle(Req, State) -> 63 | {ok, Req2} = cowboy_req:reply(404, [], 64 | <<"404 - Nothing here (via sockjs-erlang fallback)\n">>, Req), 65 | {ok, Req2, State}. 66 | 67 | terminate(_Req, _State) -> 68 | ok. 69 | 70 | %% -------------------------------------------------------------------------- 71 | 72 | service_echo(_Conn, init, state) -> {ok, state}; 73 | service_echo(Conn, {recv, Data}, state) -> Conn:send(Data); 74 | service_echo(_Conn, closed, state) -> {ok, state}. 75 | 76 | service_close(Conn, _, _State) -> 77 | Conn:close(3000, "Go away!"). 78 | 79 | service_amplify(Conn, {recv, Data}, _State) -> 80 | N0 = list_to_integer(binary_to_list(Data)), 81 | N = if N0 > 0 andalso N0 < 19 -> N0; 82 | true -> 1 83 | end, 84 | Conn:send(list_to_binary( 85 | string:copies("x", round(math:pow(2, N))))); 86 | service_amplify(_Conn, _, _State) -> 87 | ok. 88 | 89 | service_broadcast(Conn, init, _State) -> 90 | case ets:info(broadcast_table, memory) of 91 | undefined -> 92 | ets:new(broadcast_table, [public, named_table]); 93 | _Any -> 94 | ok 95 | end, 96 | true = ets:insert(broadcast_table, {Conn}), 97 | ok; 98 | service_broadcast(Conn, closed, _State) -> 99 | true = ets:delete_object(broadcast_table, {Conn}), 100 | ok; 101 | service_broadcast(_Conn, {recv, Data}, _State) -> 102 | ets:foldl(fun({Conn1}, _Acc) -> Conn1:send(Data) end, 103 | [], broadcast_table), 104 | ok. 105 | -------------------------------------------------------------------------------- /src/sockjs_http.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_http). 2 | 3 | -export([path/1, method/1, body/1, body_qs/1, header/2, jsessionid/1, 4 | callback/1, peername/1, sockname/1]). 5 | -export([reply/4, chunk_start/3, chunk/2, chunk_end/1]). 6 | -export([hook_tcp_close/1, unhook_tcp_close/1, abruptly_kill/1]). 7 | -include("sockjs_internal.hrl"). 8 | 9 | %% -------------------------------------------------------------------------- 10 | 11 | -spec path(req()) -> {string(), req()}. 12 | path({cowboy, Req}) -> {Path, Req1} = cowboy_req:path(Req), 13 | {binary_to_list(Path), {cowboy, Req1}}. 14 | 15 | -spec method(req()) -> {atom(), req()}. 16 | method({cowboy, Req}) -> {Method, Req1} = cowboy_req:method(Req), 17 | {method_atom(Method), {cowboy, Req1}}. 18 | 19 | -spec method_atom(binary() | atom()) -> atom(). 20 | method_atom(<<"GET">>) -> 'GET'; 21 | method_atom(<<"PUT">>) -> 'PUT'; 22 | method_atom(<<"POST">>) -> 'POST'; 23 | method_atom(<<"DELETE">>) -> 'DELETE'; 24 | method_atom(<<"OPTIONS">>) -> 'OPTIONS'; 25 | method_atom(<<"PATCH">>) -> 'PATCH'; 26 | method_atom(<<"HEAD">>) -> 'HEAD'; 27 | method_atom('GET') -> 'GET'; 28 | method_atom('PUT') -> 'PUT'; 29 | method_atom('POST') -> 'POST'; 30 | method_atom('DELETE') -> 'DELETE'; 31 | method_atom('OPTIONS') -> 'OPTIONS'; 32 | method_atom('PATCH') -> 'PATCH'; 33 | method_atom('HEAD') -> 'HEAD'. 34 | 35 | -spec body(req()) -> {binary(), req()}. 36 | body({cowboy, Req}) -> {ok, Body, Req1} = cowboy_req:body(Req), 37 | {Body, {cowboy, Req1}}. 38 | 39 | -spec body_qs(req()) -> {binary(), req()}. 40 | body_qs(Req) -> 41 | {H, Req1} = header('content-type', Req), 42 | case H of 43 | H when H =:= "text/plain" orelse H =:= "" -> 44 | body(Req1); 45 | _ -> 46 | %% By default assume application/x-www-form-urlencoded 47 | body_qs2(Req1) 48 | end. 49 | body_qs2({cowboy, Req}) -> 50 | {ok, BodyQS, Req1} = cowboy_req:body_qs(Req), 51 | case proplists:get_value(<<"d">>, BodyQS) of 52 | undefined -> 53 | {<<>>, {cowboy, Req1}}; 54 | V -> 55 | {V, {cowboy, Req1}} 56 | end. 57 | 58 | -spec header(atom(), req()) -> {nonempty_string() | undefined, req()}. 59 | header(K, {cowboy, Req})-> 60 | {H, Req2} = cowboy_req:header(K, Req), 61 | {V, Req3} = case H of 62 | undefined -> 63 | cowboy_req:header(atom_to_binary(K, utf8), Req2); 64 | _ -> {H, Req2} 65 | end, 66 | case V of 67 | undefined -> {undefined, {cowboy, Req3}}; 68 | _ -> {binary_to_list(V), {cowboy, Req3}} 69 | end. 70 | 71 | -spec jsessionid(req()) -> {nonempty_string() | undefined, req()}. 72 | jsessionid({cowboy, Req}) -> 73 | {C, Req2} = cowboy_req:cookie(<<"jsessionid">>, Req), 74 | case C of 75 | _ when is_binary(C) -> 76 | {binary_to_list(C), {cowboy, Req2}}; 77 | undefined -> 78 | {undefined, {cowboy, Req2}} 79 | end. 80 | 81 | -spec callback(req()) -> {nonempty_string() | undefined, req()}. 82 | callback({cowboy, Req}) -> 83 | {CB, Req1} = cowboy_req:qs_val(<<"c">>, Req), 84 | case CB of 85 | undefined -> {undefined, {cowboy, Req1}}; 86 | _ -> {binary_to_list(CB), {cowboy, Req1}} 87 | end. 88 | 89 | -spec peername(req()) -> {{inet:ip_address(), non_neg_integer()}, req()}. 90 | peername({cowboy, Req}) -> 91 | {P, Req1} = cowboy_req:peer(Req), 92 | {P, {cowboy, Req1}}. 93 | 94 | -spec sockname(req()) -> {{inet:ip_address(), non_neg_integer()}, req()}. 95 | sockname({cowboy, Req} = R) -> 96 | {Addr, _Req} = cowboy_req:peer(Req), 97 | {Addr, R}. 98 | 99 | %% -------------------------------------------------------------------------- 100 | 101 | -spec reply(non_neg_integer(), headers(), iodata(), req()) -> req(). 102 | reply(Code, Headers, Body, {cowboy, Req}) -> 103 | Body1 = iolist_to_binary(Body), 104 | {ok, Req1} = cowboy_req:reply(Code, enbinary(Headers), Body1, Req), 105 | {cowboy, Req1}. 106 | 107 | -spec chunk_start(non_neg_integer(), headers(), req()) -> req(). 108 | chunk_start(Code, Headers, {cowboy, Req}) -> 109 | {ok, Req1} = cowboy_req:chunked_reply(Code, enbinary(Headers), Req), 110 | {cowboy, Req1}. 111 | 112 | -spec chunk(iodata(), req()) -> {ok | error, req()}. 113 | chunk(Chunk, {cowboy, Req} = R) -> 114 | case cowboy_req:chunk(Chunk, Req) of 115 | ok -> {ok, R}; 116 | {error, _E} -> {error, R} 117 | %% This shouldn't happen too often, usually we 118 | %% should catch tco socket closure before. 119 | end. 120 | 121 | -spec chunk_end(req()) -> req(). 122 | chunk_end({cowboy, _Req} = R) -> R. 123 | 124 | enbinary(L) -> [{list_to_binary(K), list_to_binary(V)} || {K, V} <- L]. 125 | 126 | 127 | -spec hook_tcp_close(req()) -> req(). 128 | hook_tcp_close(R = {cowboy, Req}) -> 129 | [T, S] = cowboy_req:get([transport, socket], Req), 130 | T:setopts(S,[{active,once}]), 131 | R. 132 | 133 | -spec unhook_tcp_close(req()) -> req(). 134 | unhook_tcp_close(R = {cowboy, Req}) -> 135 | [T, S] = cowboy_req:get([transport, socket], Req), 136 | T:setopts(S,[{active,false}]), 137 | R. 138 | 139 | -spec abruptly_kill(req()) -> req(). 140 | abruptly_kill(R = {cowboy, Req}) -> 141 | [T, S] = cowboy_req:get([transport, socket], Req), 142 | ok = T:close(S), 143 | R. 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SockJS family: 2 | 3 | * [SockJS-client](https://github.com/sockjs/sockjs-client) JavaScript client library 4 | * [SockJS-node](https://github.com/sockjs/sockjs-node) Node.js server 5 | * [SockJS-erlang](https://github.com/sockjs/sockjs-erlang) Erlang server 6 | 7 | 8 | SockJS-erlang server 9 | ==================== 10 | 11 | [SockJS](http://sockjs.org) server written in Erlang. Can run with 12 | [Cowboy](https://github.com/extend/cowboy) http server. SockJS-erlang 13 | is in core web-framework agnostic (up to version 14 | [v0.2.1](https://github.com/sockjs/sockjs-erlang/tree/v0.2.1 ) we also 15 | supported 16 | [Misultin](https://github.com/ostinelli/misultin)). SockJS-erlang is 17 | compatible with 18 | [SockJS client version 0.3](http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.3.html). See 19 | https://github.com/sockjs/sockjs-client for more information on 20 | SockJS. 21 | 22 | 23 | Show me the code! 24 | ----------------- 25 | 26 | A simplistic echo SockJS server using Cowboy may look more or less 27 | like this: 28 | 29 | ```erlang 30 | main(_) -> 31 | ok = application:start(xmerl), 32 | ok = application:start(sockjs), 33 | ok = application:start(ranch), 34 | ok = application:start(crypto), 35 | ok = application:start(cowboy), 36 | 37 | SockjsState = sockjs_handler:init_state( 38 | <<"/echo">>, fun service_echo/3, state, []), 39 | 40 | Routes = [{'_', [{<<"/echo/[...]">>, 41 | sockjs_cowboy_handler, SockjsState}]}], 42 | Dispatch = cowboy_router:compile(Routes), 43 | 44 | cowboy:start_http(cowboy_test_http_listener, 100, 45 | [{port, 8081}], 46 | [{env, [{dispatch, Dispatch}]}]), 47 | receive 48 | _ -> ok 49 | end. 50 | 51 | service_echo(_Conn, init, state) -> {ok, state}; 52 | service_echo(Conn, {recv, Data}, state) -> Conn:send(Data); 53 | service_echo(_Conn, {info, _Info}, state) -> {ok, state}; 54 | service_echo(_Conn, closed, state) -> {ok, state}. 55 | ``` 56 | 57 | Dig into the `examples` directory to get working code: 58 | 59 | * https://github.com/sockjs/sockjs-erlang/examples/cowboy_echo.erl 60 | 61 | 62 | How to run the examples? 63 | ------------------------ 64 | 65 | You may need a recent version of Erlang/OTP, at least R14B is recommended. 66 | 67 | To run Cowboy example: 68 | 69 | cd sockjs-erlang 70 | ./rebar get-deps 71 | ./rebar compile 72 | ./examples/cowboy_echo.erl 73 | 74 | This will start a simple `/echo` SockJS server on 75 | `http://localhost:8081`. Open this link in a browser and play 76 | around. 77 | 78 | 79 | SockJS-erlang API 80 | ----------------- 81 | 82 | Except for the web framework-specific API's, SockJS-erlang is rather 83 | simple. It has just a couple of methods: 84 | 85 | * **sockjs_handler:init_state(prefix, callback, state, options) -> service()** 86 | 87 | Initializes the state of a SockJS service (ie: a thing you can 88 | access from the browser, it has an url and a code on the server 89 | side). `prefix` is a binary that must exacty match the url prefix 90 | of the service, for example, if service will be listening on 91 | '/echo', this parameter must be set to `<<"/echo">>`. `callback` 92 | function will be called when a new SockJS connection is 93 | established, data received or a connection is closed. The value of 94 | `state` will be passed to the callback and preserved if returned 95 | value has changed. Options is a proplist that can contain 96 | following tuples: 97 | 98 | * `{sockjs_url, string()}` - Transports which don't support 99 | cross-domain communication natively ('eventsource' to name one) 100 | use an iframe trick. A simple page is served from the SockJS 101 | server (using its foreign domain) and is placed in an invisible 102 | iframe. Code run from this iframe doesn't need to worry about 103 | cross-domain issues, as it's being run from domain local to the 104 | SockJS server. This iframe also does need to load SockJS 105 | javascript client library, and this option lets you specify its 106 | url (if you're unsure, point it to the latest 108 | minified SockJS client release, this is the default). 109 | * `{websocket, boolean()}` - are native websockets enabled? This 110 | can be usefull when your loadbalancer doesn't support them. 111 | * `{cookie_needed, boolean()}` - is your load balancer relying on 112 | cookies to get sticky sessions working? 113 | * `{heartbeat_delay, integer()}` - how often to send heartbeat 114 | packets (in ms). 115 | * `{disconnect_delay, integer()}` - how long to hold session state 116 | after the client was last connected (in ms). 117 | * `{response_limit, integer()}` - the maximum size of a single 118 | http streaming response (in bytes). 119 | * `{logger, fun/3}` - a function called on every request, used 120 | to print request to the logs (or on the screen by default). 121 | 122 | For more explanation, please do take a look at 123 | [SockJS-node readme](https://github.com/sockjs/sockjs-node/blob/master/README.md). 124 | 125 | * **Connection:send(payload) -> ok** 126 | 127 | Send data over an active SockJS connection. Payload should be of 128 | iodata() type. Messages sent after connection gets closed will be 129 | lost. 130 | 131 | * **Connection:close(code, reason) -> ok** 132 | 133 | Close an active SockJS connection with code and reason. If code 134 | and reason are skipped, the defaults are used. 135 | 136 | * **Connection:info() -> proplist()** 137 | 138 | Sometimes you may want to know more about the underlying 139 | connection. This method returns a proplist with few attributes 140 | extracted from the first HTTP/websocket request that was coming 141 | to this connection. You should see: 142 | 143 | * peername - ip address and port of the remote host 144 | * sockname - ip address and port of the local endpoint 145 | * path - the path used by the request that started the connection 146 | * headers - a set of headers extracted from the request that 147 | may be handy (don't expect to retrieve Cookie header). 148 | 149 | 150 | The framework-specific calls are more problematic. Instead of trying 151 | to explain how to use them, please take a look at the examples. 152 | 153 | * **type(req() :: {cowboy, request()})** 154 | * **sockjs_handler:handle_req(service(), req()) -> req()** 155 | * **sockjs_handler:handle_ws(service(), req()) -> req()** 156 | 157 | 158 | Stability 159 | --------- 160 | 161 | SockJS-erlang is quite new, but should be reasonably stable. Cowboy is passes all the 162 | [SockJS-protocol tests](https://github.com/sockjs/sockjs-protocol). 163 | 164 | Deployment and load balancing 165 | ----------------------------- 166 | 167 | SockJS servers should work well behind many load balancer setups, but 168 | it sometimes requres some additional twaks. For more details, please 169 | do take a look at the 'Deployment' section in 170 | [SockJS-node readme](https://github.com/sockjs/sockjs-node/blob/master/README.md). 171 | 172 | 173 | Development and testing 174 | ----------------------- 175 | 176 | You need [rebar](https://github.com/basho/rebar) 177 | ([instructions](https://github.com/basho/rebar/wiki/Building-rebar)). 178 | Due to a bug in rebar config handling you need a reasonably recent 179 | version - newer than late Oct 2011. Alternatively, SockJS-erlang is 180 | bundeled with a recent rebar binary. 181 | 182 | SockJS-erlang contains a `test_server`, a simple server used for 183 | testing. 184 | 185 | To run Cowboy test_server: 186 | 187 | cd sockjs-erlang 188 | ./rebar get-deps 189 | ./rebar compile 190 | ./examples/cowboy_test_server.erl 191 | 192 | That should start test_server on port 8081. Currently, there are two 193 | separate test suits using test_server. 194 | 195 | ### SockJS-protocol Python tests 196 | 197 | Once test_server is listening on `http://localhost:8081` you may test it 198 | using SockJS-protocol: 199 | 200 | cd sockjs-protocol 201 | make test_deps 202 | ./venv/bin/python sockjs-protocol-dev.py 203 | 204 | For details see 205 | [SockJS-protocol README](https://github.com/sockjs/sockjs-protocol#readme). 206 | 207 | ### SockJS-client QUnit tests 208 | 209 | You need to start a second web server (by default listening on 8080) 210 | that is serving various static html and javascript files: 211 | 212 | cd sockjs-client 213 | make test 214 | 215 | At that point you should have two web servers running: sockjs-erlang on 216 | 8081 and sockjs-client on 8080. When you open the browser on 217 | [http://localhost:8080/](http://localhost:8080/) you should be able 218 | run the QUnit tests against your sockjs-node server. 219 | 220 | For details see 221 | [SockJS-client README](https://github.com/sockjs/sockjs-client#readme). 222 | 223 | Additionally, if you're doing more serious development consider using 224 | `make serve`, which will automatically the server when you modify the 225 | source code. 226 | -------------------------------------------------------------------------------- /src/sockjs_handler.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_handler). 2 | 3 | -export([init_state/4]). 4 | -export([is_valid_ws/2, get_action/2]). 5 | -export([dispatch_req/2, handle_req/2]). 6 | -export([extract_info/1]). 7 | 8 | -include("sockjs_internal.hrl"). 9 | 10 | -define(SOCKJS_URL, "https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.min.js"). 11 | 12 | %% -------------------------------------------------------------------------- 13 | 14 | -spec init_state(binary(), callback(), any(), list(tuple())) -> service(). 15 | init_state(Prefix, Callback, State, Options) -> 16 | #service{prefix = binary_to_list(Prefix), 17 | callback = Callback, 18 | state = State, 19 | sockjs_url = 20 | proplists:get_value(sockjs_url, Options, ?SOCKJS_URL), 21 | websocket = 22 | proplists:get_value(websocket, Options, true), 23 | cookie_needed = 24 | proplists:get_value(cookie_needed, Options, false), 25 | disconnect_delay = 26 | proplists:get_value(disconnect_delay, Options, 5000), 27 | heartbeat_delay = 28 | proplists:get_value(heartbeat_delay, Options, 25000), 29 | response_limit = 30 | proplists:get_value(response_limit, Options, 128*1024), 31 | logger = 32 | proplists:get_value(logger, Options, fun default_logger/3) 33 | }. 34 | 35 | %% -------------------------------------------------------------------------- 36 | 37 | -spec is_valid_ws(service(), req()) -> {boolean(), req(), tuple()}. 38 | is_valid_ws(Service, Req) -> 39 | case get_action(Service, Req) of 40 | {{match, WS}, Req1} when WS =:= websocket orelse 41 | WS =:= rawwebsocket -> 42 | valid_ws_request(Service, Req1); 43 | {_Else, Req1} -> 44 | {false, Req1, {}} 45 | end. 46 | 47 | -spec valid_ws_request(service(), req()) -> {boolean(), req(), tuple()}. 48 | valid_ws_request(_Service, Req) -> 49 | {R1, Req1} = valid_ws_upgrade(Req), 50 | {R2, Req2} = valid_ws_connection(Req1), 51 | {R1 and R2, Req2, {R1, R2}}. 52 | 53 | valid_ws_upgrade(Req) -> 54 | case sockjs_http:header('upgrade', Req) of 55 | {undefined, Req2} -> 56 | {false, Req2}; 57 | {V, Req2} -> 58 | case string:to_lower(V) of 59 | "websocket" -> 60 | {true, Req2}; 61 | _Else -> 62 | {false, Req2} 63 | end 64 | end. 65 | 66 | valid_ws_connection(Req) -> 67 | case sockjs_http:header('connection', Req) of 68 | {undefined, Req2} -> 69 | {false, Req2}; 70 | {V, Req2} -> 71 | Vs = [string:strip(T) || 72 | T <- string:tokens(string:to_lower(V), ",")], 73 | {lists:member("upgrade", Vs), Req2} 74 | end. 75 | 76 | -spec get_action(service(), req()) -> {nomatch | {match, atom()}, req()}. 77 | get_action(Service, Req) -> 78 | {Dispatch, Req1} = dispatch_req(Service, Req), 79 | case Dispatch of 80 | {match, {_, Action, _, _, _}} -> 81 | {{match, Action}, Req1}; 82 | _Else -> 83 | {nomatch, Req1} 84 | end. 85 | 86 | %% -------------------------------------------------------------------------- 87 | 88 | strip_prefix(LongPath, Prefix) -> 89 | {A, B} = lists:split(length(Prefix), LongPath), 90 | case Prefix of 91 | A -> {ok, B}; 92 | _Any -> {error, io_lib:format("Wrong prefix: ~p is not ~p", [A, Prefix])} 93 | end. 94 | 95 | 96 | -type(dispatch_result() :: 97 | nomatch | 98 | {match, {send | recv | none , atom(), 99 | server(), session(), list(atom())}} | 100 | {bad_method, list(atom())}). 101 | 102 | -spec dispatch_req(service(), req()) -> {dispatch_result(), req()}. 103 | dispatch_req(#service{prefix = Prefix}, Req) -> 104 | {Method, Req1} = sockjs_http:method(Req), 105 | {LongPath, Req2} = sockjs_http:path(Req1), 106 | {ok, PathRemainder} = strip_prefix(LongPath, Prefix), 107 | {dispatch(Method, PathRemainder), Req2}. 108 | 109 | -spec dispatch(atom(), nonempty_string()) -> dispatch_result(). 110 | dispatch(Method, Path) -> 111 | lists:foldl( 112 | fun ({Match, MethodFilters}, nomatch) -> 113 | case Match(Path) of 114 | nomatch -> 115 | nomatch; 116 | [Server, Session] -> 117 | case lists:keyfind(Method, 1, MethodFilters) of 118 | false -> 119 | Methods = [ K || 120 | {K, _, _, _} <- MethodFilters], 121 | {bad_method, Methods}; 122 | {_Method, Type, A, Filters} -> 123 | {match, {Type, A, Server, Session, Filters}} 124 | end 125 | end; 126 | (_, Result) -> 127 | Result 128 | end, nomatch, filters()). 129 | 130 | %% -------------------------------------------------------------------------- 131 | 132 | filters() -> 133 | OptsFilters = [h_sid, xhr_cors, cache_for, xhr_options_post], 134 | %% websocket does not actually go via handle_req/3 but we need 135 | %% something in dispatch/2 136 | [{t("/websocket"), [{'GET', none, websocket, []}]}, 137 | {t("/xhr_send"), [{'POST', recv, xhr_send, [h_sid, h_no_cache, xhr_cors]}, 138 | {'OPTIONS', none, options, OptsFilters}]}, 139 | {t("/xhr"), [{'POST', send, xhr_polling, [h_sid, h_no_cache, xhr_cors]}, 140 | {'OPTIONS', none, options, OptsFilters}]}, 141 | {t("/xhr_streaming"), [{'POST', send, xhr_streaming, [h_sid, h_no_cache, xhr_cors]}, 142 | {'OPTIONS', none, options, OptsFilters}]}, 143 | {t("/jsonp_send"), [{'POST', recv, jsonp_send, [h_sid, h_no_cache]}]}, 144 | {t("/jsonp"), [{'GET', send, jsonp, [h_sid, h_no_cache]}]}, 145 | {t("/eventsource"), [{'GET', send, eventsource, [h_sid, h_no_cache]}]}, 146 | {t("/htmlfile"), [{'GET', send, htmlfile, [h_sid, h_no_cache]}]}, 147 | {p("/websocket"), [{'GET', none, rawwebsocket, []}]}, 148 | {p(""), [{'GET', none, welcome_screen, []}]}, 149 | {p("/iframe[0-9-.a-z_]*.html"), [{'GET', none, iframe, [cache_for]}]}, 150 | {p("/info"), [{'GET', none, info_test, [h_no_cache, xhr_cors]}, 151 | {'OPTIONS', none, options, [h_sid, xhr_cors, cache_for, xhr_options_get]}]} 152 | ]. 153 | 154 | p(S) -> fun (Path) -> re(Path, "^" ++ S ++ "[/]?\$") end. 155 | t(S) -> fun (Path) -> re(Path, "^/([^/.]+)/([^/.]+)" ++ S ++ "[/]?\$") end. 156 | 157 | re(Path, S) -> 158 | case re:run(Path, S, [{capture, all_but_first, list}]) of 159 | nomatch -> nomatch; 160 | {match, []} -> [dummy, dummy]; 161 | {match, [Server, Session]} -> [Server, Session] 162 | end. 163 | 164 | %% -------------------------------------------------------------------------- 165 | 166 | -spec handle_req(service(), req()) -> req(). 167 | handle_req(Service = #service{logger = Logger}, Req) -> 168 | Req0 = Logger(Service, Req, http), 169 | 170 | {Dispatch, Req1} = dispatch_req(Service, Req0), 171 | handle(Dispatch, Service, Req1). 172 | 173 | handle(nomatch, _Service, Req) -> 174 | sockjs_http:reply(404, [], "", Req); 175 | 176 | handle({bad_method, Methods}, _Service, Req) -> 177 | MethodsStr = string:join([atom_to_list(M) || M <- Methods], 178 | ", "), 179 | H = [{"Allow", MethodsStr}], 180 | sockjs_http:reply(405, H, "", Req); 181 | 182 | handle({match, {Type, Action, _Server, Session, Filters}}, Service, Req) -> 183 | {Headers, Req2} = lists:foldl( 184 | fun (Filter, {Headers0, Req1}) -> 185 | sockjs_filters:Filter(Req1, Headers0) 186 | end, {[], Req}, Filters), 187 | case Type of 188 | send -> 189 | {Info, Req3} = extract_info(Req2), 190 | _SPid = sockjs_session:maybe_create(Session, Service, Info), 191 | sockjs_action:Action(Req3, Headers, Service, Session); 192 | recv -> 193 | try 194 | sockjs_action:Action(Req2, Headers, Service, Session) 195 | catch throw:no_session -> 196 | {H, Req3} = sockjs_filters:h_sid(Req2, []), 197 | sockjs_http:reply(404, H, "", Req3) 198 | end; 199 | none -> 200 | sockjs_action:Action(Req2, Headers, Service) 201 | end. 202 | 203 | %% -------------------------------------------------------------------------- 204 | 205 | -spec default_logger(service(), req(), websocket | http) -> req(). 206 | default_logger(_Service, Req, _Type) -> 207 | {LongPath, Req1} = sockjs_http:path(Req), 208 | {Method, Req2} = sockjs_http:method(Req1), 209 | io:format("~s ~s~n", [Method, LongPath]), 210 | Req2. 211 | 212 | -spec extract_info(req()) -> {info(), req()}. 213 | extract_info(Req) -> 214 | {Peer, Req0} = sockjs_http:peername(Req), 215 | {Sock, Req1} = sockjs_http:sockname(Req0), 216 | {Path, Req2} = sockjs_http:path(Req1), 217 | {Headers, Req3} = lists:foldl(fun (H, {Acc, R0}) -> 218 | case sockjs_http:header(H, R0) of 219 | {undefined, R1} -> {Acc, R1}; 220 | {V, R1} -> {[{H, V} | Acc], R1} 221 | end 222 | end, {[], Req2}, 223 | ['referer', 'x-client-ip', 'x-forwarded-for', 224 | 'x-cluster-client-ip', 'via', 'x-real-ip']), 225 | {[{peername, Peer}, 226 | {sockname, Sock}, 227 | {path, Path}, 228 | {headers, Headers}], Req3}. 229 | -------------------------------------------------------------------------------- /LICENSE-APL2-Rebar: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | -------------------------------------------------------------------------------- /src/mochinum_fork.erl: -------------------------------------------------------------------------------- 1 | %% @copyright 2007 Mochi Media, Inc. 2 | %% @author Bob Ippolito 3 | 4 | %% @doc Useful numeric algorithms for floats that cover some deficiencies 5 | %% in the math module. More interesting is digits/1, which implements 6 | %% the algorithm from: 7 | %% http://www.cs.indiana.edu/~burger/fp/index.html 8 | %% See also "Printing Floating-Point Numbers Quickly and Accurately" 9 | %% in Proceedings of the SIGPLAN '96 Conference on Programming Language 10 | %% Design and Implementation. 11 | 12 | -module(mochinum_fork). 13 | -author("Bob Ippolito "). 14 | -export([digits/1, frexp/1, int_pow/2, int_ceil/1]). 15 | 16 | %% IEEE 754 Float exponent bias 17 | -define(FLOAT_BIAS, 1022). 18 | -define(MIN_EXP, -1074). 19 | -define(BIG_POW, 4503599627370496). 20 | 21 | %% External API 22 | 23 | %% @spec digits(number()) -> string() 24 | %% @doc Returns a string that accurately represents the given integer or float 25 | %% using a conservative amount of digits. Great for generating 26 | %% human-readable output, or compact ASCII serializations for floats. 27 | digits(N) when is_integer(N) -> 28 | integer_to_list(N); 29 | digits(0.0) -> 30 | "0.0"; 31 | digits(Float) -> 32 | {Frac1, Exp1} = frexp_int(Float), 33 | [Place0 | Digits0] = digits1(Float, Exp1, Frac1), 34 | {Place, Digits} = transform_digits(Place0, Digits0), 35 | R = insert_decimal(Place, Digits), 36 | case Float < 0 of 37 | true -> 38 | [$- | R]; 39 | _ -> 40 | R 41 | end. 42 | 43 | %% @spec frexp(F::float()) -> {Frac::float(), Exp::float()} 44 | %% @doc Return the fractional and exponent part of an IEEE 754 double, 45 | %% equivalent to the libc function of the same name. 46 | %% F = Frac * pow(2, Exp). 47 | frexp(F) -> 48 | frexp1(unpack(F)). 49 | 50 | %% @spec int_pow(X::integer(), N::integer()) -> Y::integer() 51 | %% @doc Moderately efficient way to exponentiate integers. 52 | %% int_pow(10, 2) = 100. 53 | int_pow(_X, 0) -> 54 | 1; 55 | int_pow(X, N) when N > 0 -> 56 | int_pow(X, N, 1). 57 | 58 | %% @spec int_ceil(F::float()) -> integer() 59 | %% @doc Return the ceiling of F as an integer. The ceiling is defined as 60 | %% F when F == trunc(F); 61 | %% trunc(F) when F < 0; 62 | %% trunc(F) + 1 when F > 0. 63 | int_ceil(X) -> 64 | T = trunc(X), 65 | case (X - T) of 66 | Pos when Pos > 0 -> T + 1; 67 | _ -> T 68 | end. 69 | 70 | 71 | %% Internal API 72 | 73 | int_pow(X, N, R) when N < 2 -> 74 | R * X; 75 | int_pow(X, N, R) -> 76 | int_pow(X * X, N bsr 1, case N band 1 of 1 -> R * X; 0 -> R end). 77 | 78 | insert_decimal(0, S) -> 79 | "0." ++ S; 80 | insert_decimal(Place, S) when Place > 0 -> 81 | L = length(S), 82 | case Place - L of 83 | 0 -> 84 | S ++ ".0"; 85 | N when N < 0 -> 86 | {S0, S1} = lists:split(L + N, S), 87 | S0 ++ "." ++ S1; 88 | N when N < 6 -> 89 | %% More places than digits 90 | S ++ lists:duplicate(N, $0) ++ ".0"; 91 | _ -> 92 | insert_decimal_exp(Place, S) 93 | end; 94 | insert_decimal(Place, S) when Place > -6 -> 95 | "0." ++ lists:duplicate(abs(Place), $0) ++ S; 96 | insert_decimal(Place, S) -> 97 | insert_decimal_exp(Place, S). 98 | 99 | insert_decimal_exp(Place, S) -> 100 | [C | S0] = S, 101 | S1 = case S0 of 102 | [] -> 103 | "0"; 104 | _ -> 105 | S0 106 | end, 107 | Exp = case Place < 0 of 108 | true -> 109 | "e-"; 110 | false -> 111 | "e+" 112 | end, 113 | [C] ++ "." ++ S1 ++ Exp ++ integer_to_list(abs(Place - 1)). 114 | 115 | 116 | digits1(Float, Exp, Frac) -> 117 | Round = ((Frac band 1) =:= 0), 118 | case Exp >= 0 of 119 | true -> 120 | BExp = 1 bsl Exp, 121 | case (Frac =/= ?BIG_POW) of 122 | true -> 123 | scale((Frac * BExp * 2), 2, BExp, BExp, 124 | Round, Round, Float); 125 | false -> 126 | scale((Frac * BExp * 4), 4, (BExp * 2), BExp, 127 | Round, Round, Float) 128 | end; 129 | false -> 130 | case (Exp =:= ?MIN_EXP) orelse (Frac =/= ?BIG_POW) of 131 | true -> 132 | scale((Frac * 2), 1 bsl (1 - Exp), 1, 1, 133 | Round, Round, Float); 134 | false -> 135 | scale((Frac * 4), 1 bsl (2 - Exp), 2, 1, 136 | Round, Round, Float) 137 | end 138 | end. 139 | 140 | scale(R, S, MPlus, MMinus, LowOk, HighOk, Float) -> 141 | Est = int_ceil(math:log10(abs(Float)) - 1.0e-10), 142 | %% Note that the scheme implementation uses a 326 element look-up table 143 | %% for int_pow(10, N) where we do not. 144 | case Est >= 0 of 145 | true -> 146 | fixup(R, S * int_pow(10, Est), MPlus, MMinus, Est, 147 | LowOk, HighOk); 148 | false -> 149 | Scale = int_pow(10, -Est), 150 | fixup(R * Scale, S, MPlus * Scale, MMinus * Scale, Est, 151 | LowOk, HighOk) 152 | end. 153 | 154 | fixup(R, S, MPlus, MMinus, K, LowOk, HighOk) -> 155 | TooLow = case HighOk of 156 | true -> 157 | (R + MPlus) >= S; 158 | false -> 159 | (R + MPlus) > S 160 | end, 161 | case TooLow of 162 | true -> 163 | [(K + 1) | generate(R, S, MPlus, MMinus, LowOk, HighOk)]; 164 | false -> 165 | [K | generate(R * 10, S, MPlus * 10, MMinus * 10, LowOk, HighOk)] 166 | end. 167 | 168 | generate(R0, S, MPlus, MMinus, LowOk, HighOk) -> 169 | D = R0 div S, 170 | R = R0 rem S, 171 | TC1 = case LowOk of 172 | true -> 173 | R =< MMinus; 174 | false -> 175 | R < MMinus 176 | end, 177 | TC2 = case HighOk of 178 | true -> 179 | (R + MPlus) >= S; 180 | false -> 181 | (R + MPlus) > S 182 | end, 183 | case TC1 of 184 | false -> 185 | case TC2 of 186 | false -> 187 | [D | generate(R * 10, S, MPlus * 10, MMinus * 10, 188 | LowOk, HighOk)]; 189 | true -> 190 | [D + 1] 191 | end; 192 | true -> 193 | case TC2 of 194 | false -> 195 | [D]; 196 | true -> 197 | case R * 2 < S of 198 | true -> 199 | [D]; 200 | false -> 201 | [D + 1] 202 | end 203 | end 204 | end. 205 | 206 | unpack(Float) -> 207 | <> = <>, 208 | {Sign, Exp, Frac}. 209 | 210 | frexp1({_Sign, 0, 0}) -> 211 | {0.0, 0}; 212 | frexp1({Sign, 0, Frac}) -> 213 | Exp = log2floor(Frac), 214 | <> = <>, 215 | {Frac1, -(?FLOAT_BIAS) - 52 + Exp}; 216 | frexp1({Sign, Exp, Frac}) -> 217 | <> = <>, 218 | {Frac1, Exp - ?FLOAT_BIAS}. 219 | 220 | log2floor(Int) -> 221 | log2floor(Int, 0). 222 | 223 | log2floor(0, N) -> 224 | N; 225 | log2floor(Int, N) -> 226 | log2floor(Int bsr 1, 1 + N). 227 | 228 | 229 | transform_digits(Place, [0 | Rest]) -> 230 | transform_digits(Place, Rest); 231 | transform_digits(Place, Digits) -> 232 | {Place, [$0 + D || D <- Digits]}. 233 | 234 | 235 | frexp_int(F) -> 236 | case unpack(F) of 237 | {_Sign, 0, Frac} -> 238 | {Frac, ?MIN_EXP}; 239 | {_Sign, Exp, Frac} -> 240 | {Frac + (1 bsl 52), Exp - 53 - ?FLOAT_BIAS} 241 | end. 242 | 243 | %% 244 | %% Tests 245 | %% 246 | -ifdef(TEST). 247 | -include_lib("eunit/include/eunit.hrl"). 248 | 249 | int_ceil_test() -> 250 | ?assertEqual(1, int_ceil(0.0001)), 251 | ?assertEqual(0, int_ceil(0.0)), 252 | ?assertEqual(1, int_ceil(0.99)), 253 | ?assertEqual(1, int_ceil(1.0)), 254 | ?assertEqual(-1, int_ceil(-1.5)), 255 | ?assertEqual(-2, int_ceil(-2.0)), 256 | ok. 257 | 258 | int_pow_test() -> 259 | ?assertEqual(1, int_pow(1, 1)), 260 | ?assertEqual(1, int_pow(1, 0)), 261 | ?assertEqual(1, int_pow(10, 0)), 262 | ?assertEqual(10, int_pow(10, 1)), 263 | ?assertEqual(100, int_pow(10, 2)), 264 | ?assertEqual(1000, int_pow(10, 3)), 265 | ok. 266 | 267 | digits_test() -> 268 | ?assertEqual("0", 269 | digits(0)), 270 | ?assertEqual("0.0", 271 | digits(0.0)), 272 | ?assertEqual("1.0", 273 | digits(1.0)), 274 | ?assertEqual("-1.0", 275 | digits(-1.0)), 276 | ?assertEqual("0.1", 277 | digits(0.1)), 278 | ?assertEqual("0.01", 279 | digits(0.01)), 280 | ?assertEqual("0.001", 281 | digits(0.001)), 282 | ?assertEqual("1.0e+6", 283 | digits(1000000.0)), 284 | ?assertEqual("0.5", 285 | digits(0.5)), 286 | ?assertEqual("4503599627370496.0", 287 | digits(4503599627370496.0)), 288 | %% small denormalized number 289 | %% 4.94065645841246544177e-324 =:= 5.0e-324 290 | <> = <<0,0,0,0,0,0,0,1>>, 291 | ?assertEqual("5.0e-324", 292 | digits(SmallDenorm)), 293 | ?assertEqual(SmallDenorm, 294 | list_to_float(digits(SmallDenorm))), 295 | %% large denormalized number 296 | %% 2.22507385850720088902e-308 297 | <> = <<0,15,255,255,255,255,255,255>>, 298 | ?assertEqual("2.225073858507201e-308", 299 | digits(BigDenorm)), 300 | ?assertEqual(BigDenorm, 301 | list_to_float(digits(BigDenorm))), 302 | %% small normalized number 303 | %% 2.22507385850720138309e-308 304 | <> = <<0,16,0,0,0,0,0,0>>, 305 | ?assertEqual("2.2250738585072014e-308", 306 | digits(SmallNorm)), 307 | ?assertEqual(SmallNorm, 308 | list_to_float(digits(SmallNorm))), 309 | %% large normalized number 310 | %% 1.79769313486231570815e+308 311 | <> = <<127,239,255,255,255,255,255,255>>, 312 | ?assertEqual("1.7976931348623157e+308", 313 | digits(LargeNorm)), 314 | ?assertEqual(LargeNorm, 315 | list_to_float(digits(LargeNorm))), 316 | %% issue #10 - mochinum:frexp(math:pow(2, -1074)). 317 | ?assertEqual("5.0e-324", 318 | digits(math:pow(2, -1074))), 319 | ok. 320 | 321 | frexp_test() -> 322 | %% zero 323 | ?assertEqual({0.0, 0}, frexp(0.0)), 324 | %% one 325 | ?assertEqual({0.5, 1}, frexp(1.0)), 326 | %% negative one 327 | ?assertEqual({-0.5, 1}, frexp(-1.0)), 328 | %% small denormalized number 329 | %% 4.94065645841246544177e-324 330 | <> = <<0,0,0,0,0,0,0,1>>, 331 | ?assertEqual({0.5, -1073}, frexp(SmallDenorm)), 332 | %% large denormalized number 333 | %% 2.22507385850720088902e-308 334 | <> = <<0,15,255,255,255,255,255,255>>, 335 | ?assertEqual( 336 | {0.99999999999999978, -1022}, 337 | frexp(BigDenorm)), 338 | %% small normalized number 339 | %% 2.22507385850720138309e-308 340 | <> = <<0,16,0,0,0,0,0,0>>, 341 | ?assertEqual({0.5, -1021}, frexp(SmallNorm)), 342 | %% large normalized number 343 | %% 1.79769313486231570815e+308 344 | <> = <<127,239,255,255,255,255,255,255>>, 345 | ?assertEqual( 346 | {0.99999999999999989, 1024}, 347 | frexp(LargeNorm)), 348 | %% issue #10 - mochinum:frexp(math:pow(2, -1074)). 349 | ?assertEqual( 350 | {0.5, -1073}, 351 | frexp(math:pow(2, -1074))), 352 | ok. 353 | 354 | -endif. 355 | -------------------------------------------------------------------------------- /src/sockjs_action.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_action). 2 | 3 | % none 4 | -export([welcome_screen/3, options/3, iframe/3, info_test/3]). 5 | % send 6 | -export([xhr_polling/4, xhr_streaming/4, eventsource/4, htmlfile/4, jsonp/4]). 7 | % recv 8 | -export([xhr_send/4, jsonp_send/4]). 9 | % misc 10 | -export([websocket/3, rawwebsocket/3]). 11 | 12 | -include("sockjs_internal.hrl"). 13 | 14 | %% -------------------------------------------------------------------------- 15 | 16 | -define(IFRAME, " 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 |

Don't panic!

29 |

This is a SockJS hidden iframe. It's used for cross domain magic.

30 | 31 | "). 32 | 33 | -define(IFRAME_HTMLFILE, " 34 | 35 | 36 | 37 |

Don't panic!

38 | "). 45 | 46 | %% -------------------------------------------------------------------------- 47 | 48 | -spec welcome_screen(req(), headers(), service()) -> req(). 49 | welcome_screen(Req, Headers, _Service) -> 50 | H = [{"Content-Type", "text/plain; charset=UTF-8"}], 51 | sockjs_http:reply(200, H ++ Headers, 52 | "Welcome to SockJS!\n", Req). 53 | 54 | -spec options(req(), headers(), service()) -> req(). 55 | options(Req, Headers, _Service) -> 56 | sockjs_http:reply(204, Headers, "", Req). 57 | 58 | -spec iframe(req(), headers(), service()) -> req(). 59 | iframe(Req, Headers, #service{sockjs_url = SockjsUrl}) -> 60 | IFrame = io_lib:format(?IFRAME, [SockjsUrl]), 61 | MD5 = "\"" ++ binary_to_list(base64:encode(erlang:md5(IFrame))) ++ "\"", 62 | {H, Req2} = sockjs_http:header('if-none-match', Req), 63 | case H of 64 | MD5 -> sockjs_http:reply(304, Headers, "", Req2); 65 | _ -> sockjs_http:reply( 66 | 200, [{"Content-Type", "text/html; charset=UTF-8"}, 67 | {"ETag", MD5}] ++ Headers, IFrame, Req2) 68 | end. 69 | 70 | 71 | -spec info_test(req(), headers(), service()) -> req(). 72 | info_test(Req, Headers, #service{websocket = Websocket, 73 | cookie_needed = CookieNeeded}) -> 74 | I = [{websocket, Websocket}, 75 | {cookie_needed, CookieNeeded}, 76 | {origins, [<<"*:*">>]}, 77 | {entropy, sockjs_util:rand32()}], 78 | D = sockjs_json:encode({I}), 79 | H = [{"Content-Type", "application/json; charset=UTF-8"}], 80 | sockjs_http:reply(200, H ++ Headers, D, Req). 81 | 82 | %% -------------------------------------------------------------------------- 83 | 84 | -spec xhr_polling(req(), headers(), service(), session()) -> req(). 85 | xhr_polling(Req, Headers, Service, Session) -> 86 | Req1 = chunk_start(Req, Headers), 87 | reply_loop(Req1, Session, 1, fun fmt_xhr/1, Service). 88 | 89 | -spec xhr_streaming(req(), headers(), service(), session()) -> req(). 90 | xhr_streaming(Req, Headers, Service = #service{response_limit = ResponseLimit}, 91 | Session) -> 92 | Req1 = chunk_start(Req, Headers), 93 | %% IE requires 2KB prefix: 94 | %% http://blogs.msdn.com/b/ieinternals/archive/2010/04/06/comet-streaming-in-internet-explorer-with-xmlhttprequest-and-xdomainrequest.aspx 95 | Req2 = chunk(Req1, list_to_binary(string:copies("h", 2048)), 96 | fun fmt_xhr/1), 97 | reply_loop(Req2, Session, ResponseLimit, fun fmt_xhr/1, Service). 98 | 99 | -spec eventsource(req(), headers(), service(), session()) -> req(). 100 | eventsource(Req, Headers, Service = #service{response_limit = ResponseLimit}, 101 | SessionId) -> 102 | Req1 = chunk_start(Req, Headers, "text/event-stream; charset=UTF-8"), 103 | Req2 = chunk(Req1, <<$\r, $\n>>), 104 | reply_loop(Req2, SessionId, ResponseLimit, fun fmt_eventsource/1, Service). 105 | 106 | 107 | -spec htmlfile(req(), headers(), service(), session()) -> req(). 108 | htmlfile(Req, Headers, Service = #service{response_limit = ResponseLimit}, 109 | SessionId) -> 110 | S = fun (Req1, CB) -> 111 | Req2 = chunk_start(Req1, Headers, "text/html; charset=UTF-8"), 112 | IFrame = iolist_to_binary(io_lib:format(?IFRAME_HTMLFILE, [CB])), 113 | %% Safari needs at least 1024 bytes to parse the 114 | %% website. Relevant: 115 | %% http://code.google.com/p/browsersec/wiki/Part2#Survey_of_content_sniffing_behaviors 116 | Padding = string:copies(" ", 1024 - size(IFrame)), 117 | Req3 = chunk(Req2, [IFrame, Padding, <<"\r\n\r\n">>]), 118 | reply_loop(Req3, SessionId, ResponseLimit, fun fmt_htmlfile/1, Service) 119 | end, 120 | verify_callback(Req, S). 121 | 122 | -spec jsonp(req(), headers(), service(), session()) -> req(). 123 | jsonp(Req, Headers, Service, SessionId) -> 124 | S = fun (Req1, CB) -> 125 | Req2 = chunk_start(Req1, Headers), 126 | reply_loop(Req2, SessionId, 1, 127 | fun (Body) -> fmt_jsonp(Body, CB) end, Service) 128 | end, 129 | verify_callback(Req, S). 130 | 131 | verify_callback(Req, Success) -> 132 | {CB, Req1} = sockjs_http:callback(Req), 133 | case CB of 134 | undefined -> 135 | sockjs_http:reply(500, [], "\"callback\" parameter required", Req1); 136 | _ -> 137 | Success(Req1, CB) 138 | end. 139 | 140 | %% -------------------------------------------------------------------------- 141 | 142 | -spec xhr_send(req(), headers(), service(), session()) -> req(). 143 | xhr_send(Req, Headers, _Service, Session) -> 144 | {Body, Req1} = sockjs_http:body(Req), 145 | case handle_recv(Req1, Body, Session) of 146 | {error, Req2} -> 147 | Req2; 148 | ok -> 149 | H = [{"content-type", "text/plain; charset=UTF-8"}], 150 | sockjs_http:reply(204, H ++ Headers, "", Req1) 151 | end. 152 | 153 | -spec jsonp_send(req(), headers(), service(), session()) -> req(). 154 | jsonp_send(Req, Headers, _Service, Session) -> 155 | {Body, Req1} = sockjs_http:body_qs(Req), 156 | case handle_recv(Req1, Body, Session) of 157 | {error, Req2} -> 158 | Req2; 159 | ok -> 160 | H = [{"content-type", "text/plain; charset=UTF-8"}], 161 | sockjs_http:reply(200, H ++ Headers, "ok", Req1) 162 | end. 163 | 164 | handle_recv(Req, Body, Session) -> 165 | case Body of 166 | _Any when Body =:= <<>> -> 167 | {error, sockjs_http:reply(500, [], "Payload expected.", Req)}; 168 | _Any -> 169 | case sockjs_json:decode(Body) of 170 | {ok, Decoded} when is_list(Decoded)-> 171 | sockjs_session:received(Decoded, Session), 172 | ok; 173 | {error, _} -> 174 | {error, sockjs_http:reply(500, [], 175 | "Broken JSON encoding.", Req)} 176 | end 177 | end. 178 | 179 | %% -------------------------------------------------------------------------- 180 | 181 | -define(STILL_OPEN, {2010, "Another connection still open"}). 182 | 183 | chunk_start(Req, Headers) -> 184 | chunk_start(Req, Headers, "application/javascript; charset=UTF-8"). 185 | chunk_start(Req, Headers, ContentType) -> 186 | sockjs_http:chunk_start(200, [{"Content-Type", ContentType}] ++ Headers, 187 | Req). 188 | 189 | reply_loop(Req, SessionId, ResponseLimit, Fmt, Service) -> 190 | Req0 = sockjs_http:hook_tcp_close(Req), 191 | case sockjs_session:reply(SessionId) of 192 | wait -> receive 193 | %% In Cowboy we need to capture async 194 | %% messages from the tcp connection - 195 | %% ie: {active, once}. 196 | {tcp_closed, _} -> 197 | Req0; 198 | %% In Cowboy we may in theory get real 199 | %% http requests, this is bad. 200 | {tcp, _S, Data} -> 201 | error_logger:error_msg( 202 | "Received unexpected data on a " 203 | "long-polling http connection: ~p. " 204 | "Connection aborted.~n", 205 | [Data]), 206 | Req1 = sockjs_http:abruptly_kill(Req), 207 | Req1; 208 | go -> 209 | Req1 = sockjs_http:unhook_tcp_close(Req0), 210 | reply_loop(Req1, SessionId, ResponseLimit, 211 | Fmt, Service) 212 | end; 213 | session_in_use -> Frame = sockjs_util:encode_frame({close, ?STILL_OPEN}), 214 | chunk_end(Req0, Frame, Fmt); 215 | {close, Frame} -> Frame1 = sockjs_util:encode_frame(Frame), 216 | chunk_end(Req0, Frame1, Fmt); 217 | {ok, Frame} -> Frame1 = sockjs_util:encode_frame(Frame), 218 | Frame2 = iolist_to_binary(Frame1), 219 | Req2 = chunk(Req0, Frame2, Fmt), 220 | reply_loop0(Req2, SessionId, 221 | ResponseLimit - size(Frame2), 222 | Fmt, Service) 223 | end. 224 | 225 | reply_loop0(Req, _SessionId, ResponseLimit, _Fmt, _Service) when ResponseLimit =< 0 -> 226 | chunk_end(Req); 227 | reply_loop0(Req, SessionId, ResponseLimit, Fmt, Service) -> 228 | reply_loop(Req, SessionId, ResponseLimit, Fmt, Service). 229 | 230 | chunk(Req, Body) -> 231 | {_, Req1} = sockjs_http:chunk(Body, Req), 232 | Req1. 233 | chunk(Req, Body, Fmt) -> chunk(Req, Fmt(Body)). 234 | 235 | chunk_end(Req) -> sockjs_http:chunk_end(Req). 236 | chunk_end(Req, Body, Fmt) -> Req1 = chunk(Req, Body, Fmt), 237 | chunk_end(Req1). 238 | 239 | -spec fmt_xhr(iodata()) -> iodata(). 240 | fmt_xhr(Body) -> [Body, "\n"]. 241 | 242 | -spec fmt_eventsource(iodata()) -> iodata(). 243 | fmt_eventsource(Body) -> 244 | Escaped = sockjs_util:url_escape(binary_to_list(iolist_to_binary(Body)), 245 | "%\r\n\0"), %% $% must be first! 246 | [<<"data: ">>, Escaped, <<"\r\n\r\n">>]. 247 | 248 | -spec fmt_htmlfile(iodata()) -> iodata(). 249 | fmt_htmlfile(Body) -> 250 | Double = sockjs_json:encode(iolist_to_binary(Body)), 251 | [<<"\r\n">>]. 252 | 253 | -spec fmt_jsonp(iodata(), iodata()) -> iodata(). 254 | fmt_jsonp(Body, Callback) -> 255 | %% Yes, JSONed twice, there isn't a a better way, we must pass 256 | %% a string back, and the script, will be evaled() by the 257 | %% browser. 258 | [Callback, "(", sockjs_json:encode(iolist_to_binary(Body)), ");\r\n"]. 259 | 260 | %% -------------------------------------------------------------------------- 261 | 262 | -spec websocket(req(), headers(), service()) -> req(). 263 | websocket(Req, Headers, Service) -> 264 | {_Any, Req1, {R1, R2}} = sockjs_handler:is_valid_ws(Service, Req), 265 | case {R1, R2} of 266 | {false, _} -> 267 | sockjs_http:reply(400, Headers, 268 | "Can \"Upgrade\" only to \"WebSocket\".", Req1); 269 | {_, false} -> 270 | sockjs_http:reply(400, Headers, 271 | "\"Connection\" must be \"Upgrade\"", Req1); 272 | {true, true} -> 273 | sockjs_http:reply(400, Headers, 274 | "This WebSocket request can't be handled.", Req1) 275 | end. 276 | 277 | -spec rawwebsocket(req(), headers(), service()) -> req(). 278 | rawwebsocket(Req, Headers, Service) -> 279 | websocket(Req, Headers, Service). 280 | -------------------------------------------------------------------------------- /src/sockjs_session.erl: -------------------------------------------------------------------------------- 1 | -module(sockjs_session). 2 | 3 | -behaviour(gen_server). 4 | 5 | -export([init/0, start_link/3]). 6 | -export([maybe_create/3, reply/1, reply/2, received/2]). 7 | -export([send/2, close/3, info/1]). 8 | 9 | 10 | -export([init/1, handle_call/3, handle_info/2, terminate/2, code_change/3, 11 | handle_cast/2]). 12 | 13 | -include("sockjs_internal.hrl"). 14 | -type(handle() :: {?MODULE, {pid(), info()}}). 15 | 16 | -record(session, {id :: session(), 17 | outbound_queue = queue:new() :: queue(), 18 | response_pid :: pid(), 19 | disconnect_tref :: reference(), 20 | disconnect_delay = 5000 :: non_neg_integer(), 21 | heartbeat_tref :: reference() | triggered, 22 | heartbeat_delay = 25000 :: non_neg_integer(), 23 | ready_state = connecting :: connecting | open | closed, 24 | close_msg :: {non_neg_integer(), string()}, 25 | callback, 26 | state, 27 | handle :: handle() 28 | }). 29 | -define(ETS, sockjs_table). 30 | 31 | 32 | -type(session_or_undefined() :: session() | undefined). 33 | -type(session_or_pid() :: session() | pid()). 34 | 35 | %% -------------------------------------------------------------------------- 36 | 37 | -spec init() -> ok. 38 | init() -> 39 | _ = ets:new(?ETS, [public, named_table]), 40 | ok. 41 | 42 | -spec start_link(session_or_undefined(), service(), info()) -> {ok, pid()}. 43 | start_link(SessionId, Service, Info) -> 44 | gen_server:start_link(?MODULE, {SessionId, Service, Info}, []). 45 | 46 | -spec maybe_create(session_or_undefined(), service(), info()) -> pid(). 47 | maybe_create(SessionId, Service, Info) -> 48 | case ets:lookup(?ETS, SessionId) of 49 | [] -> {ok, SPid} = sockjs_session_sup:start_child( 50 | SessionId, Service, Info), 51 | SPid; 52 | [{_, SPid}] -> SPid 53 | end. 54 | 55 | 56 | -spec received(list(iodata()), session_or_pid()) -> ok. 57 | received(Messages, SessionPid) when is_pid(SessionPid) -> 58 | case gen_server:call(SessionPid, {received, Messages}, infinity) of 59 | ok -> ok; 60 | error -> throw(no_session) 61 | %% TODO: should we respond 404 when session is closed? 62 | end; 63 | received(Messages, SessionId) -> 64 | received(Messages, spid(SessionId)). 65 | 66 | -spec send(iodata(), handle()) -> ok. 67 | send(Data, {?MODULE, {SPid, _}}) -> 68 | gen_server:cast(SPid, {send, Data}), 69 | ok. 70 | 71 | -spec close(non_neg_integer(), string(), handle()) -> ok. 72 | close(Code, Reason, {?MODULE, {SPid, _}}) -> 73 | gen_server:cast(SPid, {close, Code, Reason}), 74 | ok. 75 | 76 | -spec info(handle()) -> info(). 77 | info({?MODULE, {_SPid, Info}}) -> 78 | Info. 79 | 80 | -spec reply(session_or_pid()) -> 81 | wait | session_in_use | {ok | close, frame()}. 82 | reply(Session) -> 83 | reply(Session, true). 84 | 85 | -spec reply(session_or_pid(), boolean()) -> 86 | wait | session_in_use | {ok | close, frame()}. 87 | reply(SessionPid, Multiple) when is_pid(SessionPid) -> 88 | gen_server:call(SessionPid, {reply, self(), Multiple}, infinity); 89 | reply(SessionId, Multiple) -> 90 | reply(spid(SessionId), Multiple). 91 | 92 | %% -------------------------------------------------------------------------- 93 | 94 | cancel_timer_safe(Timer, Atom) -> 95 | case erlang:cancel_timer(Timer) of 96 | false -> 97 | receive Atom -> ok 98 | after 0 -> ok end; 99 | _ -> ok 100 | end. 101 | 102 | spid(SessionId) -> 103 | case ets:lookup(?ETS, SessionId) of 104 | [] -> throw(no_session); 105 | [{_, SPid}] -> SPid 106 | end. 107 | 108 | %% Mark a process as waiting for data. 109 | %% 1) The same process may ask for messages multiple times. 110 | mark_waiting(Pid, State = #session{response_pid = Pid, 111 | disconnect_tref = undefined}) -> 112 | State; 113 | %% 2) Noone else waiting - link and start heartbeat timeout. 114 | mark_waiting(Pid, State = #session{response_pid = undefined, 115 | disconnect_tref = DisconnectTRef, 116 | heartbeat_delay = HeartbeatDelay}) 117 | when DisconnectTRef =/= undefined -> 118 | link(Pid), 119 | cancel_timer_safe(DisconnectTRef, session_timeout), 120 | TRef = erlang:send_after(HeartbeatDelay, self(), heartbeat_triggered), 121 | State#session{response_pid = Pid, 122 | disconnect_tref = undefined, 123 | heartbeat_tref = TRef}. 124 | 125 | %% Prolong session lifetime. 126 | %% 1) Maybe clear up response_pid if already awaiting. 127 | unmark_waiting(RPid, State = #session{response_pid = RPid, 128 | heartbeat_tref = HeartbeatTRef, 129 | disconnect_tref = undefined, 130 | disconnect_delay = DisconnectDelay}) -> 131 | unlink(RPid), 132 | _ = case HeartbeatTRef of 133 | undefined -> ok; 134 | triggered -> ok; 135 | _Else -> cancel_timer_safe(HeartbeatTRef, heartbeat_triggered) 136 | end, 137 | TRef = erlang:send_after(DisconnectDelay, self(), session_timeout), 138 | State#session{response_pid = undefined, 139 | heartbeat_tref = undefined, 140 | disconnect_tref = TRef}; 141 | 142 | %% 2) prolong disconnect timer if no connection is waiting 143 | unmark_waiting(_Pid, State = #session{response_pid = undefined, 144 | disconnect_tref = DisconnectTRef, 145 | disconnect_delay = DisconnectDelay}) 146 | when DisconnectTRef =/= undefined -> 147 | cancel_timer_safe(DisconnectTRef, session_timeout), 148 | TRef = erlang:send_after(DisconnectDelay, self(), session_timeout), 149 | State#session{disconnect_tref = TRef}; 150 | 151 | %% 3) Event from someone else? Ignore. 152 | unmark_waiting(RPid, State = #session{response_pid = Pid, 153 | disconnect_tref = undefined}) 154 | when Pid =/= undefined andalso Pid =/= RPid -> 155 | State. 156 | 157 | -spec emit(emittable(), #session{}) -> #session{}. 158 | emit(What, State = #session{callback = Callback, 159 | state = UserState, 160 | handle = Handle}) -> 161 | R = case Callback of 162 | _ when is_function(Callback) -> 163 | Callback(Handle, What, UserState); 164 | _ when is_atom(Callback) -> 165 | case What of 166 | init -> Callback:sockjs_init(Handle, UserState); 167 | {recv, Data} -> Callback:sockjs_handle(Handle, Data, UserState); 168 | closed -> Callback:sockjs_terminate(Handle, UserState) 169 | end 170 | end, 171 | case R of 172 | {ok, UserState1} -> State#session{state = UserState1}; 173 | ok -> State 174 | end. 175 | 176 | %% -------------------------------------------------------------------------- 177 | 178 | -spec init({session_or_undefined(), service(), info()}) -> {ok, #session{}}. 179 | init({SessionId, #service{callback = Callback, 180 | state = UserState, 181 | disconnect_delay = DisconnectDelay, 182 | heartbeat_delay = HeartbeatDelay}, Info}) -> 183 | case SessionId of 184 | undefined -> ok; 185 | _Else -> ets:insert(?ETS, {SessionId, self()}) 186 | end, 187 | process_flag(trap_exit, true), 188 | TRef = erlang:send_after(DisconnectDelay, self(), session_timeout), 189 | {ok, #session{id = SessionId, 190 | callback = Callback, 191 | state = UserState, 192 | response_pid = undefined, 193 | disconnect_tref = TRef, 194 | disconnect_delay = DisconnectDelay, 195 | heartbeat_tref = undefined, 196 | heartbeat_delay = HeartbeatDelay, 197 | handle = {?MODULE, {self(), Info}}}}. 198 | 199 | 200 | handle_call({reply, Pid, _Multiple}, _From, State = #session{ 201 | response_pid = undefined, 202 | ready_state = connecting}) -> 203 | State0 = emit(init, State), 204 | State1 = unmark_waiting(Pid, State0), 205 | {reply, {ok, {open, nil}}, 206 | State1#session{ready_state = open}}; 207 | 208 | handle_call({reply, Pid, _Multiple}, _From, State = #session{ 209 | ready_state = closed, 210 | close_msg = CloseMsg}) -> 211 | State1 = unmark_waiting(Pid, State), 212 | {reply, {close, {close, CloseMsg}}, State1}; 213 | 214 | 215 | handle_call({reply, Pid, _Multiple}, _From, State = #session{ 216 | response_pid = RPid}) 217 | when RPid =/= Pid andalso RPid =/= undefined -> 218 | %% don't use unmark_waiting(), this shouldn't touch the session lifetime 219 | {reply, session_in_use, State}; 220 | 221 | handle_call({reply, Pid, Multiple}, _From, State = #session{ 222 | ready_state = open, 223 | response_pid = RPid, 224 | heartbeat_tref = HeartbeatTRef, 225 | outbound_queue = Q}) 226 | when RPid == undefined orelse RPid == Pid -> 227 | {Messages, Q1} = case Multiple of 228 | true -> {queue:to_list(Q), queue:new()}; 229 | false -> case queue:out(Q) of 230 | {{value, Msg}, Q2} -> {[Msg], Q2}; 231 | {empty, Q2} -> {[], Q2} 232 | end 233 | end, 234 | case {Messages, HeartbeatTRef} of 235 | {[], triggered} -> State1 = unmark_waiting(Pid, State), 236 | {reply, {ok, {heartbeat, nil}}, State1}; 237 | {[], _TRef} -> State1 = mark_waiting(Pid, State), 238 | {reply, wait, State1}; 239 | _More -> State1 = unmark_waiting(Pid, State), 240 | {reply, {ok, {data, Messages}}, 241 | State1#session{outbound_queue = Q1}} 242 | end; 243 | 244 | handle_call({received, Messages}, _From, State = #session{ready_state = open}) -> 245 | State2 = lists:foldl(fun(Msg, State1) -> 246 | emit({recv, iolist_to_binary(Msg)}, State1) 247 | end, State, Messages), 248 | {reply, ok, State2}; 249 | 250 | handle_call({received, _Data}, _From, State = #session{ready_state = _Any}) -> 251 | {reply, error, State}; 252 | 253 | handle_call(Request, _From, State) -> 254 | {stop, {odd_request, Request}, State}. 255 | 256 | 257 | handle_cast({send, Data}, State = #session{outbound_queue = Q, 258 | response_pid = RPid}) -> 259 | case RPid of 260 | undefined -> ok; 261 | _Else -> RPid ! go 262 | end, 263 | {noreply, State#session{outbound_queue = queue:in(Data, Q)}}; 264 | 265 | handle_cast({close, Status, Reason}, State = #session{response_pid = RPid}) -> 266 | case RPid of 267 | undefined -> ok; 268 | _Else -> RPid ! go 269 | end, 270 | {noreply, State#session{ready_state = closed, 271 | close_msg = {Status, Reason}}}; 272 | 273 | handle_cast(Cast, State) -> 274 | {stop, {odd_cast, Cast}, State}. 275 | 276 | 277 | handle_info({'EXIT', Pid, _Reason}, 278 | State = #session{response_pid = Pid}) -> 279 | %% It is illegal for a connection to go away when receiving, we 280 | %% may lose some messages that are in transit. Kill current 281 | %% session. 282 | {stop, normal, State#session{response_pid = undefined}}; 283 | 284 | handle_info(force_shutdown, State) -> 285 | %% Websockets may want to force closure sometimes 286 | {stop, normal, State}; 287 | 288 | handle_info(session_timeout, State = #session{response_pid = undefined}) -> 289 | {stop, normal, State}; 290 | 291 | handle_info(heartbeat_triggered, State = #session{response_pid = RPid}) when RPid =/= undefined -> 292 | RPid ! go, 293 | {noreply, State#session{heartbeat_tref = triggered}}; 294 | 295 | handle_info(Info, State) -> 296 | {stop, {odd_info, Info}, State}. 297 | 298 | 299 | terminate(_, State = #session{id = SessionId}) -> 300 | ets:delete(?ETS, SessionId), 301 | _ = emit(closed, State), 302 | ok. 303 | 304 | code_change(_OldVsn, State, _Extra) -> 305 | {ok, State}. 306 | 307 | -------------------------------------------------------------------------------- /LICENSE-EPL-OTP: -------------------------------------------------------------------------------- 1 | ERLANG PUBLIC LICENSE 2 | Version 1.1 3 | 4 | 1. Definitions. 5 | 6 | 1.1. ``Contributor'' means each entity that creates or contributes to 7 | the creation of Modifications. 8 | 9 | 1.2. ``Contributor Version'' means the combination of the Original 10 | Code, prior Modifications used by a Contributor, and the Modifications 11 | made by that particular Contributor. 12 | 13 | 1.3. ``Covered Code'' means the Original Code or Modifications or the 14 | combination of the Original Code and Modifications, in each case 15 | including portions thereof. 16 | 17 | 1.4. ``Electronic Distribution Mechanism'' means a mechanism generally 18 | accepted in the software development community for the electronic 19 | transfer of data. 20 | 21 | 1.5. ``Executable'' means Covered Code in any form other than Source 22 | Code. 23 | 24 | 1.6. ``Initial Developer'' means the individual or entity identified 25 | as the Initial Developer in the Source Code notice required by Exhibit 26 | A. 27 | 28 | 1.7. ``Larger Work'' means a work which combines Covered Code or 29 | portions thereof with code not governed by the terms of this License. 30 | 31 | 1.8. ``License'' means this document. 32 | 33 | 1.9. ``Modifications'' means any addition to or deletion from the 34 | substance or structure of either the Original Code or any previous 35 | Modifications. When Covered Code is released as a series of files, a 36 | Modification is: 37 | 38 | A. Any addition to or deletion from the contents of a file containing 39 | Original Code or previous Modifications. 40 | 41 | B. Any new file that contains any part of the Original Code or 42 | previous Modifications. 43 | 44 | 1.10. ``Original Code'' means Source Code of computer software code 45 | which is described in the Source Code notice required by Exhibit A as 46 | Original Code, and which, at the time of its release under this 47 | License is not already Covered Code governed by this License. 48 | 49 | 1.11. ``Source Code'' means the preferred form of the Covered Code for 50 | making modifications to it, including all modules it contains, plus 51 | any associated interface definition files, scripts used to control 52 | compilation and installation of an Executable, or a list of source 53 | code differential comparisons against either the Original Code or 54 | another well known, available Covered Code of the Contributor's 55 | choice. The Source Code can be in a compressed or archival form, 56 | provided the appropriate decompression or de-archiving software is 57 | widely available for no charge. 58 | 59 | 1.12. ``You'' means an individual or a legal entity exercising rights 60 | under, and complying with all of the terms of, this License. For legal 61 | entities,``You'' includes any entity which controls, is controlled by, 62 | or is under common control with You. For purposes of this definition, 63 | ``control'' means (a) the power, direct or indirect, to cause the 64 | direction or management of such entity, whether by contract or 65 | otherwise, or (b) ownership of fifty percent (50%) or more of the 66 | outstanding shares or beneficial ownership of such entity. 67 | 68 | 2. Source Code License. 69 | 70 | 2.1. The Initial Developer Grant. 71 | The Initial Developer hereby grants You a world-wide, royalty-free, 72 | non-exclusive license, subject to third party intellectual property 73 | claims: 74 | 75 | (a) to use, reproduce, modify, display, perform, sublicense and 76 | distribute the Original Code (or portions thereof) with or without 77 | Modifications, or as part of a Larger Work; and 78 | 79 | (b) under patents now or hereafter owned or controlled by Initial 80 | Developer, to make, have made, use and sell (``Utilize'') the 81 | Original Code (or portions thereof), but solely to the extent that 82 | any such patent is reasonably necessary to enable You to Utilize 83 | the Original Code (or portions thereof) and not to any greater 84 | extent that may be necessary to Utilize further Modifications or 85 | combinations. 86 | 87 | 2.2. Contributor Grant. 88 | Each Contributor hereby grants You a world-wide, royalty-free, 89 | non-exclusive license, subject to third party intellectual property 90 | claims: 91 | 92 | (a) to use, reproduce, modify, display, perform, sublicense and 93 | distribute the Modifications created by such Contributor (or 94 | portions thereof) either on an unmodified basis, with other 95 | Modifications, as Covered Code or as part of a Larger Work; and 96 | 97 | (b) under patents now or hereafter owned or controlled by Contributor, 98 | to Utilize the Contributor Version (or portions thereof), but 99 | solely to the extent that any such patent is reasonably necessary 100 | to enable You to Utilize the Contributor Version (or portions 101 | thereof), and not to any greater extent that may be necessary to 102 | Utilize further Modifications or combinations. 103 | 104 | 3. Distribution Obligations. 105 | 106 | 3.1. Application of License. 107 | The Modifications which You contribute are governed by the terms of 108 | this License, including without limitation Section 2.2. The Source 109 | Code version of Covered Code may be distributed only under the terms 110 | of this License, and You must include a copy of this License with 111 | every copy of the Source Code You distribute. You may not offer or 112 | impose any terms on any Source Code version that alters or restricts 113 | the applicable version of this License or the recipients' rights 114 | hereunder. However, You may include an additional document offering 115 | the additional rights described in Section 3.5. 116 | 117 | 3.2. Availability of Source Code. 118 | Any Modification which You contribute must be made available in Source 119 | Code form under the terms of this License either on the same media as 120 | an Executable version or via an accepted Electronic Distribution 121 | Mechanism to anyone to whom you made an Executable version available; 122 | and if made available via Electronic Distribution Mechanism, must 123 | remain available for at least twelve (12) months after the date it 124 | initially became available, or at least six (6) months after a 125 | subsequent version of that particular Modification has been made 126 | available to such recipients. You are responsible for ensuring that 127 | the Source Code version remains available even if the Electronic 128 | Distribution Mechanism is maintained by a third party. 129 | 130 | 3.3. Description of Modifications. 131 | You must cause all Covered Code to which you contribute to contain a 132 | file documenting the changes You made to create that Covered Code and 133 | the date of any change. You must include a prominent statement that 134 | the Modification is derived, directly or indirectly, from Original 135 | Code provided by the Initial Developer and including the name of the 136 | Initial Developer in (a) the Source Code, and (b) in any notice in an 137 | Executable version or related documentation in which You describe the 138 | origin or ownership of the Covered Code. 139 | 140 | 3.4. Intellectual Property Matters 141 | 142 | (a) Third Party Claims. 143 | If You have knowledge that a party claims an intellectual property 144 | right in particular functionality or code (or its utilization 145 | under this License), you must include a text file with the source 146 | code distribution titled ``LEGAL'' which describes the claim and 147 | the party making the claim in sufficient detail that a recipient 148 | will know whom to contact. If you obtain such knowledge after You 149 | make Your Modification available as described in Section 3.2, You 150 | shall promptly modify the LEGAL file in all copies You make 151 | available thereafter and shall take other steps (such as notifying 152 | appropriate mailing lists or newsgroups) reasonably calculated to 153 | inform those who received the Covered Code that new knowledge has 154 | been obtained. 155 | 156 | (b) Contributor APIs. 157 | If Your Modification is an application programming interface and 158 | You own or control patents which are reasonably necessary to 159 | implement that API, you must also include this information in the 160 | LEGAL file. 161 | 162 | 3.5. Required Notices. 163 | You must duplicate the notice in Exhibit A in each file of the Source 164 | Code, and this License in any documentation for the Source Code, where 165 | You describe recipients' rights relating to Covered Code. If You 166 | created one or more Modification(s), You may add your name as a 167 | Contributor to the notice described in Exhibit A. If it is not 168 | possible to put such notice in a particular Source Code file due to 169 | its structure, then you must include such notice in a location (such 170 | as a relevant directory file) where a user would be likely to look for 171 | such a notice. You may choose to offer, and to charge a fee for, 172 | warranty, support, indemnity or liability obligations to one or more 173 | recipients of Covered Code. However, You may do so only on Your own 174 | behalf, and not on behalf of the Initial Developer or any 175 | Contributor. You must make it absolutely clear than any such warranty, 176 | support, indemnity or liability obligation is offered by You alone, 177 | and You hereby agree to indemnify the Initial Developer and every 178 | Contributor for any liability incurred by the Initial Developer or 179 | such Contributor as a result of warranty, support, indemnity or 180 | liability terms You offer. 181 | 182 | 3.6. Distribution of Executable Versions. 183 | You may distribute Covered Code in Executable form only if the 184 | requirements of Section 3.1-3.5 have been met for that Covered Code, 185 | and if You include a notice stating that the Source Code version of 186 | the Covered Code is available under the terms of this License, 187 | including a description of how and where You have fulfilled the 188 | obligations of Section 3.2. The notice must be conspicuously included 189 | in any notice in an Executable version, related documentation or 190 | collateral in which You describe recipients' rights relating to the 191 | Covered Code. You may distribute the Executable version of Covered 192 | Code under a license of Your choice, which may contain terms different 193 | from this License, provided that You are in compliance with the terms 194 | of this License and that the license for the Executable version does 195 | not attempt to limit or alter the recipient's rights in the Source 196 | Code version from the rights set forth in this License. If You 197 | distribute the Executable version under a different license You must 198 | make it absolutely clear that any terms which differ from this License 199 | are offered by You alone, not by the Initial Developer or any 200 | Contributor. You hereby agree to indemnify the Initial Developer and 201 | every Contributor for any liability incurred by the Initial Developer 202 | or such Contributor as a result of any such terms You offer. 203 | 204 | 3.7. Larger Works. 205 | You may create a Larger Work by combining Covered Code with other code 206 | not governed by the terms of this License and distribute the Larger 207 | Work as a single product. In such a case, You must make sure the 208 | requirements of this License are fulfilled for the Covered Code. 209 | 210 | 4. Inability to Comply Due to Statute or Regulation. 211 | If it is impossible for You to comply with any of the terms of this 212 | License with respect to some or all of the Covered Code due to statute 213 | or regulation then You must: (a) comply with the terms of this License 214 | to the maximum extent possible; and (b) describe the limitations and 215 | the code they affect. Such description must be included in the LEGAL 216 | file described in Section 3.4 and must be included with all 217 | distributions of the Source Code. Except to the extent prohibited by 218 | statute or regulation, such description must be sufficiently detailed 219 | for a recipient of ordinary skill to be able to understand it. 220 | 221 | 5. Application of this License. 222 | 223 | This License applies to code to which the Initial Developer has 224 | attached the notice in Exhibit A, and to related Covered Code. 225 | 226 | 6. CONNECTION TO MOZILLA PUBLIC LICENSE 227 | 228 | This Erlang License is a derivative work of the Mozilla Public 229 | License, Version 1.0. It contains terms which differ from the Mozilla 230 | Public License, Version 1.0. 231 | 232 | 7. DISCLAIMER OF WARRANTY. 233 | 234 | COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN ``AS IS'' BASIS, 235 | WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, 236 | WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF 237 | DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR 238 | NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF 239 | THE COVERED CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE 240 | IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER 241 | CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR 242 | CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART 243 | OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER 244 | EXCEPT UNDER THIS DISCLAIMER. 245 | 246 | 8. TERMINATION. 247 | This License and the rights granted hereunder will terminate 248 | automatically if You fail to comply with terms herein and fail to cure 249 | such breach within 30 days of becoming aware of the breach. All 250 | sublicenses to the Covered Code which are properly granted shall 251 | survive any termination of this License. Provisions which, by their 252 | nature, must remain in effect beyond the termination of this License 253 | shall survive. 254 | 255 | 9. DISCLAIMER OF LIABILITY 256 | Any utilization of Covered Code shall not cause the Initial Developer 257 | or any Contributor to be liable for any damages (neither direct nor 258 | indirect). 259 | 260 | 10. MISCELLANEOUS 261 | This License represents the complete agreement concerning the subject 262 | matter hereof. If any provision is held to be unenforceable, such 263 | provision shall be reformed only to the extent necessary to make it 264 | enforceable. This License shall be construed by and in accordance with 265 | the substantive laws of Sweden. Any dispute, controversy or claim 266 | arising out of or relating to this License, or the breach, termination 267 | or invalidity thereof, shall be subject to the exclusive jurisdiction 268 | of Swedish courts, with the Stockholm City Court as the first 269 | instance. 270 | 271 | EXHIBIT A. 272 | 273 | ``The contents of this file are subject to the Erlang Public License, 274 | Version 1.1, (the "License"); you may not use this file except in 275 | compliance with the License. You should have received a copy of the 276 | Erlang Public License along with this software. If not, it can be 277 | retrieved via the world wide web at http://www.erlang.org/. 278 | 279 | Software distributed under the License is distributed on an "AS IS" 280 | basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See 281 | the License for the specific language governing rights and limitations 282 | under the License. 283 | 284 | The Initial Developer of the Original Code is Ericsson AB. 285 | Portions created by Ericsson are Copyright 2013, Ericsson AB. 286 | All Rights Reserved.'' 287 | -------------------------------------------------------------------------------- /src/sockjs_pmod_pt.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% %CopyrightBegin% 3 | %% 4 | %% Copyright Ericsson AB 2013. All Rights Reserved. 5 | %% 6 | %% The contents of this file are subject to the Erlang Public License, 7 | %% Version 1.1, (the "License"); you may not use this file except in 8 | %% compliance with the License. You should have received a copy of the 9 | %% Erlang Public License along with this software. If not, it can be 10 | %% retrieved online at http://www.erlang.org/. 11 | %% 12 | %% Software distributed under the License is distributed on an "AS IS" 13 | %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See 14 | %% the License for the specific language governing rights and limitations 15 | %% under the License. 16 | %% 17 | %% %CopyrightEnd% 18 | %% 19 | 20 | -module(sockjs_pmod_pt). 21 | -export([parse_transform/2, 22 | format_error/1]). 23 | 24 | %% Expand function definition forms of parameterized module. 25 | %% The code is based on the code in sys_expand_pmod which used to be 26 | %% included in the compiler, but details are different because 27 | %% sys_pre_expand has not been run. In particular: 28 | %% 29 | %% * Record definitions are still present and must be handled. 30 | %% 31 | %% * (Syntatic) local calls may actually be calls to an imported 32 | %% funtion or a BIF. It is a local call if and only if there 33 | %% is a definition for the function in the module. 34 | %% 35 | %% * When we introduce the module parameters and 'THIS' in each 36 | %% function, we must artificially use it to avoid a warning for 37 | %% unused variables. 38 | %% 39 | %% * On the other hand, we don't have to worry about module_info/0,1 40 | %% because they have not been added yet. 41 | 42 | -record(pmod, {parameters, 43 | defined 44 | }). 45 | 46 | parse_transform(Forms0, _Options) -> 47 | put(?MODULE, []), 48 | Forms = transform(Forms0), 49 | case erase(?MODULE) of 50 | [] -> 51 | Forms; 52 | [_|_]=Errors -> 53 | File = get_file(Forms), 54 | {error,[{File,Errors}],[]} 55 | end. 56 | 57 | format_error(extends_self) -> 58 | "cannot extend from self"; 59 | format_error(define_instance) -> 60 | "defining instance function not allowed in parameterized module". 61 | 62 | add_error(Line, Error) -> 63 | put(?MODULE, get(?MODULE) ++ [{Line,?MODULE,Error}]). 64 | 65 | get_file([{attribute,_,file,{File,_}}|_]) -> File; 66 | get_file([_|T]) -> get_file(T). 67 | 68 | transform(Forms0) -> 69 | Def = collect_defined(Forms0), 70 | {Base,ModAs,Forms1} = attribs(Forms0, [], undefined, []), 71 | {Mod,Ps0} = case ModAs of 72 | {M0,P0} -> {M0,P0}; 73 | M0 -> {M0,undefined} 74 | end, 75 | Forms2 = case Ps0 of 76 | undefined -> 77 | Forms1; 78 | _ -> 79 | pmod_expand(Forms1, Mod, Base, Ps0, Def) 80 | end, 81 | 82 | %% Add new functions. 83 | NewFs0 = maybe_extend(Base, Mod, Ps0), 84 | NewExps = collect_defined(NewFs0), 85 | Forms3 = add_attributes(Forms2, [{attribute,0,export,NewExps}]), 86 | add_new_funcs(Forms3, NewFs0). 87 | 88 | pmod_expand(Forms0, Mod, Base, Ps0, Def) -> 89 | Ps = if is_atom(Base) -> 90 | ['BASE' | Ps0]; 91 | true -> 92 | Ps0 93 | end, 94 | St0 = #pmod{parameters=Ps,defined=gb_sets:from_list(Def)}, 95 | {Forms1,_} = forms(Forms0, St0), 96 | Forms2 = update_exps(Forms1), 97 | Forms3 = update_forms(Forms2), 98 | NewFs0 = add_instance(Mod, Ps, []), 99 | NewFs = ensure_new(Base, Ps0, NewFs0), 100 | Forms = add_new_funcs(Forms3, NewFs), 101 | NewExps = collect_defined(NewFs), 102 | add_attributes(Forms, [{attribute,0,export,NewExps}]). 103 | 104 | add_attributes([{attribute,_,module,_}=F|Fs], Attrs) -> 105 | [F|Attrs++Fs]; 106 | add_attributes([F|Fs], Attrs) -> 107 | [F|add_attributes(Fs, Attrs)]. 108 | 109 | add_new_funcs([{eof,_}|_]=Fs, NewFs) -> 110 | NewFs ++ Fs; 111 | add_new_funcs([F|Fs], Es) -> 112 | [F|add_new_funcs(Fs, Es)]. 113 | 114 | maybe_extend([], _, _) -> 115 | %% No 'extends' attribute. 116 | []; 117 | maybe_extend(Base, _Mod, undefined) -> 118 | %% There is a an 'extends' attribute; the module is not parameterized. 119 | Name = '$handle_undefined_function', 120 | Args = [{var,0,'Func'},{var,0,'Args'}], 121 | Body = [make_apply({atom,0,Base}, {var,0,'Func'}, {var,0,'Args'})], 122 | F = {function,0,Name,2,[{clause,0,Args,[],Body}]}, 123 | [F]; 124 | maybe_extend(Base, Mod, Ps) -> 125 | %% There is a an 'extends' attribute; the module is parameterized. 126 | Name = '$handle_undefined_function', 127 | Args = [{var,0,'Func'},{var,0,'Args'}], 128 | DontCares = [{var,0,'_'} || _ <- Ps], 129 | TuplePs = {tuple,0,[{atom,0,Mod},{var,0,'BaseVars'}|DontCares]}, 130 | G = [{call,0,{atom,0,is_atom}, 131 | [{call,0,{atom,0,element}, 132 | [{integer,0,1},{var,0,'BaseVars'}]}]}], 133 | FixedArgs = make_lists_rev([{var,0,'Rs'}, 134 | {cons,0,{var,0,'BaseVars'},{nil,0}}]), 135 | Body = [{'case',0,make_lists_rev([{var,0,'Args'}]), 136 | [{clause,0,[{cons,0,TuplePs,{var,0,'Rs'}}],[G], 137 | [make_apply({atom,0,Base}, {var,0,'Func'}, FixedArgs)]}, 138 | {clause,0,[{var,0,'_'}],[], 139 | [make_apply({atom,0,Base}, {var,0,'Func'}, {var,0,'Args'})]} 140 | ]}], 141 | F = {function,0,Name,2,[{clause,0,Args,[],Body}]}, 142 | [F]. 143 | 144 | make_apply(M, F, A) -> 145 | {call,0,{remote,0,{atom,0,erlang},{atom,0,apply}},[M,F,A]}. 146 | 147 | make_lists_rev(As) -> 148 | {call,0,{remote,0,{atom,0,lists},{atom,0,reverse}},As}. 149 | 150 | ensure_new(Base, Ps, Fs) -> 151 | case has_new(Fs) of 152 | true -> 153 | Fs; 154 | false -> 155 | add_new(Base, Ps, Fs) 156 | end. 157 | 158 | has_new([{function,_L,new,_A,_Cs} | _Fs]) -> 159 | true; 160 | has_new([_ | Fs]) -> 161 | has_new(Fs); 162 | has_new([]) -> 163 | false. 164 | 165 | add_new(Base, Ps, Fs) -> 166 | Vs = [{var,0,V} || V <- Ps], 167 | As = if is_atom(Base) -> 168 | [{call,0,{remote,0,{atom,0,Base},{atom,0,new}},Vs} | Vs]; 169 | true -> 170 | Vs 171 | end, 172 | Body = [{call,0,{atom,0,instance},As}], 173 | add_func(new, Vs, Body, Fs). 174 | 175 | add_instance(Mod, Ps, Fs) -> 176 | Vs = [{var,0,V} || V <- Ps], 177 | AbsMod = [{tuple,0,[{atom,0,Mod}|Vs]}], 178 | add_func(instance, Vs, AbsMod, Fs). 179 | 180 | add_func(Name, Args, Body, Fs) -> 181 | A = length(Args), 182 | F = {function,0,Name,A,[{clause,0,Args,[],Body}]}, 183 | [F|Fs]. 184 | 185 | collect_defined(Fs) -> 186 | [{N,A} || {function,_,N,A,_} <- Fs]. 187 | 188 | attribs([{attribute,Line,module,{Mod,_}=ModAs}|T], Base, _, Acc) -> 189 | attribs(T, Base, ModAs, [{attribute,Line,module,Mod}|Acc]); 190 | attribs([{attribute,_,module,Mod}=H|T], Base, _, Acc) -> 191 | attribs(T, Base, Mod, [H|Acc]); 192 | attribs([{attribute,Line,extends,Base}|T], Base0, Ps, Acc) when is_atom(Base) -> 193 | Mod = case Ps of 194 | {Mod0,_} -> Mod0; 195 | Mod0 -> Mod0 196 | end, 197 | case Mod of 198 | Base -> 199 | add_error(Line, extends_self), 200 | attribs(T, Base0, Ps, Acc); 201 | _ -> 202 | attribs(T, Base, Ps, Acc) 203 | end; 204 | attribs([H|T], Base, Ps, Acc) -> 205 | attribs(T, Base, Ps, [H|Acc]); 206 | attribs([], Base, Ps, Acc) -> 207 | {Base,Ps,lists:reverse(Acc)}. 208 | 209 | %% This is extremely simplistic for now; all functions get an extra 210 | %% parameter, whether they need it or not, except for static functions. 211 | 212 | update_function_name({F,A}) when F =/= new -> 213 | {F,A+1}; 214 | update_function_name(E) -> 215 | E. 216 | 217 | update_forms([{function,L,N,A,Cs}|Fs]) when N =/= new -> 218 | [{function,L,N,A+1,Cs}|update_forms(Fs)]; 219 | update_forms([F|Fs]) -> 220 | [F|update_forms(Fs)]; 221 | update_forms([]) -> 222 | []. 223 | 224 | update_exps([{attribute,Line,export,Es0}|T]) -> 225 | Es = [update_function_name(E) || E <- Es0], 226 | [{attribute,Line,export,Es}|update_exps(T)]; 227 | update_exps([H|T]) -> 228 | [H|update_exps(T)]; 229 | update_exps([]) -> 230 | []. 231 | 232 | %% Process the program forms. 233 | 234 | forms([F0|Fs0],St0) -> 235 | {F1,St1} = form(F0,St0), 236 | {Fs1,St2} = forms(Fs0,St1), 237 | {[F1|Fs1],St2}; 238 | forms([], St0) -> 239 | {[], St0}. 240 | 241 | %% Only function definitions are of interest here. State is not updated. 242 | form({function,Line,instance,_Arity,_Clauses}=F,St) -> 243 | add_error(Line, define_instance), 244 | {F,St}; 245 | form({function,Line,Name0,Arity0,Clauses0},St) when Name0 =/= new -> 246 | {Name,Arity,Clauses} = function(Name0, Arity0, Clauses0, St), 247 | {{function,Line,Name,Arity,Clauses},St}; 248 | %% Pass anything else through 249 | form(F,St) -> {F,St}. 250 | 251 | function(Name, Arity, Clauses0, St) -> 252 | Clauses1 = clauses(Clauses0,St), 253 | {Name,Arity,Clauses1}. 254 | 255 | clauses([C|Cs],#pmod{parameters=Ps}=St) -> 256 | {clause,L,H,G,B0} = clause(C,St), 257 | T = {tuple,L,[{var,L,V} || V <- ['_'|Ps]]}, 258 | B = [{match,L,{var,L,'_'},{var,L,V}} || V <- ['THIS'|Ps]] ++ B0, 259 | [{clause,L,H++[{match,L,T,{var,L,'THIS'}}],G,B}|clauses(Cs,St)]; 260 | clauses([],_St) -> []. 261 | 262 | clause({clause,Line,H,G,B0},St) -> 263 | %% We never update H and G, so we will just copy them. 264 | B1 = exprs(B0,St), 265 | {clause,Line,H,G,B1}. 266 | 267 | pattern_grp([{bin_element,L1,E1,S1,T1} | Fs],St) -> 268 | S2 = case S1 of 269 | default -> 270 | default; 271 | _ -> 272 | expr(S1,St) 273 | end, 274 | T2 = case T1 of 275 | default -> 276 | default; 277 | _ -> 278 | bit_types(T1) 279 | end, 280 | [{bin_element,L1,expr(E1,St),S2,T2} | pattern_grp(Fs,St)]; 281 | pattern_grp([],_St) -> 282 | []. 283 | 284 | bit_types([]) -> 285 | []; 286 | bit_types([Atom | Rest]) when is_atom(Atom) -> 287 | [Atom | bit_types(Rest)]; 288 | bit_types([{Atom, Integer} | Rest]) when is_atom(Atom), is_integer(Integer) -> 289 | [{Atom, Integer} | bit_types(Rest)]. 290 | 291 | exprs([E0|Es],St) -> 292 | E1 = expr(E0,St), 293 | [E1|exprs(Es,St)]; 294 | exprs([],_St) -> []. 295 | 296 | expr({var,_L,_V}=Var,_St) -> 297 | Var; 298 | expr({integer,_Line,_I}=Integer,_St) -> Integer; 299 | expr({float,_Line,_F}=Float,_St) -> Float; 300 | expr({atom,_Line,_A}=Atom,_St) -> Atom; 301 | expr({string,_Line,_S}=String,_St) -> String; 302 | expr({char,_Line,_C}=Char,_St) -> Char; 303 | expr({nil,_Line}=Nil,_St) -> Nil; 304 | expr({cons,Line,H0,T0},St) -> 305 | H1 = expr(H0,St), 306 | T1 = expr(T0,St), 307 | {cons,Line,H1,T1}; 308 | expr({lc,Line,E0,Qs0},St) -> 309 | Qs1 = lc_bc_quals(Qs0,St), 310 | E1 = expr(E0,St), 311 | {lc,Line,E1,Qs1}; 312 | expr({bc,Line,E0,Qs0},St) -> 313 | Qs1 = lc_bc_quals(Qs0,St), 314 | E1 = expr(E0,St), 315 | {bc,Line,E1,Qs1}; 316 | expr({tuple,Line,Es0},St) -> 317 | Es1 = expr_list(Es0,St), 318 | {tuple,Line,Es1}; 319 | expr({record_index,_,_,_}=RI, _St) -> 320 | RI; 321 | expr({record,Line,Name,Is0},St) -> 322 | Is = record_fields(Is0,St), 323 | {record,Line,Name,Is}; 324 | expr({record,Line,E0,Name,Is0},St) -> 325 | E = expr(E0,St), 326 | Is = record_fields(Is0,St), 327 | {record,Line,E,Name,Is}; 328 | expr({record_field,Line,E0,Name,Key},St) -> 329 | E = expr(E0,St), 330 | {record_field,Line,E,Name,Key}; 331 | expr({block,Line,Es0},St) -> 332 | Es1 = exprs(Es0,St), 333 | {block,Line,Es1}; 334 | expr({'if',Line,Cs0},St) -> 335 | Cs1 = icr_clauses(Cs0,St), 336 | {'if',Line,Cs1}; 337 | expr({'case',Line,E0,Cs0},St) -> 338 | E1 = expr(E0,St), 339 | Cs1 = icr_clauses(Cs0,St), 340 | {'case',Line,E1,Cs1}; 341 | expr({'receive',Line,Cs0},St) -> 342 | Cs1 = icr_clauses(Cs0,St), 343 | {'receive',Line,Cs1}; 344 | expr({'receive',Line,Cs0,To0,ToEs0},St) -> 345 | To1 = expr(To0,St), 346 | ToEs1 = exprs(ToEs0,St), 347 | Cs1 = icr_clauses(Cs0,St), 348 | {'receive',Line,Cs1,To1,ToEs1}; 349 | expr({'try',Line,Es0,Scs0,Ccs0,As0},St) -> 350 | Es1 = exprs(Es0,St), 351 | Scs1 = icr_clauses(Scs0,St), 352 | Ccs1 = icr_clauses(Ccs0,St), 353 | As1 = exprs(As0,St), 354 | {'try',Line,Es1,Scs1,Ccs1,As1}; 355 | expr({'fun',_,{function,_,_,_}}=ExtFun,_St) -> 356 | ExtFun; 357 | expr({'fun',Line,Body},St) -> 358 | case Body of 359 | {clauses,Cs0} -> 360 | Cs1 = fun_clauses(Cs0,St), 361 | {'fun',Line,{clauses,Cs1}}; 362 | {function,F,A} = Function -> 363 | {F1,A1} = update_function_name({F,A}), 364 | if A1 =:= A -> 365 | {'fun',Line,Function}; 366 | true -> 367 | %% Must rewrite local fun-name to a fun that does a 368 | %% call with the extra THIS parameter. 369 | As = make_vars(A, Line), 370 | As1 = As ++ [{var,Line,'THIS'}], 371 | Call = {call,Line,{atom,Line,F1},As1}, 372 | Cs = [{clause,Line,As,[],[Call]}], 373 | {'fun',Line,{clauses,Cs}} 374 | end; 375 | {function,_M,_F,_A} = Fun4 -> %This is an error in lint! 376 | {'fun',Line,Fun4} 377 | end; 378 | expr({call,Lc,{atom,_,instance}=Name,As0},St) -> 379 | %% All local functions 'instance(...)' are static by definition, 380 | %% so they do not take a 'THIS' argument when called 381 | As1 = expr_list(As0,St), 382 | {call,Lc,Name,As1}; 383 | expr({call,Lc,{atom,_,new}=Name,As0},St) -> 384 | %% All local functions 'new(...)' are static by definition, 385 | %% so they do not take a 'THIS' argument when called 386 | As1 = expr_list(As0,St), 387 | {call,Lc,Name,As1}; 388 | expr({call,Lc,{atom,_Lf,F}=Atom,As0}, #pmod{defined=Def}=St) -> 389 | As1 = expr_list(As0,St), 390 | case gb_sets:is_member({F,length(As0)}, Def) of 391 | false -> 392 | %% BIF or imported function. 393 | {call,Lc,Atom,As1}; 394 | true -> 395 | %% Local function call - needs THIS parameter. 396 | {call,Lc,Atom,As1 ++ [{var,0,'THIS'}]} 397 | end; 398 | expr({call,Line,F0,As0},St) -> 399 | %% Other function call 400 | F1 = expr(F0,St), 401 | As1 = expr_list(As0,St), 402 | {call,Line,F1,As1}; 403 | expr({'catch',Line,E0},St) -> 404 | E1 = expr(E0,St), 405 | {'catch',Line,E1}; 406 | expr({match,Line,P,E0},St) -> 407 | E1 = expr(E0,St), 408 | {match,Line,P,E1}; 409 | expr({bin,Line,Fs},St) -> 410 | Fs2 = pattern_grp(Fs,St), 411 | {bin,Line,Fs2}; 412 | expr({op,Line,Op,A0},St) -> 413 | A1 = expr(A0,St), 414 | {op,Line,Op,A1}; 415 | expr({op,Line,Op,L0,R0},St) -> 416 | L1 = expr(L0,St), 417 | R1 = expr(R0,St), 418 | {op,Line,Op,L1,R1}; 419 | %% The following are not allowed to occur anywhere! 420 | expr({remote,Line,M0,F0},St) -> 421 | M1 = expr(M0,St), 422 | F1 = expr(F0,St), 423 | {remote,Line,M1,F1}. 424 | 425 | expr_list([E0|Es],St) -> 426 | E1 = expr(E0,St), 427 | [E1|expr_list(Es,St)]; 428 | expr_list([],_St) -> []. 429 | 430 | record_fields([{record_field,L,K,E0}|T],St) -> 431 | E = expr(E0,St), 432 | [{record_field,L,K,E}|record_fields(T,St)]; 433 | record_fields([],_) -> []. 434 | 435 | icr_clauses([C0|Cs],St) -> 436 | C1 = clause(C0,St), 437 | [C1|icr_clauses(Cs,St)]; 438 | icr_clauses([],_St) -> []. 439 | 440 | lc_bc_quals([{generate,Line,P,E0}|Qs],St) -> 441 | E1 = expr(E0,St), 442 | [{generate,Line,P,E1}|lc_bc_quals(Qs,St)]; 443 | lc_bc_quals([{b_generate,Line,P,E0}|Qs],St) -> 444 | E1 = expr(E0,St), 445 | [{b_generate,Line,P,E1}|lc_bc_quals(Qs,St)]; 446 | lc_bc_quals([E0|Qs],St) -> 447 | E1 = expr(E0,St), 448 | [E1|lc_bc_quals(Qs,St)]; 449 | lc_bc_quals([],_St) -> []. 450 | 451 | fun_clauses([C0|Cs],St) -> 452 | C1 = clause(C0,St), 453 | [C1|fun_clauses(Cs,St)]; 454 | fun_clauses([],_St) -> []. 455 | 456 | make_vars(N, L) -> 457 | make_vars(1, N, L). 458 | 459 | make_vars(N, M, L) when N =< M -> 460 | V = list_to_atom("X"++integer_to_list(N)), 461 | [{var,L,V} | make_vars(N + 1, M, L)]; 462 | make_vars(_, _, _) -> 463 | []. -------------------------------------------------------------------------------- /src/mochijson2_fork.erl: -------------------------------------------------------------------------------- 1 | %% @author Bob Ippolito 2 | %% @copyright 2007 Mochi Media, Inc. 3 | 4 | %% Changes specific to SockJS: support for handling \xFFFE and \xFFFF 5 | %% characters. The usual xmerl_ucs:to_utf8 doesn't work for those (in 6 | %% fact these characters aren't valid unicode characters). But we can 7 | %% support them, why not: 8 | %% 9 | %% diff --git a/src/mochijson2_fork.erl b/src/mochijson2_fork.erl 10 | %% index ddd62c7..8c26fc6 100644 11 | %% --- a/src/mochijson2_fork.erl 12 | %% +++ b/src/mochijson2_fork.erl 13 | %% @@ -458,7 +458,14 @@ tokenize_string(B, S=#decoder{offset=O}, Acc) -> 14 | %% Acc1 = lists:reverse(xmerl_ucs:to_utf8(CodePoint), Acc), 15 | %% tokenize_string(B, ?ADV_COL(S, 12), Acc1); 16 | %% true -> 17 | %% - Acc1 = lists:reverse(xmerl_ucs:to_utf8(C), Acc), 18 | %% + R = if C < 16#FFFE -> 19 | %% + xmerl_ucs:to_utf8(C); 20 | %% + true -> 21 | %% + [16#E0 + (C bsr 12), 22 | %% + 128+((C bsr 6) band 16#3F), 23 | %% + 128+(C band 16#3F)] 24 | %% + end, 25 | %% + Acc1 = lists:reverse(R, Acc), 26 | %% tokenize_string(B, ?ADV_COL(S, 6), Acc1) 27 | %% end; 28 | %% <<_:O/binary, C1, _/binary>> when C1 < 128 -> 29 | %% 30 | 31 | %% @doc Yet another JSON (RFC 4627) library for Erlang. mochijson2 works 32 | %% with binaries as strings, arrays as lists (without an {array, _}) 33 | %% wrapper and it only knows how to decode UTF-8 (and ASCII). 34 | %% 35 | %% JSON terms are decoded as follows (javascript -> erlang): 36 | %%
    37 | %%
  • {"key": "value"} -> 38 | %% {struct, [{<<"key">>, <<"value">>}]}
  • 39 | %%
  • ["array", 123, 12.34, true, false, null] -> 40 | %% [<<"array">>, 123, 12.34, true, false, null] 41 | %%
  • 42 | %%
43 | %%
    44 | %%
  • Strings in JSON decode to UTF-8 binaries in Erlang
  • 45 | %%
  • Objects decode to {struct, PropList}
  • 46 | %%
  • Numbers decode to integer or float
  • 47 | %%
  • true, false, null decode to their respective terms.
  • 48 | %%
49 | %% The encoder will accept the same format that the decoder will produce, 50 | %% but will also allow additional cases for leniency: 51 | %%
    52 | %%
  • atoms other than true, false, null will be considered UTF-8 53 | %% strings (even as a proplist key) 54 | %%
  • 55 | %%
  • {json, IoList} will insert IoList directly into the output 56 | %% with no validation 57 | %%
  • 58 | %%
  • {array, Array} will be encoded as Array 59 | %% (legacy mochijson style) 60 | %%
  • 61 | %%
  • A non-empty raw proplist will be encoded as an object as long 62 | %% as the first pair does not have an atom key of json, struct, 63 | %% or array 64 | %%
  • 65 | %%
66 | 67 | -module(mochijson2_fork). 68 | -author('bob@mochimedia.com'). 69 | -export([encoder/1, encode/1]). 70 | -export([decoder/1, decode/1, decode/2]). 71 | 72 | %% This is a macro to placate syntax highlighters.. 73 | -define(Q, $\"). 74 | -define(ADV_COL(S, N), S#decoder{offset=N+S#decoder.offset, 75 | column=N+S#decoder.column}). 76 | -define(INC_COL(S), S#decoder{offset=1+S#decoder.offset, 77 | column=1+S#decoder.column}). 78 | -define(INC_LINE(S), S#decoder{offset=1+S#decoder.offset, 79 | column=1, 80 | line=1+S#decoder.line}). 81 | -define(INC_CHAR(S, C), 82 | case C of 83 | $\n -> 84 | S#decoder{column=1, 85 | line=1+S#decoder.line, 86 | offset=1+S#decoder.offset}; 87 | _ -> 88 | S#decoder{column=1+S#decoder.column, 89 | offset=1+S#decoder.offset} 90 | end). 91 | -define(IS_WHITESPACE(C), 92 | (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)). 93 | 94 | -type(decoder_option() :: any()). 95 | -type(handler_option() :: any()). 96 | 97 | -type(json_string() :: atom | binary()). 98 | -type(json_number() :: integer() | float()). 99 | -type(json_array() :: [json_term()]). 100 | -type(json_object() :: {struct, [{json_string(), json_term()}]}). 101 | -type(json_eep18_object() :: {[{json_string(), json_term()}]}). 102 | -type(json_iolist() :: {json, iolist()}). 103 | -type(json_term() :: json_string() | json_number() | json_array() | 104 | json_object() | json_eep18_object() | json_iolist()). 105 | 106 | -record(encoder, {handler=null, 107 | utf8=false}). 108 | 109 | -record(decoder, {object_hook=null, 110 | offset=0, 111 | line=1, 112 | column=1, 113 | state=null}). 114 | 115 | -type(utf8_option() :: boolean()). 116 | -type(encoder_option() :: handler_option() | utf8_option()). 117 | -spec encoder([encoder_option()]) -> function(). 118 | %% @doc Create an encoder/1 with the given options. 119 | %% Emit unicode as utf8 (default - false) 120 | encoder(Options) -> 121 | State = parse_encoder_options(Options, #encoder{}), 122 | fun (O) -> json_encode(O, State) end. 123 | 124 | -spec encode(json_term()) -> iolist(). 125 | %% @doc Encode the given as JSON to an iolist. 126 | encode(Any) -> 127 | json_encode(Any, #encoder{}). 128 | 129 | -spec decoder([decoder_option()]) -> function(). 130 | %% @doc Create a decoder/1 with the given options. 131 | decoder(Options) -> 132 | State = parse_decoder_options(Options, #decoder{}), 133 | fun (O) -> json_decode(O, State) end. 134 | 135 | -spec decode(iolist(), [{format, proplist | eep18 | struct}]) -> json_term(). 136 | %% @doc Decode the given iolist to Erlang terms using the given object format 137 | %% for decoding, where proplist returns JSON objects as [{binary(), json_term()}] 138 | %% proplists, eep18 returns JSON objects as {[binary(), json_term()]}, and struct 139 | %% returns them as-is. 140 | decode(S, Options) -> 141 | json_decode(S, parse_decoder_options(Options, #decoder{})). 142 | 143 | -spec decode(iolist()) -> json_term(). 144 | %% @doc Decode the given iolist to Erlang terms. 145 | decode(S) -> 146 | json_decode(S, #decoder{}). 147 | 148 | %% Internal API 149 | 150 | parse_encoder_options([], State) -> 151 | State; 152 | parse_encoder_options([{handler, Handler} | Rest], State) -> 153 | parse_encoder_options(Rest, State#encoder{handler=Handler}); 154 | parse_encoder_options([{utf8, Switch} | Rest], State) -> 155 | parse_encoder_options(Rest, State#encoder{utf8=Switch}). 156 | 157 | parse_decoder_options([], State) -> 158 | State; 159 | parse_decoder_options([{object_hook, Hook} | Rest], State) -> 160 | parse_decoder_options(Rest, State#decoder{object_hook=Hook}); 161 | parse_decoder_options([{format, Format} | Rest], State) 162 | when Format =:= struct orelse Format =:= eep18 orelse Format =:= proplist -> 163 | parse_decoder_options(Rest, State#decoder{object_hook=Format}). 164 | 165 | json_encode(true, _State) -> 166 | <<"true">>; 167 | json_encode(false, _State) -> 168 | <<"false">>; 169 | json_encode(null, _State) -> 170 | <<"null">>; 171 | json_encode(I, _State) when is_integer(I) -> 172 | integer_to_list(I); 173 | json_encode(F, _State) when is_float(F) -> 174 | mochinum_fork:digits(F); 175 | json_encode(S, State) when is_binary(S); is_atom(S) -> 176 | json_encode_string(S, State); 177 | json_encode([{K, _}|_] = Props, State) when (K =/= struct andalso 178 | K =/= array andalso 179 | K =/= json) -> 180 | json_encode_proplist(Props, State); 181 | json_encode({struct, Props}, State) when is_list(Props) -> 182 | json_encode_proplist(Props, State); 183 | json_encode({Props}, State) when is_list(Props) -> 184 | json_encode_proplist(Props, State); 185 | json_encode({}, State) -> 186 | json_encode_proplist([], State); 187 | json_encode(Array, State) when is_list(Array) -> 188 | json_encode_array(Array, State); 189 | json_encode({array, Array}, State) when is_list(Array) -> 190 | json_encode_array(Array, State); 191 | json_encode({json, IoList}, _State) -> 192 | IoList; 193 | json_encode(Bad, #encoder{handler=null}) -> 194 | exit({json_encode, {bad_term, Bad}}); 195 | json_encode(Bad, State=#encoder{handler=Handler}) -> 196 | json_encode(Handler(Bad), State). 197 | 198 | json_encode_array([], _State) -> 199 | <<"[]">>; 200 | json_encode_array(L, State) -> 201 | F = fun (O, Acc) -> 202 | [$,, json_encode(O, State) | Acc] 203 | end, 204 | [$, | Acc1] = lists:foldl(F, "[", L), 205 | lists:reverse([$\] | Acc1]). 206 | 207 | json_encode_proplist([], _State) -> 208 | <<"{}">>; 209 | json_encode_proplist(Props, State) -> 210 | F = fun ({K, V}, Acc) -> 211 | KS = json_encode_string(K, State), 212 | VS = json_encode(V, State), 213 | [$,, VS, $:, KS | Acc] 214 | end, 215 | [$, | Acc1] = lists:foldl(F, "{", Props), 216 | lists:reverse([$\} | Acc1]). 217 | 218 | json_encode_string(A, State) when is_atom(A) -> 219 | L = atom_to_list(A), 220 | case json_string_is_safe(L) of 221 | true -> 222 | [?Q, L, ?Q]; 223 | false -> 224 | json_encode_string_unicode(xmerl_ucs:from_utf8(L), State, [?Q]) 225 | end; 226 | json_encode_string(B, State) when is_binary(B) -> 227 | case json_bin_is_safe(B) of 228 | true -> 229 | [?Q, B, ?Q]; 230 | false -> 231 | json_encode_string_unicode(xmerl_ucs:from_utf8(B), State, [?Q]) 232 | end; 233 | json_encode_string(I, _State) when is_integer(I) -> 234 | [?Q, integer_to_list(I), ?Q]; 235 | json_encode_string(L, State) when is_list(L) -> 236 | case json_string_is_safe(L) of 237 | true -> 238 | [?Q, L, ?Q]; 239 | false -> 240 | json_encode_string_unicode(L, State, [?Q]) 241 | end. 242 | 243 | json_string_is_safe([]) -> 244 | true; 245 | json_string_is_safe([C | Rest]) -> 246 | case C of 247 | ?Q -> 248 | false; 249 | $\\ -> 250 | false; 251 | $\b -> 252 | false; 253 | $\f -> 254 | false; 255 | $\n -> 256 | false; 257 | $\r -> 258 | false; 259 | $\t -> 260 | false; 261 | C when C >= 0, C < $\s; C >= 16#7f, C =< 16#10FFFF -> 262 | false; 263 | C when C < 16#7f -> 264 | json_string_is_safe(Rest); 265 | _ -> 266 | false 267 | end. 268 | 269 | json_bin_is_safe(<<>>) -> 270 | true; 271 | json_bin_is_safe(<>) -> 272 | case C of 273 | ?Q -> 274 | false; 275 | $\\ -> 276 | false; 277 | $\b -> 278 | false; 279 | $\f -> 280 | false; 281 | $\n -> 282 | false; 283 | $\r -> 284 | false; 285 | $\t -> 286 | false; 287 | C when C >= 0, C < $\s; C >= 16#7f -> 288 | false; 289 | C when C < 16#7f -> 290 | json_bin_is_safe(Rest) 291 | end. 292 | 293 | json_encode_string_unicode([], _State, Acc) -> 294 | lists:reverse([$\" | Acc]); 295 | json_encode_string_unicode([C | Cs], State, Acc) -> 296 | Acc1 = case C of 297 | ?Q -> 298 | [?Q, $\\ | Acc]; 299 | %% Escaping solidus is only useful when trying to protect 300 | %% against "" injection attacks which are only 301 | %% possible when JSON is inserted into a HTML document 302 | %% in-line. mochijson2 does not protect you from this, so 303 | %% if you do insert directly into HTML then you need to 304 | %% uncomment the following case or escape the output of encode. 305 | %% 306 | %% $/ -> 307 | %% [$/, $\\ | Acc]; 308 | %% 309 | $\\ -> 310 | [$\\, $\\ | Acc]; 311 | $\b -> 312 | [$b, $\\ | Acc]; 313 | $\f -> 314 | [$f, $\\ | Acc]; 315 | $\n -> 316 | [$n, $\\ | Acc]; 317 | $\r -> 318 | [$r, $\\ | Acc]; 319 | $\t -> 320 | [$t, $\\ | Acc]; 321 | C when C >= 0, C < $\s -> 322 | [unihex(C) | Acc]; 323 | C when C >= 16#7f, C =< 16#10FFFF, State#encoder.utf8 -> 324 | [xmerl_ucs:to_utf8(C) | Acc]; 325 | C when C >= 16#7f, C =< 16#10FFFF, not State#encoder.utf8 -> 326 | [unihex(C) | Acc]; 327 | C when C < 16#7f -> 328 | [C | Acc]; 329 | _ -> 330 | exit({json_encode, {bad_char, C}}) 331 | end, 332 | json_encode_string_unicode(Cs, State, Acc1). 333 | 334 | hexdigit(C) when C >= 0, C =< 9 -> 335 | C + $0; 336 | hexdigit(C) when C =< 15 -> 337 | C + $a - 10. 338 | 339 | unihex(C) when C < 16#10000 -> 340 | <> = <>, 341 | Digits = [hexdigit(D) || D <- [D3, D2, D1, D0]], 342 | [$\\, $u | Digits]; 343 | unihex(C) when C =< 16#10FFFF -> 344 | N = C - 16#10000, 345 | S1 = 16#d800 bor ((N bsr 10) band 16#3ff), 346 | S2 = 16#dc00 bor (N band 16#3ff), 347 | [unihex(S1), unihex(S2)]. 348 | 349 | json_decode(L, S) when is_list(L) -> 350 | json_decode(iolist_to_binary(L), S); 351 | json_decode(B, S) -> 352 | {Res, S1} = decode1(B, S), 353 | {eof, _} = tokenize(B, S1#decoder{state=trim}), 354 | Res. 355 | 356 | decode1(B, S=#decoder{state=null}) -> 357 | case tokenize(B, S#decoder{state=any}) of 358 | {{const, C}, S1} -> 359 | {C, S1}; 360 | {start_array, S1} -> 361 | decode_array(B, S1); 362 | {start_object, S1} -> 363 | decode_object(B, S1) 364 | end. 365 | 366 | make_object(V, #decoder{object_hook=N}) when N =:= null orelse N =:= struct -> 367 | V; 368 | make_object({struct, P}, #decoder{object_hook=eep18}) -> 369 | {P}; 370 | make_object({struct, P}, #decoder{object_hook=proplist}) -> 371 | P; 372 | make_object(V, #decoder{object_hook=Hook}) -> 373 | Hook(V). 374 | 375 | decode_object(B, S) -> 376 | decode_object(B, S#decoder{state=key}, []). 377 | 378 | decode_object(B, S=#decoder{state=key}, Acc) -> 379 | case tokenize(B, S) of 380 | {end_object, S1} -> 381 | V = make_object({struct, lists:reverse(Acc)}, S1), 382 | {V, S1#decoder{state=null}}; 383 | {{const, K}, S1} -> 384 | {colon, S2} = tokenize(B, S1), 385 | {V, S3} = decode1(B, S2#decoder{state=null}), 386 | decode_object(B, S3#decoder{state=comma}, [{K, V} | Acc]) 387 | end; 388 | decode_object(B, S=#decoder{state=comma}, Acc) -> 389 | case tokenize(B, S) of 390 | {end_object, S1} -> 391 | V = make_object({struct, lists:reverse(Acc)}, S1), 392 | {V, S1#decoder{state=null}}; 393 | {comma, S1} -> 394 | decode_object(B, S1#decoder{state=key}, Acc) 395 | end. 396 | 397 | decode_array(B, S) -> 398 | decode_array(B, S#decoder{state=any}, []). 399 | 400 | decode_array(B, S=#decoder{state=any}, Acc) -> 401 | case tokenize(B, S) of 402 | {end_array, S1} -> 403 | {lists:reverse(Acc), S1#decoder{state=null}}; 404 | {start_array, S1} -> 405 | {Array, S2} = decode_array(B, S1), 406 | decode_array(B, S2#decoder{state=comma}, [Array | Acc]); 407 | {start_object, S1} -> 408 | {Array, S2} = decode_object(B, S1), 409 | decode_array(B, S2#decoder{state=comma}, [Array | Acc]); 410 | {{const, Const}, S1} -> 411 | decode_array(B, S1#decoder{state=comma}, [Const | Acc]) 412 | end; 413 | decode_array(B, S=#decoder{state=comma}, Acc) -> 414 | case tokenize(B, S) of 415 | {end_array, S1} -> 416 | {lists:reverse(Acc), S1#decoder{state=null}}; 417 | {comma, S1} -> 418 | decode_array(B, S1#decoder{state=any}, Acc) 419 | end. 420 | 421 | tokenize_string(B, S=#decoder{offset=O}) -> 422 | case tokenize_string_fast(B, O) of 423 | {escape, O1} -> 424 | Length = O1 - O, 425 | S1 = ?ADV_COL(S, Length), 426 | <<_:O/binary, Head:Length/binary, _/binary>> = B, 427 | tokenize_string(B, S1, lists:reverse(binary_to_list(Head))); 428 | O1 -> 429 | Length = O1 - O, 430 | <<_:O/binary, String:Length/binary, ?Q, _/binary>> = B, 431 | {{const, String}, ?ADV_COL(S, Length + 1)} 432 | end. 433 | 434 | tokenize_string_fast(B, O) -> 435 | case B of 436 | <<_:O/binary, ?Q, _/binary>> -> 437 | O; 438 | <<_:O/binary, $\\, _/binary>> -> 439 | {escape, O}; 440 | <<_:O/binary, C1, _/binary>> when C1 < 128 -> 441 | tokenize_string_fast(B, 1 + O); 442 | <<_:O/binary, C1, C2, _/binary>> when C1 >= 194, C1 =< 223, 443 | C2 >= 128, C2 =< 191 -> 444 | tokenize_string_fast(B, 2 + O); 445 | <<_:O/binary, C1, C2, C3, _/binary>> when C1 >= 224, C1 =< 239, 446 | C2 >= 128, C2 =< 191, 447 | C3 >= 128, C3 =< 191 -> 448 | tokenize_string_fast(B, 3 + O); 449 | <<_:O/binary, C1, C2, C3, C4, _/binary>> when C1 >= 240, C1 =< 244, 450 | C2 >= 128, C2 =< 191, 451 | C3 >= 128, C3 =< 191, 452 | C4 >= 128, C4 =< 191 -> 453 | tokenize_string_fast(B, 4 + O); 454 | _ -> 455 | throw(invalid_utf8) 456 | end. 457 | 458 | tokenize_string(B, S=#decoder{offset=O}, Acc) -> 459 | case B of 460 | <<_:O/binary, ?Q, _/binary>> -> 461 | {{const, iolist_to_binary(lists:reverse(Acc))}, ?INC_COL(S)}; 462 | <<_:O/binary, "\\\"", _/binary>> -> 463 | tokenize_string(B, ?ADV_COL(S, 2), [$\" | Acc]); 464 | <<_:O/binary, "\\\\", _/binary>> -> 465 | tokenize_string(B, ?ADV_COL(S, 2), [$\\ | Acc]); 466 | <<_:O/binary, "\\/", _/binary>> -> 467 | tokenize_string(B, ?ADV_COL(S, 2), [$/ | Acc]); 468 | <<_:O/binary, "\\b", _/binary>> -> 469 | tokenize_string(B, ?ADV_COL(S, 2), [$\b | Acc]); 470 | <<_:O/binary, "\\f", _/binary>> -> 471 | tokenize_string(B, ?ADV_COL(S, 2), [$\f | Acc]); 472 | <<_:O/binary, "\\n", _/binary>> -> 473 | tokenize_string(B, ?ADV_COL(S, 2), [$\n | Acc]); 474 | <<_:O/binary, "\\r", _/binary>> -> 475 | tokenize_string(B, ?ADV_COL(S, 2), [$\r | Acc]); 476 | <<_:O/binary, "\\t", _/binary>> -> 477 | tokenize_string(B, ?ADV_COL(S, 2), [$\t | Acc]); 478 | <<_:O/binary, "\\u", C3, C2, C1, C0, Rest/binary>> -> 479 | C = erlang:list_to_integer([C3, C2, C1, C0], 16), 480 | if C > 16#D7FF, C < 16#DC00 -> 481 | %% coalesce UTF-16 surrogate pair 482 | <<"\\u", D3, D2, D1, D0, _/binary>> = Rest, 483 | D = erlang:list_to_integer([D3,D2,D1,D0], 16), 484 | [CodePoint] = xmerl_ucs:from_utf16be(<>), 486 | Acc1 = lists:reverse(xmerl_ucs:to_utf8(CodePoint), Acc), 487 | tokenize_string(B, ?ADV_COL(S, 12), Acc1); 488 | true -> 489 | R = if C < 16#FFFE -> 490 | xmerl_ucs:to_utf8(C); 491 | true -> 492 | [16#E0 + (C bsr 12), 493 | 128+((C bsr 6) band 16#3F), 494 | 128+(C band 16#3F)] 495 | end, 496 | Acc1 = lists:reverse(R, Acc), 497 | tokenize_string(B, ?ADV_COL(S, 6), Acc1) 498 | end; 499 | <<_:O/binary, C1, _/binary>> when C1 < 128 -> 500 | tokenize_string(B, ?INC_CHAR(S, C1), [C1 | Acc]); 501 | <<_:O/binary, C1, C2, _/binary>> when C1 >= 194, C1 =< 223, 502 | C2 >= 128, C2 =< 191 -> 503 | tokenize_string(B, ?ADV_COL(S, 2), [C2, C1 | Acc]); 504 | <<_:O/binary, C1, C2, C3, _/binary>> when C1 >= 224, C1 =< 239, 505 | C2 >= 128, C2 =< 191, 506 | C3 >= 128, C3 =< 191 -> 507 | tokenize_string(B, ?ADV_COL(S, 3), [C3, C2, C1 | Acc]); 508 | <<_:O/binary, C1, C2, C3, C4, _/binary>> when C1 >= 240, C1 =< 244, 509 | C2 >= 128, C2 =< 191, 510 | C3 >= 128, C3 =< 191, 511 | C4 >= 128, C4 =< 191 -> 512 | tokenize_string(B, ?ADV_COL(S, 4), [C4, C3, C2, C1 | Acc]); 513 | _ -> 514 | throw(invalid_utf8) 515 | end. 516 | 517 | tokenize_number(B, S) -> 518 | case tokenize_number(B, sign, S, []) of 519 | {{int, Int}, S1} -> 520 | {{const, list_to_integer(Int)}, S1}; 521 | {{float, Float}, S1} -> 522 | {{const, list_to_float(Float)}, S1} 523 | end. 524 | 525 | tokenize_number(B, sign, S=#decoder{offset=O}, []) -> 526 | case B of 527 | <<_:O/binary, $-, _/binary>> -> 528 | tokenize_number(B, int, ?INC_COL(S), [$-]); 529 | _ -> 530 | tokenize_number(B, int, S, []) 531 | end; 532 | tokenize_number(B, int, S=#decoder{offset=O}, Acc) -> 533 | case B of 534 | <<_:O/binary, $0, _/binary>> -> 535 | tokenize_number(B, frac, ?INC_COL(S), [$0 | Acc]); 536 | <<_:O/binary, C, _/binary>> when C >= $1 andalso C =< $9 -> 537 | tokenize_number(B, int1, ?INC_COL(S), [C | Acc]) 538 | end; 539 | tokenize_number(B, int1, S=#decoder{offset=O}, Acc) -> 540 | case B of 541 | <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> 542 | tokenize_number(B, int1, ?INC_COL(S), [C | Acc]); 543 | _ -> 544 | tokenize_number(B, frac, S, Acc) 545 | end; 546 | tokenize_number(B, frac, S=#decoder{offset=O}, Acc) -> 547 | case B of 548 | <<_:O/binary, $., C, _/binary>> when C >= $0, C =< $9 -> 549 | tokenize_number(B, frac1, ?ADV_COL(S, 2), [C, $. | Acc]); 550 | <<_:O/binary, E, _/binary>> when E =:= $e orelse E =:= $E -> 551 | tokenize_number(B, esign, ?INC_COL(S), [$e, $0, $. | Acc]); 552 | _ -> 553 | {{int, lists:reverse(Acc)}, S} 554 | end; 555 | tokenize_number(B, frac1, S=#decoder{offset=O}, Acc) -> 556 | case B of 557 | <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> 558 | tokenize_number(B, frac1, ?INC_COL(S), [C | Acc]); 559 | <<_:O/binary, E, _/binary>> when E =:= $e orelse E =:= $E -> 560 | tokenize_number(B, esign, ?INC_COL(S), [$e | Acc]); 561 | _ -> 562 | {{float, lists:reverse(Acc)}, S} 563 | end; 564 | tokenize_number(B, esign, S=#decoder{offset=O}, Acc) -> 565 | case B of 566 | <<_:O/binary, C, _/binary>> when C =:= $- orelse C=:= $+ -> 567 | tokenize_number(B, eint, ?INC_COL(S), [C | Acc]); 568 | _ -> 569 | tokenize_number(B, eint, S, Acc) 570 | end; 571 | tokenize_number(B, eint, S=#decoder{offset=O}, Acc) -> 572 | case B of 573 | <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> 574 | tokenize_number(B, eint1, ?INC_COL(S), [C | Acc]) 575 | end; 576 | tokenize_number(B, eint1, S=#decoder{offset=O}, Acc) -> 577 | case B of 578 | <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> 579 | tokenize_number(B, eint1, ?INC_COL(S), [C | Acc]); 580 | _ -> 581 | {{float, lists:reverse(Acc)}, S} 582 | end. 583 | 584 | tokenize(B, S=#decoder{offset=O}) -> 585 | case B of 586 | <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) -> 587 | tokenize(B, ?INC_CHAR(S, C)); 588 | <<_:O/binary, "{", _/binary>> -> 589 | {start_object, ?INC_COL(S)}; 590 | <<_:O/binary, "}", _/binary>> -> 591 | {end_object, ?INC_COL(S)}; 592 | <<_:O/binary, "[", _/binary>> -> 593 | {start_array, ?INC_COL(S)}; 594 | <<_:O/binary, "]", _/binary>> -> 595 | {end_array, ?INC_COL(S)}; 596 | <<_:O/binary, ",", _/binary>> -> 597 | {comma, ?INC_COL(S)}; 598 | <<_:O/binary, ":", _/binary>> -> 599 | {colon, ?INC_COL(S)}; 600 | <<_:O/binary, "null", _/binary>> -> 601 | {{const, null}, ?ADV_COL(S, 4)}; 602 | <<_:O/binary, "true", _/binary>> -> 603 | {{const, true}, ?ADV_COL(S, 4)}; 604 | <<_:O/binary, "false", _/binary>> -> 605 | {{const, false}, ?ADV_COL(S, 5)}; 606 | <<_:O/binary, "\"", _/binary>> -> 607 | tokenize_string(B, ?INC_COL(S)); 608 | <<_:O/binary, C, _/binary>> when (C >= $0 andalso C =< $9) 609 | orelse C =:= $- -> 610 | tokenize_number(B, S); 611 | <<_:O/binary>> -> 612 | trim = S#decoder.state, 613 | {eof, S} 614 | end. 615 | %% 616 | %% Tests 617 | %% 618 | -ifdef(TEST). 619 | -include_lib("eunit/include/eunit.hrl"). 620 | 621 | 622 | %% testing constructs borrowed from the Yaws JSON implementation. 623 | 624 | %% Create an object from a list of Key/Value pairs. 625 | 626 | obj_new() -> 627 | {struct, []}. 628 | 629 | is_obj({struct, Props}) -> 630 | F = fun ({K, _}) when is_binary(K) -> true end, 631 | lists:all(F, Props). 632 | 633 | obj_from_list(Props) -> 634 | Obj = {struct, Props}, 635 | ?assert(is_obj(Obj)), 636 | Obj. 637 | 638 | %% Test for equivalence of Erlang terms. 639 | %% Due to arbitrary order of construction, equivalent objects might 640 | %% compare unequal as erlang terms, so we need to carefully recurse 641 | %% through aggregates (tuples and objects). 642 | 643 | equiv({struct, Props1}, {struct, Props2}) -> 644 | equiv_object(Props1, Props2); 645 | equiv(L1, L2) when is_list(L1), is_list(L2) -> 646 | equiv_list(L1, L2); 647 | equiv(N1, N2) when is_number(N1), is_number(N2) -> N1 == N2; 648 | equiv(B1, B2) when is_binary(B1), is_binary(B2) -> B1 == B2; 649 | equiv(A, A) when A =:= true orelse A =:= false orelse A =:= null -> true. 650 | 651 | %% Object representation and traversal order is unknown. 652 | %% Use the sledgehammer and sort property lists. 653 | 654 | equiv_object(Props1, Props2) -> 655 | L1 = lists:keysort(1, Props1), 656 | L2 = lists:keysort(1, Props2), 657 | Pairs = lists:zip(L1, L2), 658 | true = lists:all(fun({{K1, V1}, {K2, V2}}) -> 659 | equiv(K1, K2) and equiv(V1, V2) 660 | end, Pairs). 661 | 662 | %% Recursively compare tuple elements for equivalence. 663 | 664 | equiv_list([], []) -> 665 | true; 666 | equiv_list([V1 | L1], [V2 | L2]) -> 667 | equiv(V1, V2) andalso equiv_list(L1, L2). 668 | 669 | decode_test() -> 670 | [1199344435545.0, 1] = decode(<<"[1199344435545.0,1]">>), 671 | <<16#F0,16#9D,16#9C,16#95>> = decode([34,"\\ud835","\\udf15",34]). 672 | 673 | e2j_vec_test() -> 674 | test_one(e2j_test_vec(utf8), 1). 675 | 676 | test_one([], _N) -> 677 | %% io:format("~p tests passed~n", [N-1]), 678 | ok; 679 | test_one([{E, J} | Rest], N) -> 680 | %% io:format("[~p] ~p ~p~n", [N, E, J]), 681 | true = equiv(E, decode(J)), 682 | true = equiv(E, decode(encode(E))), 683 | test_one(Rest, 1+N). 684 | 685 | e2j_test_vec(utf8) -> 686 | [ 687 | {1, "1"}, 688 | {3.1416, "3.14160"}, %% text representation may truncate, trail zeroes 689 | {-1, "-1"}, 690 | {-3.1416, "-3.14160"}, 691 | {12.0e10, "1.20000e+11"}, 692 | {1.234E+10, "1.23400e+10"}, 693 | {-1.234E-10, "-1.23400e-10"}, 694 | {10.0, "1.0e+01"}, 695 | {123.456, "1.23456E+2"}, 696 | {10.0, "1e1"}, 697 | {<<"foo">>, "\"foo\""}, 698 | {<<"foo", 5, "bar">>, "\"foo\\u0005bar\""}, 699 | {<<"">>, "\"\""}, 700 | {<<"\n\n\n">>, "\"\\n\\n\\n\""}, 701 | {<<"\" \b\f\r\n\t\"">>, "\"\\\" \\b\\f\\r\\n\\t\\\"\""}, 702 | {obj_new(), "{}"}, 703 | {obj_from_list([{<<"foo">>, <<"bar">>}]), "{\"foo\":\"bar\"}"}, 704 | {obj_from_list([{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]), 705 | "{\"foo\":\"bar\",\"baz\":123}"}, 706 | {[], "[]"}, 707 | {[[]], "[[]]"}, 708 | {[1, <<"foo">>], "[1,\"foo\"]"}, 709 | 710 | %% json array in a json object 711 | {obj_from_list([{<<"foo">>, [123]}]), 712 | "{\"foo\":[123]}"}, 713 | 714 | %% json object in a json object 715 | {obj_from_list([{<<"foo">>, obj_from_list([{<<"bar">>, true}])}]), 716 | "{\"foo\":{\"bar\":true}}"}, 717 | 718 | %% fold evaluation order 719 | {obj_from_list([{<<"foo">>, []}, 720 | {<<"bar">>, obj_from_list([{<<"baz">>, true}])}, 721 | {<<"alice">>, <<"bob">>}]), 722 | "{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}"}, 723 | 724 | %% json object in a json array 725 | {[-123, <<"foo">>, obj_from_list([{<<"bar">>, []}]), null], 726 | "[-123,\"foo\",{\"bar\":[]},null]"} 727 | ]. 728 | 729 | %% test utf8 encoding 730 | encoder_utf8_test() -> 731 | %% safe conversion case (default) 732 | [34,"\\u0001","\\u0442","\\u0435","\\u0441","\\u0442",34] = 733 | encode(<<1,"\321\202\320\265\321\201\321\202">>), 734 | 735 | %% raw utf8 output (optional) 736 | Enc = mochijson2:encoder([{utf8, true}]), 737 | [34,"\\u0001",[209,130],[208,181],[209,129],[209,130],34] = 738 | Enc(<<1,"\321\202\320\265\321\201\321\202">>). 739 | 740 | input_validation_test() -> 741 | Good = [ 742 | {16#00A3, <>}, %% pound 743 | {16#20AC, <>}, %% euro 744 | {16#10196, <>} %% denarius 745 | ], 746 | lists:foreach(fun({CodePoint, UTF8}) -> 747 | Expect = list_to_binary(xmerl_ucs:to_utf8(CodePoint)), 748 | Expect = decode(UTF8) 749 | end, Good), 750 | 751 | Bad = [ 752 | %% 2nd, 3rd, or 4th byte of a multi-byte sequence w/o leading byte 753 | <>, 754 | %% missing continuations, last byte in each should be 80-BF 755 | <>, 756 | <>, 757 | <>, 758 | %% we don't support code points > 10FFFF per RFC 3629 759 | <>, 760 | %% escape characters trigger a different code path 761 | <> 762 | ], 763 | lists:foreach( 764 | fun(X) -> 765 | ok = try decode(X) catch invalid_utf8 -> ok end, 766 | %% could be {ucs,{bad_utf8_character_code}} or 767 | %% {json_encode,{bad_char,_}} 768 | {'EXIT', _} = (catch encode(X)) 769 | end, Bad). 770 | 771 | inline_json_test() -> 772 | ?assertEqual(<<"\"iodata iodata\"">>, 773 | iolist_to_binary( 774 | encode({json, [<<"\"iodata">>, " iodata\""]}))), 775 | ?assertEqual({struct, [{<<"key">>, <<"iodata iodata">>}]}, 776 | decode( 777 | encode({struct, 778 | [{key, {json, [<<"\"iodata">>, " iodata\""]}}]}))), 779 | ok. 780 | 781 | big_unicode_test() -> 782 | UTF8Seq = list_to_binary(xmerl_ucs:to_utf8(16#0001d120)), 783 | ?assertEqual( 784 | <<"\"\\ud834\\udd20\"">>, 785 | iolist_to_binary(encode(UTF8Seq))), 786 | ?assertEqual( 787 | UTF8Seq, 788 | decode(iolist_to_binary(encode(UTF8Seq)))), 789 | ok. 790 | 791 | custom_decoder_test() -> 792 | ?assertEqual( 793 | {struct, [{<<"key">>, <<"value">>}]}, 794 | (decoder([]))("{\"key\": \"value\"}")), 795 | F = fun ({struct, [{<<"key">>, <<"value">>}]}) -> win end, 796 | ?assertEqual( 797 | win, 798 | (decoder([{object_hook, F}]))("{\"key\": \"value\"}")), 799 | ok. 800 | 801 | atom_test() -> 802 | %% JSON native atoms 803 | [begin 804 | ?assertEqual(A, decode(atom_to_list(A))), 805 | ?assertEqual(iolist_to_binary(atom_to_list(A)), 806 | iolist_to_binary(encode(A))) 807 | end || A <- [true, false, null]], 808 | %% Atom to string 809 | ?assertEqual( 810 | <<"\"foo\"">>, 811 | iolist_to_binary(encode(foo))), 812 | ?assertEqual( 813 | <<"\"\\ud834\\udd20\"">>, 814 | iolist_to_binary(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))), 815 | ok. 816 | 817 | key_encode_test() -> 818 | %% Some forms are accepted as keys that would not be strings in other 819 | %% cases 820 | ?assertEqual( 821 | <<"{\"foo\":1}">>, 822 | iolist_to_binary(encode({struct, [{foo, 1}]}))), 823 | ?assertEqual( 824 | <<"{\"foo\":1}">>, 825 | iolist_to_binary(encode({struct, [{<<"foo">>, 1}]}))), 826 | ?assertEqual( 827 | <<"{\"foo\":1}">>, 828 | iolist_to_binary(encode({struct, [{"foo", 1}]}))), 829 | ?assertEqual( 830 | <<"{\"foo\":1}">>, 831 | iolist_to_binary(encode([{foo, 1}]))), 832 | ?assertEqual( 833 | <<"{\"foo\":1}">>, 834 | iolist_to_binary(encode([{<<"foo">>, 1}]))), 835 | ?assertEqual( 836 | <<"{\"foo\":1}">>, 837 | iolist_to_binary(encode([{"foo", 1}]))), 838 | ?assertEqual( 839 | <<"{\"\\ud834\\udd20\":1}">>, 840 | iolist_to_binary( 841 | encode({struct, [{[16#0001d120], 1}]}))), 842 | ?assertEqual( 843 | <<"{\"1\":1}">>, 844 | iolist_to_binary(encode({struct, [{1, 1}]}))), 845 | ok. 846 | 847 | unsafe_chars_test() -> 848 | Chars = "\"\\\b\f\n\r\t", 849 | [begin 850 | ?assertEqual(false, json_string_is_safe([C])), 851 | ?assertEqual(false, json_bin_is_safe(<>)), 852 | ?assertEqual(<>, decode(encode(<>))) 853 | end || C <- Chars], 854 | ?assertEqual( 855 | false, 856 | json_string_is_safe([16#0001d120])), 857 | ?assertEqual( 858 | false, 859 | json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8(16#0001d120)))), 860 | ?assertEqual( 861 | [16#0001d120], 862 | xmerl_ucs:from_utf8( 863 | binary_to_list( 864 | decode(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))))), 865 | ?assertEqual( 866 | false, 867 | json_string_is_safe([16#110000])), 868 | ?assertEqual( 869 | false, 870 | json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8([16#110000])))), 871 | %% solidus can be escaped but isn't unsafe by default 872 | ?assertEqual( 873 | <<"/">>, 874 | decode(<<"\"\\/\"">>)), 875 | ok. 876 | 877 | int_test() -> 878 | ?assertEqual(0, decode("0")), 879 | ?assertEqual(1, decode("1")), 880 | ?assertEqual(11, decode("11")), 881 | ok. 882 | 883 | large_int_test() -> 884 | ?assertEqual(<<"-2147483649214748364921474836492147483649">>, 885 | iolist_to_binary(encode(-2147483649214748364921474836492147483649))), 886 | ?assertEqual(<<"2147483649214748364921474836492147483649">>, 887 | iolist_to_binary(encode(2147483649214748364921474836492147483649))), 888 | ok. 889 | 890 | float_test() -> 891 | ?assertEqual(<<"-2147483649.0">>, iolist_to_binary(encode(-2147483649.0))), 892 | ?assertEqual(<<"2147483648.0">>, iolist_to_binary(encode(2147483648.0))), 893 | ok. 894 | 895 | handler_test() -> 896 | ?assertEqual( 897 | {'EXIT',{json_encode,{bad_term,{x,y}}}}, 898 | catch encode({x,y})), 899 | F = fun ({x,y}) -> [] end, 900 | ?assertEqual( 901 | <<"[]">>, 902 | iolist_to_binary((encoder([{handler, F}]))({x, y}))), 903 | ok. 904 | 905 | encode_empty_test_() -> 906 | [{A, ?_assertEqual(<<"{}">>, iolist_to_binary(encode(B)))} 907 | || {A, B} <- [{"eep18 {}", {}}, 908 | {"eep18 {[]}", {[]}}, 909 | {"{struct, []}", {struct, []}}]]. 910 | 911 | encode_test_() -> 912 | P = [{<<"k">>, <<"v">>}], 913 | JSON = iolist_to_binary(encode({struct, P})), 914 | [{atom_to_list(F), 915 | ?_assertEqual(JSON, iolist_to_binary(encode(decode(JSON, [{format, F}]))))} 916 | || F <- [struct, eep18, proplist]]. 917 | 918 | format_test_() -> 919 | P = [{<<"k">>, <<"v">>}], 920 | JSON = iolist_to_binary(encode({struct, P})), 921 | [{atom_to_list(F), 922 | ?_assertEqual(A, decode(JSON, [{format, F}]))} 923 | || {F, A} <- [{struct, {struct, P}}, 924 | {eep18, {P}}, 925 | {proplist, P}]]. 926 | 927 | -endif. 928 | --------------------------------------------------------------------------------