This commit is contained in:
Matt Basta 2013-12-19 16:39:02 -08:00
Родитель 6e8677e464
Коммит cab3057812
8 изменённых файлов: 466 добавлений и 117 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -17,3 +17,4 @@ node_modules
settings_local.js
data/
dump.rdb

27
app.js
Просмотреть файл

@ -37,7 +37,7 @@ wss.on('connection', function(ws) {
var authenticated = false;
var subscribed = false;
var user = new user.User(clientData);
var user_ = new user.User(clientData);
function send(data) {
return ws.send(JSON.stringify(data));
@ -53,8 +53,8 @@ wss.on('connection', function(ws) {
return;
}
console.log('auth', user.authenticated);
if (!user.authenticated) {
console.log('auth', user_.authenticated);
if (!user_.authenticated) {
if (message.type !== 'auth') {
// Ignore non-auth requests from the client until
// authentication has taken place.
@ -66,12 +66,12 @@ wss.on('connection', function(ws) {
send({type: 'error', error: 'bad_token'});
return;
}
user.authenticate(result, function(err) {
user_.authenticate(result, function(err) {
if (err) {
send({type: 'error', error: err});
} else {
clientPub.subscribe('user:' + user.get('id'));
send({type: 'authenticated', id: user.get('id')});
clientPub.subscribe('user:' + user_.get('id'));
send({type: 'authenticated', id: user_.get('id')});
// TODO: broadcast to friends that user is online.
}
});
@ -80,17 +80,17 @@ wss.on('connection', function(ws) {
console.log('message', message)
switch (message.type) {
case 'playing':
user.startPlaying(message.game, function(err) {
user_.startPlaying(message.game, function(err) {
if (!err) return;
send({type: 'error', error: err});
});
// TODO: broadcast this to friends.
break;
case 'notPlaying':
user.donePlaying();
user_.donePlaying();
break;
case 'score':
user.updateLeaderboard(message.board, message.value, function(err) {
user_.updateLeaderboard(message.board, message.value, function(err) {
if (!err) return;
send({type: 'error', error: err});
});
@ -100,19 +100,16 @@ wss.on('connection', function(ws) {
ws.on('close', function() {
console.log('close');
intervals.forEach(function(v) {
clearInterval(v);
});
if (user.authenticated && subscribed) {
if (user_.authenticated && subscribed) {
clientPub.unsubscribe();
}
user.finish();
user_.finish();
clientPub.end();
clientData.end();
});
clientPub.on('message', function(channel, message) {
if (!user.authenticated) {
if (!user_.authenticated) {
return;
}
ws.send(message);

Просмотреть файл

@ -1,90 +1,2 @@
<ul id="pings"></ul>
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script>
function getVars(qs, excl_undefined) {
if (!qs) qs = location.search;
if (!qs || qs === '?') return {};
if (qs[0] == '?') {
qs = qs.substr(1); // Filter off the leading ? if it's there.
}
return _.chain(qs.split('&')) // ['a=b', 'c=d']
.map(function(c) {return c.split('=').map(decodeURIComponent);}) // [['a', 'b'], ['c', 'd']]
.filter(function(p) { // [['a', 'b'], ['c', undefined]] -> [['a', 'b']]
return !!p[0] && (!excl_undefined || !_.isUndefined(p[1]));
}).object() // {'a': 'b', 'c': 'd'}
.value();
}
var host = 'ws://' + window.location.host;
host = 'ws://localhost:5000';
var GET = getVars();
var game = GET.game;
var user = GET.user;
console.log(game, user);
var timerID;
var intervals = [];
function WS() {
var ws = new WebSocket(host);
ws.onmessage = function(event) {
var li = document.createElement('li');
li.innerHTML = JSON.parse(event.data);
document.querySelector('#pings').appendChild(li);
};
ws.onerror = function(error) {
console.error('[websocket] Error:', error.toString());
};
ws.onclose = function() {
console.log('[websocket] open');
intervals.forEach(function(v) {
clearInterval(v);
});
// Fire only one setTimeout at a time.
if (!timerID) {
timerID = setTimeout(function() {
WS();
}, 5000);
}
};
ws.sendd = function(msg) {
console.log(msg);
return ws.send(msg);
};
ws.onopen = function() {
if (timerID) { // A setInterval has been fired
clearInterval(timerID);
timerID = 0;
}
console.log('[websocket] open');
// TODO: Authenticate.
ws.sendd(JSON.stringify({
type: 'auth',
user: user
}));
ws.sendd(JSON.stringify({
type: 'playing',
user: user,
game: game
}));
};
}
WS();
</script>
<html><body><script src="http://localhost:5000/include.js"></script></body></html>

Просмотреть файл

@ -56,7 +56,12 @@ function createSSA(email) {
exports.createSSA = createSSA;
function verifySSA(token) {
var tokenBits = token.split(',', 3);
var tokenBits;
try {
tokenBits = token.split(',', 3);
} catch(e) {
return false;
}
if (tokenBits.length !== 3) return false;
var guid = tokenBits[1];

Просмотреть файл

@ -50,10 +50,13 @@ user.prototype.authenticate = function(email, callback) {
return;
}
if (!userData) {
finishAuth(newUser());
finishAuth(newUser(self.dataChannel, email));
} else {
userData = JSON.parse(userData);
self.dataChannel.hget('authenticated', userData.id, function(err, resp) {
if (userData.id === self.get('id')) {
callback(null);
return;
}
self.dataChannel.sismember('authenticated', userData.id, function(err, resp) {
if (resp) {
callback('already_authenticated');
return;
@ -64,9 +67,9 @@ user.prototype.authenticate = function(email, callback) {
});
function finishAuth(userData) {
self.dataChannel.hset('authenticated', userData.id, 'y');
self.dataChannel.sadd('authenticated', userData.id);
self.set('username', userData.username);
self.set('uid', userData.id);
self.set('id', userData.id);
self.set('email', userData.email);
self.authenticated = true;
callback(null);
@ -182,7 +185,7 @@ user.prototype.updateLeaderboard = function(board, value, callback) {
user.prototype.finish = function() {
if (this.authenticated) {
this.dataChannel.hdel('authenticated', this.get('id'));
this.dataChannel.srem('authenticated', this.get('id'));
this.authenticated = false;
}
this.donePlaying();
@ -251,6 +254,7 @@ function publicUserObj(full) {
id: full.id
};
}
exports.publicUserObj = publicUserObj;
function getPublicUserObj(client, id, callback) {
// `callback` is called with a single parameter, which is either

Просмотреть файл

@ -0,0 +1,180 @@
<html>
<head>
<title>Galaxy</title>
</head>
<body>
<script src="https://login.persona.org/include.js"></script>
<script type="text/javascript">
<!--
var host = 'ws://' + window.location.host;
var ws;
var origin = location.hash.substr(1);
var errors = 0;
function connect() {
ws = new WebSocket(host);
console.log('Starting socket');
ws.onmessage = function(event) {
window.top.postMessage(JSON.parse(event.data), origin);
};
ws.onerror = function(error) {
console.error('[websocket] Error:', error.toString());
};
ws.onclose = function() {
console.log('[websocket] close');
errors++;
if (errors > 10) return;
connect();
};
ws.sendd = function(msg) {
console.log(msg);
return this.send(msg);
};
ws.onopen = function() {
console.log('[websocket] open');
if (ssa) {
ws.send(JSON.stringify({type: 'auth', token: ssa}));
}
ready();
};
}
function xhr(method, url, data, callback) {
// TODO: Make this hardier.
var req = new XMLHttpRequest();
req.onload = function() {
if (Math.floor(req.status / 100) !== 2) {
callback(null);
return;
}
callback(req.responseText);
};
req.onerror = function() {
callback(null);
};
req.open(method, url, true);
if (data) {
req.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
}
req.send(data);
}
var SSATOKEN = '0::user';
var ssa = window.localStorage.getItem(SSATOKEN);
function authenticate(callback, callback_cancel) {
// TODO: Port login.js to work here instead.
if (ssa) return callback(ssa), null;
navigator.id.watch({
onlogin: function(assertion) {
xhr(
'POST',
'/user/login',
'assertion=' + encodeURIComponent(assertion) +
'&audience=' + encodeURIComponent(
window.location.protocol + '//' + window.location.host),
function(resp) {
var data;
try {
data = JSON.parse(resp);
} catch (e) {
callback_cancel();
return;
}
ssa = data.token;
localStorage.setItem(SSATOKEN, data.token);
if (wasReady) {
ws.send(JSON.stringify({type: 'auth', token: data.token}));
}
callback(data);
}
);
},
onlogout: function() {}
});
navigator.id.request({
siteName: 'Mozilla Galaxy',
oncancel: callback_cancel
});
}
function notify(title, body, icon, callback) {
if (window.Notification) {
function donotify() {
var notification = new Notification(title, {
body: body,
icon: icon
});
notification.onclick = callback;
}
if (Notification.permission === 'granted') {
donotify();
} else if (Notification.permission !== 'denied') {
Notification.requestPermission(function(permission) {
if (permission !== 'granted') return;
donotify();
});
}
} else {
var havePermission = window.webkitNotifications.checkPermission();
if (havePermission === 0) {
var notification = window.webkitNotifications.createNotification(
icon || null,
title,
body
);
notification.onclick = callback;
notification.show();
} else {
window.webkitNotifications.requestPermission();
}
}
}
window.addEventListener('message', function(e) {
if (e.origin !== origin) {
return;
}
console.log('host', e);
var data = e.data;
if (data.request) {
ws.send(JSON.stringify(data.request));
return;
} else if (data.require) {
switch(data.require) {
case 'auth':
authenticate(function() {
// Noop. The server sends the official authenticated message.
}, function() {
window.top.postMessage({type: 'error', error: 'could_not_auth'}, origin);
});
}
} else if (data.dispatch) {
var url = data.url;
if (data.signed && authenticated) {
url += (url.indexOf('?') === -1) ? '?' : '&';
url += '_user=' + encodeURIComponent(ssa);
}
xhr(data.method, url, data.data, function(resp) {
window.top.postMessage({type: 'response', response: data.dispatch, data: resp}, origin);
});
}
});
var wasReady = false;
function ready() {
if (wasReady) return;
wasReady = true;
console.log('Galaxy Ready!');
window.top.postMessage('galaxy ready', origin);
}
connect();
-->
</script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,253 @@
;(function(exports) {
var origin = window.location.protocol + '//' + window.location.host;
var slice = function(arr) {return Array.prototype.slice.call(arr, 0)};
var ifr = document.createElement('iframe');
ifr.src = 'http://localhost:5000/host.html#' + origin;
ifr.style.display = 'none';
document.body.appendChild(ifr);
var Deferred = (function() {
var PENDING = 'pending';
var RESOLVED = 'resolved';
var REJECTED = 'rejected';
function defer(beforeStart) {
var _this = this;
var state = PENDING;
var doneCBs = [];
var failCBs = [];
var closedArgs = [];
function execute(funcs, args, ctx) {
for (var i = 0, e; e = funcs[i++];) {
if (Array.isArray(e)) {
execute(e, args, ctx);
} else {
e.apply(ctx || _this, args);
}
}
}
function closer(list, new_state, ctx) {
return function() {
if (state !== PENDING) {
return;
}
state = new_state;
var args = slice(arguments);
execute(list, closedArgs = ctx ? args.slice(1) : args, ctx ? args[0] : _this);
return _this;
};
}
this.resolve = closer(doneCBs, RESOLVED);
this.resolveWith = closer(doneCBs, RESOLVED, true);
this.reject = closer(failCBs, REJECTED);
this.rejectWith = closer(failCBs, REJECTED, true);
this.promise = function(obj) {
obj = obj || {};
function wrap(instant, cblist) {
return function(arglist) {
var args = slice(arguments);
if (state === instant) {
execute(args, closedArgs);
} else if (state === PENDING) {
for (var i = 0, e; e = args[i++];) {
cblist.push(e);
}
}
return obj;
};
}
obj.state = function() {return state;};
obj.done = wrap(RESOLVED, doneCBs);
obj.fail = wrap(REJECTED, failCBs);
obj.then = function(doneFilter, failFilter) {
var def = new defer();
obj.done(function() {
var args = slice(arguments);
def.resolveWith.apply(this, [this].concat(doneFilter ? doneFilter.apply(this, args) : args));
});
obj.fail(function() {
var args = slice(arguments);
def.rejectWith.apply(this, [this].concat(failFilter ? failFilter.apply(this, args) : args));
});
return def.promise();
};
obj.always = function() {
_this.done.apply(_this, arguments).fail.apply(_this, arguments);
return obj;
};
return obj;
};
this.promise(this);
if (beforeStart) {
beforeStart.call(this, this);
}
}
return function(func) {return new defer(func)};
})();
Deferred.when = this.when = function() {
var args = slice(arguments);
if (args.length === 1 && args[0].promise) {
return args[0].promise();
}
var out = [];
var def = this.Deferred();
var count = 0;
for (var i = 0, e; e = args[i];) {
if (!e.promise) {
out[i++] = e;
continue;
}
count++;
(function(i) {
e.fail(def.reject).done(function() {
count--;
out[i] = slice(arguments);
if (!count) {
def.resolve.apply(def, out);
}
});
})(i++);
}
if (!count) {def.resolve.apply(def, out);}
return def.promise();
};
var initializer = Deferred();
var initialized = false;
var waiting = {};
window.addEventListener('message', function(e) {
if (e.origin === origin) return;
console.log('include', e);
// TODO: Put in the real Galaxy API origin.
if (false && e.origin !== 'https://api.galaxy.mozilla.org') {
return;
}
// Setup code.
if (!initialized) {
if (e.data === 'galaxy ready') {
initialized = true;
initializer.resolve();
}
return;
}
var data;
try {
data = JSON.parse(e.data);
} catch (e) {
return;
}
// XHR message bus.
if (data.type === 'authenticated') {
authenticated = true;
auth_def.resolve();
} else if (data.type === 'response') {
if (!(data.response in waiting)) return;
waiting[data.response].forEach(function(cb) {
cb(data.data);
});
delete waiting[data.response];
}
});
function send(data) {
// TODO: Make this point at the Galaxy API origin.
initializer.done(function() {
ifr.contentWindow.postMessage(data, '*');
});
}
function request(opts, callback) {
(waiting[opts.dispatch] = waiting[opts.dispatch] || []).push(callback);
send(opts);
}
var game;
exports.configure = function(this_game) {
if (game) return;
game = this_game;
};
var authenticated = false;
var auth_def = Deferred();
function requireAuth(func) {
return function() {
if (!authenticated) {
send({require: 'auth'});
var self = this;
var args = arguments;
auth_def.done(function() {
func.apply(self, args);
});
return;
}
return func.apply(this, arguments);
};
}
var playing = false;
exports.playing = requireAuth(function() {
if (playing) return;
playing = true;
send({type: 'playing', game: game});
});
exports.donePlaying = requireAuth(function() {
if (!playing) return;
playing = false;
send({type: 'notPlaying'});
});
// TODO: Throttle this method the same as on the server.
exports.updateScore = requireAuth(function(board, increment) {
if (!playing) return;
playing = false;
// Do basic validation that increment is within the right range.
send({type: 'score', game: game, value: increment | 0 || 0}); // NaN trap.
});
exports.authenticate = function() {
if (authenticated) {
return Deferred().resolve().promise();
}
send({require: 'auth'});
return auth_def.promise();
};
exports.getFriends = function() {
if (!authenticated) return [];
var resp = Deferred();
request({
dispatch: 'friends',
url: '/user/friends',
signed: true,
method: 'GET'
}, function(data) {
if (data)
resp.resolve(JSON.parse(data));
else
resp.reject();
});
return resp.promise();
};
function requestPause() {
var ev = document.createEvent('Event');
e.initEvent('requestPause', true, false);
window.dispatchEvent(e);
}
})(navigator.game || (navigator.game = {}));

Просмотреть файл

@ -20,7 +20,7 @@ module.exports = function(server) {
var audience = POST.audience || '';
console.log('Attempting verification:', audience);
auth.verifyPersonaAssertion(
assertion,
audience,
@ -46,11 +46,8 @@ module.exports = function(server) {
res.json({
error: null,
token: auth.createSSA(email),
settings: {
username: resp.username,
email: email,
id: resp.id
},
settings: resp,
public: user.publicUserObj(resp),
permissions: {}
});
client.end();