...
 
Commits (6)
PROJECT = webmu
PROJECT_DESCRIPTION = A web interface to a MU*
PROJECT_VERSION = 0.0.1
PROJECT_VERSION = 1.1.0
ERLC_OPTS += +'{parse_transform, lager_transform}'
DEPS = cowboy jiffy lager
DEPS = cowboy jiffy lager uuid
LOCAL_DEPS = runtime_tools
dep_cowboy_commit = master
......
......@@ -8,7 +8,7 @@
</head>
<body>
<div id="main-column">
<div id="impressum">This is <a href="https://gitlab.com/hlieberman/webmu">WebMU</a>, version 1.0. WebMU is free software (AGPLv3+), originally built for <a href="https://www.xmenrevolution.com">X-Men: rEvolution MUSH</a>.</div>
<div id="impressum">This is <a href="https://gitlab.com/hlieberman/webmu">WebMU</a>, version 1.1. WebMU is free software (AGPLv3+), originally built for <a href="https://www.xmenrevolution.com">X-Men: rEvolution MUSH</a>.</div>
<p>Please wait, connecting...</p>
<div id="chat-container"></div>
</div>
......
......@@ -25,11 +25,15 @@ body {
width: calc(100% - 10px);
}
#error-box {
.error-box {
color: #CC6666;
font-size: 1.2em;
}
.error-box a {
text-decoration: underline;
}
#impressum,
#impressum > a:link,
#impressum > a:visited
......
......@@ -27,15 +27,23 @@ window.webmu = (function() {
var dirtyHistory;
function init() {
// Clear any other error boxes that might be present
var errs = document.getElementsByClassName("error-box");
while (errs.length > 0) {
errs[0].parentNode.removeChild(errs[0]);
}
var proto = identify_protocol();
conn = new WebSocket(proto + window.location.host + "/mu");
conn = new WebSocket(proto + window.location.host + "/mu?" + get_sessionid());
conn.onerror = handle_error;
conn.onmessage = process_message;
conn.onclose = handle_hangup;
conn.onopen = handle_open;
mainbox = document.getElementById("chat-container");
inputbox = document.getElementById("input-box");
inputbox.onkeypress = pose_input;
timer = window.setInterval(send_ping, 60000);
timer = window.setInterval(send_ping, 7500);
timer_flag = false;
var n = Notification.requestPermission();
if (n !== undefined) {
n.then(function(r) {
......@@ -46,11 +54,19 @@ window.webmu = (function() {
function display_error(errmsg) {
var eb = document.createElement("div");
eb.id = "error-box";
eb.className = "error-box";
eb.textContent = errmsg;
mainbox.appendChild(eb);
mainbox.lastChild.scrollIntoView(true);
}
};
function get_sessionid() {
var sess = window.sessionStorage.getItem("sess");
if (sess) {
return "sess=" + encodeURIComponent(sess);
}
return "";
};
function handle_error(err) {
console.log("Error! " + JSON.stringify(err));
......@@ -59,10 +75,21 @@ window.webmu = (function() {
"poly@xmenrevolution.com with the contents of the Javascript console.");
};
function handle_open(evt) {
var a = document.createElement("div");
a.textContent = "Connected!";
mainbox.appendChild(a);
mainbox.lastChild.scrollIntoView(true);
}
function handle_hangup(evt) {
console.log("Hangup: " + JSON.stringify(evt));
window.clearInterval(timer);
display_error("The connection to the server has gone away. Maybe you intended this?");
var eb = document.createElement("div");
eb.className = "error-box";
eb.innerHTML = 'The connection to the server has gone away. <a onclick="window.webmu.init()">Reconnect?</a>';
mainbox.appendChild(eb);
mainbox.lastChild.scrollIntoView(true);
}
function identify_protocol() {
......@@ -89,7 +116,9 @@ window.webmu = (function() {
mainbox.appendChild(a);
mainbox.lastChild.scrollIntoView(true);
} else if (msg.type == "pong") {
return;
timer_flag = false;
} else if (msg.type == "sess") {
window.sessionStorage.setItem("sess", msg.id);
} else {
console.log("Got unknown command: " + msg);
}
......@@ -140,13 +169,33 @@ window.webmu = (function() {
function send_pose(pose) {
var json = JSON.stringify({"pose": pose});
conn.send(json);
try {
conn.send(json);
}
catch (e) {
handle_error(e);
}
}
function send_ping() {
var json = JSON.stringify({"type": "ping"});
conn.send(json);
if (timer_flag) {
handle_hangup("Pong Missed");
}
try {
conn.send(json);
}
catch (e) {
handle_error(e);
}
}
init();
return {
init: init,
};
})();
......@@ -23,12 +23,13 @@
start(_Type, _Args) ->
application:start(cowboy),
TabId = ets:new(sesstable, [set, public, named_table]),
Dispatch = cowboy_router:compile([{'_', [
{"/mu", webmu_handler, []},
{"/mu", webmu_ws_handler, [TabId]},
{"/", cowboy_static, {priv_file, webmu, "static/index.html"}},
{"/assets/[...]", cowboy_static, {priv_dir, webmu, "static"}}
]}]),
{ok, _} = cowboy:start_clear(mu_handler, 20, [{port, get_env(http_port)}], #{
{ok, _} = cowboy:start_clear(mu_handler, [{port, get_env(http_port)}], #{
env => #{dispatch => Dispatch}
}),
webmu_sup:start_link().
......
%% This is part of WebMU.
%%
%% Copyright 2016-2017, Harlan Lieberman-Berg <hlieberman@setec.io>
%%
%% This program is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as published by
%% the Free Software Foundation, either version 3 of the License, or
%% (at your option) any later version.
%%
%% This program is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with this program. If not, see <http://www.gnu.org/licenses/>.
-module(webmu_mu_handler).
-behaviour(gen_server).
-export([start/1]).
%% gen_server.
-export([init/1]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_info/2]).
-export([terminate/2]).
-export([code_change/3]).
-record(state, {
buffer = <<"">> :: iodata(),
id :: iodata(),
sesstable :: ets:tid(),
socket :: gen_tcp:socket(),
tref :: timer:tref(),
wshandler :: pid() | 'no_ws'
}).
start(Args) ->
gen_server:start(?MODULE, Args, []).
%% gen_server.
init([Id, TabId, Ws]) ->
ets:insert(TabId, {Id, self()}),
monitor(process, Ws),
{ok, Sock} = gen_tcp:connect(get_env(mu_host), get_env(mu_port),
[{active, true}, {mode, binary}]),
lager:debug("Connected"),
{ok, #state{id = Id, sesstable = TabId, socket = Sock, wshandler = Ws}}.
handle_call(transfer, {Ws,_Tag}, #state{tref = T, socket = Socket} = State) ->
lager:debug("Transferring to ~p", [Ws]),
_ = timer:cancel(T),
monitor(process, Ws),
inet:setopts(Socket, [{active, true}]),
{reply, ok, State#state{wshandler = Ws}};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
handle_cast({pose, Pose}, #state{socket = Socket} = State) ->
gen_tcp:send(Socket, [Pose,"\r\n"]),
{noreply, State};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info({'DOWN', _Ref, process, _Pid, _Reason}, #state{socket = Socket} = State) ->
lager:debug("WS link died"),
inet:setopts(Socket, [{active, false}]),
{ok, TRef} = timer:send_after(300000, sess_timeout), % 300000 ms = 5 minutes
{noreply, State#state{tref = TRef, wshandler = no_ws}};
handle_info({tcp_closed, _Socket}, State) ->
lager:debug("MU connection died."),
{stop, normal, State};
handle_info(sess_timeout, State) ->
lager:debug("Timed out waiting for a reconnect."),
{stop, normal, State};
handle_info({tcp, _Socket, <<255, 253, 34>>}, State) ->
%% Ignore telnet handshake
{noreply, State};
handle_info({tcp, _Socket, Data}, State) ->
handle_mu_message(Data, binary:last(Data), State);
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, #state{sesstable = TabId, id = Id}) ->
ets:delete(TabId, Id),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% Private Functions
handle_mu_message(Data, 10, #state{buffer = Buffer, wshandler = Ws} = State) ->
Pose = erlang:iolist_to_binary([Buffer, Data]),
lager:debug("respondingggg with ~p~n to ~p", [Pose, Ws]),
Ws ! {pose, Pose},
{noreply, State#state{buffer = <<"">>}};
handle_mu_message(Data, _Last, #state{buffer = Buffer} = State) ->
lager:debug("No newline =( ~p~n", [Data]),
{noreply, State#state{buffer = [Buffer, Data]}}.
get_env(Param) ->
{ok, Val} = application:get_env(webmu, Param),
Val.
......@@ -25,5 +25,11 @@ start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
Procs = [],
{ok, {{one_for_one, 1, 5}, Procs}}.
{ok, {{simple_one_for_one, 100, 5},
[
{mu_handler,
{webmu_mu_handler, start, []},
temporary, 3000, worker, [webmu_mu_handler]
}
]
}}.
%% This is part of WebMU.
%%
%% Copyright 2016, Harlan Lieberman-Berg <hlieberman@setec.io>
%% Copyright 2016-2017, Harlan Lieberman-Berg <hlieberman@setec.io>
%%
%% This program is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as published by
......@@ -15,7 +15,7 @@
%% You should have received a copy of the GNU Affero General Public License
%% along with this program. If not, see <http://www.gnu.org/licenses/>.
-module(webmu_handler).
-module(webmu_ws_handler).
-export([init/2]).
-export([websocket_init/1]).
......@@ -24,29 +24,32 @@
-export([websocket_terminate/2]).
-record(state, {
socket :: port(),
buffer = <<"">> :: iodata()
id :: iodata(),
sesstable :: ets:tid(),
muhandler :: pid(),
ping_flag = false :: boolean()
}).
init(Req, _Opts) ->
% Time the connection out after 300,000 seconds (5 min)
{cowboy_websocket, Req, #state{}, 300000}.
init(Req, [TabId]) ->
% Time the connection out after 60,000 milliseconds (1 min)
#{sess := Id} = cowboy_req:match_qs([{sess, [], undefined}], Req),
{cowboy_websocket, Req, #state{id = Id, sesstable = TabId}, #{idle_timeout => 60000}}.
websocket_init(State) ->
websocket_init(#state{id = Id, sesstable = TabId} = State) ->
lager:debug("init"),
{ok, Socket} = gen_tcp:connect(get_env(mu_host), get_env(mu_port),
[{active, true}, {mode, binary}]),
lager:debug("connected"),
{ok, State#state{socket = Socket}}.
{ok, Sess} = check_session(Id, TabId),
monitor(process, Sess),
timer:send_after(10000, ping_check),
{ok, State#state{muhandler = Sess}}.
websocket_handle({text, Data}, #state{socket = Socket} = State) ->
websocket_handle({text, Data}, #state{muhandler = Mu} = State) ->
lager:debug("ooh, data ~p~n", [Data]),
case catch json_decode(Data) of
#{<<"pose">> := Msg} ->
gen_tcp:send(Socket, [Msg,"\r\n"]),
gen_server:cast(Mu, {pose, Msg}),
{ok, State};
#{<<"type">> := <<"ping">>} ->
{reply, {text, <<"{\"type\"\:\"pong\"}">>}, State};
{reply, {text, <<"{\"type\"\:\"pong\"}">>}, State#state{ping_flag = false}};
_ ->
{ok, State}
end;
......@@ -54,39 +57,53 @@ websocket_handle(Frame, State) ->
lager:debug("what ~p~n", [Frame]),
{ok, State}.
websocket_info({tcp, _Socket, <<255, 253, 34>>}, State) ->
% Telnet handshake; disgard.
{ok, State};
websocket_info({tcp, _Socket, Data}, State) ->
handle_message(Data, binary:last(Data), State);
websocket_info({tcp_closed, _Socket}, State) ->
lager:debug("Ahhhh, motherland!"),
websocket_info({pose, Pose}, State) ->
lager:debug("Got pose!"),
JSON = jiffy:encode(#{<<"pose">> => Pose}),
{reply, {text, JSON}, State};
websocket_info({sessid, Id}, State) ->
JSON = jiffy:encode(#{<<"type">> => <<"sess">>, <<"id">> => Id}),
{reply, {text, JSON}, State#state{id = Id}};
websocket_info(ping_check, #state{ping_flag = Ping} = State) ->
case Ping of
true ->
{stop, State};
false ->
{ok, State#state{ping_flag = true}}
end;
websocket_info({'DOWN', _, process, _From, _Reason}, State) ->
{stop, State};
websocket_info(Info, State) ->
lager:debug("aaaaa ~p~n", [Info]),
{ok, State}.
websocket_terminate(_Reason, #state{socket = Socket}) ->
lager:debug("ded"),
gen_tcp:close(Socket),
websocket_terminate(_Reason, _State) ->
ok.
%%
%% Private Functions
%%
% binary:last returns an integer. \n is 10.
handle_message(Data, 10, #state{buffer = Buffer} = State) ->
JSON = jiffy:encode(#{<<"pose">> => erlang:iolist_to_binary([Buffer, Data])}),
lager:debug("respondingggg with ~p~n", [JSON]),
{reply, {text, JSON}, State#state{buffer = <<"">>}};
handle_message(Data, _Last, #state{buffer = Buffer} = State) ->
lager:debug("No newline =( ~p~n", [Data]),
{ok, State#state{buffer = [Buffer, Data]}}.
check_session(undefined, TabId) ->
create_session(TabId);
check_session(Id, TabId) ->
%% The client is trying to reconnect. Check if we have a live session.
lager:debug("Looking up session ~p", [Id]),
case ets:lookup(TabId, Id) of
[] ->
lager:debug("Dead session. =("),
create_session(TabId);
[{_Id, Pid}] ->
lager:debug("Got a live one!"),
ok = gen_server:call(Pid, transfer),
{ok, Pid}
end.
get_env(Param) ->
{ok, Val} = application:get_env(webmu, Param),
Val.
create_session(TabId) ->
quickrand:seed(),
Id = list_to_binary(uuid:uuid_to_string(uuid:get_v4_urandom())),
self() ! {sessid, Id},
supervisor:start_child(webmu_sup, [[Id, TabId, self()]]).
json_decode(JSON) ->
jiffy:decode(JSON, [return_maps]).