├── README.markdown ├── body.html ├── css └── lobby.css ├── header.html ├── index.js ├── js └── lobby.js └── lobby.js /README.markdown: -------------------------------------------------------------------------------- 1 | nodeFacebookExample 2 | =================== 3 | 4 | Simple Facebook App example using [nodejs](http://nodejs.org/) and [socket.io](http://socket.io/). 5 | 6 | You can send comments, patches, questions [here on github](https://github.com/ErikDubbelboer/nodeFacebookExample/issues) or to erik@dubbelboer.com. 7 | 8 | 9 | Usage 10 | ===== 11 | 12 | Don't forget to replace the following strings: 13 | 14 | * YOUR APP SECRET HERE 15 | * YOUR APPLICATION ID HERE 16 | * SOME RANDOM KEY HERE 17 | -------------------------------------------------------------------------------- /body.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Node Facebooke Example

5 | 6 |

Users

7 |
8 |
9 | Click on a name to poke that user. 10 | 11 |

Pokes

12 |
13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /css/lobby.css: -------------------------------------------------------------------------------- 1 | 2 | #users div { 3 | margin: 0.5em; 4 | width: 16em; 5 | background-color: #eee; 6 | cursor: pointer; 7 | } 8 | 9 | .info { 10 | color: #888; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Node Facebook Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | https = require('https'), 3 | fs = require('fs'), 4 | querystring = require('querystring'), 5 | crypto = require('crypto'); 6 | 7 | 8 | 9 | // Simple function to decode a base64url encoded string. 10 | function base64_url_decode(data) { 11 | return new Buffer(data.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('ascii'); 12 | } 13 | 14 | 15 | // Wait for and parse POST data 16 | function parse_post(req, callback) { 17 | // Pushing things into an array and joining them in the end is faster then concatenating strings. 18 | var data = []; 19 | 20 | req.addListener('data', function(chunk) { 21 | data.push(chunk); 22 | }); 23 | 24 | req.addListener('end', function() { 25 | callback(querystring.parse(data.join(''))); 26 | }); 27 | } 28 | 29 | 30 | 31 | // Uncomment this to catch all exceptions so the server doesn't crash. 32 | //process.on('uncaughtException', function (err) { 33 | // console.log(err.stack); 34 | //}); 35 | 36 | 37 | 38 | // Make sure we are in the correct working directory, otherwise fs.readFile('header.html' will fail. 39 | if (process.cwd() != __dirname) { 40 | process.chdir(__dirname); 41 | } 42 | 43 | 44 | 45 | var contentTypes = { 46 | 'js': 'text/javascript', 47 | 'css': 'text/css' 48 | }; 49 | 50 | var validFile = new RegExp('^\/[a-z]+\/[0-9a-z\-]*\.(js|css)$'); 51 | 52 | 53 | 54 | function handleRequest(req, res) { 55 | // Serve .js and .css files (files must be in a subdirectory so users can't access the javascript files for nodejs). 56 | if (validFile.test(req.url)) { 57 | // substr(1) to strip the leading / 58 | fs.readFile(req.url.substr(1), function(err, data) { 59 | if (err) { 60 | res.writeHead(404, {'Content-Type': 'text/plain'}); 61 | res.end('File not found.'); 62 | } else { 63 | var ext = req.url.substr(req.url.lastIndexOf('.') + 1); 64 | 65 | res.writeHead(200, {'Content-Type': contentTypes[ext]}); 66 | res.end(data); 67 | } 68 | }); 69 | 70 | return; 71 | } 72 | 73 | 74 | // Facebook always opens the canvas with a POST 75 | if (req.method != 'POST') { 76 | res.end('Error: No POST'); 77 | return; 78 | } 79 | 80 | // Get the POST data. 81 | parse_post(req, function(data) { 82 | if (!data.signed_request) { 83 | res.end('Error: No signed_request'); 84 | return; 85 | } 86 | 87 | // The data Facebook POSTs to use consists of one variable named signed_request that contains 2 strings concatinated using a . 88 | data = data.signed_request.split('.', 2); 89 | 90 | var facebook = JSON.parse(base64_url_decode(data[1])); // The second string is a base64url encoded json object 91 | 92 | if (!facebook.algorithm || (facebook.algorithm.toUpperCase() != 'HMAC-SHA256')) { 93 | res.end('Error: Unknown algorithm'); 94 | return; 95 | } 96 | 97 | // Make sure the data posted is valid and comes from facebook. 98 | var signature = crypto.createHmac('sha256', 'YOUR APP SECRET HERE').update(data[1]).digest('base64').replace(/\+/g, '-').replace(/\//g, '_').replace('=', ''); 99 | 100 | if (data[0] != signature) { 101 | res.end('Error: Bad signature'); 102 | return; 103 | } 104 | 105 | // Has the user authenticated our application? 106 | if (!facebook.user_id) { 107 | res.writeHead(200, {'Content-Type': 'text/html'}); 108 | 109 | // Redirect the user to a page where he/sh can authenticated our application. 110 | // For this example we only request user_about_me permission. 111 | // See http://developers.facebook.com/docs/reference/api/permissions/ for other types of permissions. 112 | var url = 'http://www.facebook.com/dialog/oauth?client_id=YOUR APPLICATION ID HERE&redirect_uri=http://apps.facebook.com/nodeexample/&scope=user_about_me'; 113 | 114 | res.end('You are being redirected to '+url+''); 115 | } else { 116 | res.writeHead(200, {'Content-Type': 'text/html'}); 117 | 118 | fs.readFile('header.html', function(err, data) { 119 | res.write(data); 120 | 121 | // We are going to write the facebook token for this user to the page so it can be passed to our lobby server. 122 | // Since it is possible to run our lobby server on a different node instance we can't just store it in a global variable here. 123 | // We don't want others to be able to see and use the token so we crypt it 124 | var tokencrypt = crypto.createCipher('des-ecb', 'SOME RANDOM KEY HERE'); 125 | 126 | // base64 encoding should be smaller but old nodejs versions bug with this (see https://github.com/joyent/node/commit/e357acc55b8126e1b8b78edcf4ac09dfa3217146) 127 | var token = tokencrypt.update(facebook.oauth_token, 'ascii', 'hex')+tokencrypt.final('hex'); 128 | 129 | res.write(''); 130 | 131 | fs.readFile('body.html', function(err, data) { 132 | res.end(data); 133 | }); 134 | }); 135 | } 136 | }); 137 | } 138 | 139 | 140 | 141 | // For information on how to get these files see: https://tootallnate.net/setting-up-free-ssl-on-your-node-server 142 | // If you don't want https you can comment out the following 7 lines. 143 | var httpsOptions = { 144 | 'ca': fs.readFileSync('../ssl/geotrust.pem'), 145 | 'key': fs.readFileSync('../ssl/server.key'), 146 | 'cert': fs.readFileSync('../ssl/dubbelboer.com.crt') 147 | }; 148 | var httpsServer = https.createServer(httpsOptions, handleRequest); 149 | httpsServer.listen(8083); 150 | 151 | 152 | var httpServer = http.createServer(handleRequest); 153 | httpServer.listen(8081); 154 | 155 | 156 | // In this example we run the lobby server on the same node instance using the same port. 157 | // In theory we could also run this on a seperate node instance. 158 | var lobby = require('./lobby'); 159 | 160 | lobby.start(httpServer); 161 | 162 | 163 | // Never let something run as root when it's not needed! 164 | if (process.getuid() == 0) { 165 | process.setgid('www-data'); 166 | process.setuid('www-data'); 167 | } 168 | 169 | -------------------------------------------------------------------------------- /js/lobby.js: -------------------------------------------------------------------------------- 1 | 2 | $(document).ready(function() { 3 | var socket = io.connect('http://dubbelboer.com:8081'); // It is possible to run the lobby server on a different node instance on a different port. 4 | 5 | socket.on('connect', function() { 6 | // Send our information to the lobby server. 7 | // nfe is injected in the header by index.js. 8 | socket.send(JSON.stringify({ 9 | 'type' : 'connect', 10 | 'id' : nfe.facebookid, 11 | 'token': nfe.facebooktoken 12 | })); 13 | }); 14 | 15 | socket.on('message', function(data) { 16 | data = JSON.parse(data); 17 | 18 | switch (data.type) { 19 | // A new user just came online. 20 | case 'newuser': { 21 | var div = $('
'+data.name+'
').click(function() { 22 | // Send a poke message to the server when the user clicks this div. 23 | socket.send(JSON.stringify({ 24 | 'type': 'poke', 25 | 'to' : data.id 26 | })); 27 | }); 28 | 29 | $('#users').append(div); 30 | 31 | break; 32 | } 33 | 34 | case 'olduser': { 35 | // Remove the div when a user goes offline. 36 | $('#user'+data.id).remove(); 37 | 38 | break; 39 | } 40 | 41 | case 'poke': { 42 | // We just got poked. 43 | $('#pokes').prepend('
'+data.from+' poked you!
'); 44 | 45 | break; 46 | } 47 | 48 | default: { 49 | console.log('Unknown message type:'); 50 | console.dir(data); 51 | 52 | break; 53 | } 54 | } 55 | }); 56 | 57 | socket.on('disconnect', function() { 58 | // Clear the users list when we go offline. 59 | $('#users').empty(); 60 | }); 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /lobby.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('request'), 3 | io = require('socket.io'), 4 | crypto = require('crypto'); 5 | 6 | 7 | 8 | // Global users array (will contain all connected users). 9 | var users = {}; 10 | var nextuser = 1; 11 | 12 | 13 | 14 | exports.start = function(server) { 15 | // If we would run this on it's own node instance this would be io.listen(); 16 | var socket = io.listen(server); 17 | 18 | // Set socket.io logging to normal (comment to get verbose logging). 19 | socket.set('log level', 1); 20 | 21 | socket.sockets.on('connection', function(client) { 22 | // Store the userid in the scope of this connection so we can use it later on. 23 | // Don't use the facebook userid for things since some people might not like it when their id is visible for others. 24 | var userid = nextuser++; 25 | 26 | client.on('message', function(data) { 27 | data = JSON.parse(data); 28 | 29 | // Some more error handling needs to be done here. Malformed requests (missing data.type for example) would be able to crash this part of the server. 30 | 31 | switch (data.type) { 32 | case 'connect': { 33 | // Decrypt the facebook user token. 34 | // Make sure to use the exact same key as in index.js! 35 | var tokencrypt = crypto.createDecipher('des-ecb', 'SOME RANDOM KEY HERE'); 36 | var token = tokencrypt.update(data.token, 'hex', 'ascii')+tokencrypt.final('ascii'); 37 | 38 | // Send a newuser message for all existing users to the just connected user. 39 | for (id in users) { 40 | client.send(JSON.stringify({ 41 | 'type': 'newuser', 42 | 'id' : id, 43 | 'name': users[id].name 44 | })); 45 | } 46 | 47 | // Use the facebook graph api to request the users information. 48 | request({uri: 'https://graph.facebook.com/'+data.id+'?access_token='+token}, function(error, response, body) { 49 | var facebook = JSON.parse(body); 50 | var name = facebook.name || 'Unknown'; 51 | 52 | console.log(name+' connected'); 53 | 54 | // Store the user information (could store more information from the facebook object). 55 | users[userid] = { 56 | 'client' : client, 57 | 'facebooktoken': token, 58 | 'facebookid' : data.id, 59 | 'name' : name 60 | }; 61 | 62 | socket.sockets.send(JSON.stringify({ 63 | 'type': 'newuser', 64 | 'id' : userid, 65 | 'name': name 66 | })); 67 | 68 | for (var uid in users) { 69 | if (uid == userid) { 70 | continue; 71 | } 72 | 73 | client.json.send({ 74 | 'type': 'newuser', 75 | 'id' : uid, 76 | 'name': users[uid].name 77 | }); 78 | } 79 | }); 80 | 81 | break; 82 | } 83 | 84 | case 'poke': { 85 | // We recieved a poke request, send it to the correct user. 86 | if (users[data.to]) { 87 | users[data.to].client.send(JSON.stringify({ 88 | 'type': 'poke', 89 | 'from': users[userid].name 90 | })); 91 | } 92 | 93 | break; 94 | } 95 | 96 | default: { 97 | console.log('Unknown message type:'); 98 | console.dir(data); 99 | 100 | break; 101 | } 102 | } 103 | }); 104 | 105 | client.on('disconnect', function() { 106 | if (users[userid]) { 107 | socket.sockets.send(JSON.stringify({ 108 | 'type': 'olduser', 109 | 'id' : userid 110 | })); 111 | 112 | delete users[userid]; 113 | } 114 | }); 115 | }); 116 | }; 117 | 118 | --------------------------------------------------------------------------------