├── TODO.md ├── HISTORY.md ├── package.json ├── test ├── test_key.pem ├── test_cert.pem └── tests.js ├── example └── connect.js ├── docs └── api.md ├── readme.md └── lib └── facebook.js /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | * test the new { agent: false } thing. 3 | * two tests are now failing: 4 | rewrite tests for constantly shifting Facebook api 5 | (consult original php-sdk unit tests) 6 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 0.3.2 / 2011-05-02 2 | ------------------ 3 | 4 | * setting the { agent: false } on https requests to Facebook (experimental) 5 | 6 | 0.3.1 / 2011-04-19 7 | ------------------ 8 | 9 | * added Jozef Dransfield's patch parsing of signed requests coming from http post 10 | 11 | 0.3.0 / 2011-04-19 12 | ------------------ 13 | 14 | * connect body parser support 15 | * cookies are now read from req.cookies, and should be parsed using connect middleware 16 | * siteUrl option has been removed 17 | 18 | < 0.3.0 / 2011-04-18 19 | -------------------- 20 | 21 | Straight up porting the original php-sdk 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "facebook-sdk", 3 | "description": "!!!Unmaintained!!! Consider using fb", 4 | "keywords": [ 5 | "facebook", 6 | "sdk", 7 | "graph", 8 | "api", 9 | "connect", 10 | "canvas" 11 | ], 12 | "version": "0.3.3", 13 | "author": "Christopher Johnson (http://github.com/tenorviol)", 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/tenorviol/node-facebook-sdk.git" 17 | }, 18 | "dependencies": { 19 | "connect": "1.4.1", 20 | "npm": "^5.8.0" 21 | }, 22 | "directories": { 23 | "lib": "./lib" 24 | }, 25 | "licenses": [ 26 | { 27 | "type": "Apache License, Version 2.0", 28 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 29 | } 30 | ], 31 | "main": "lib/facebook", 32 | "engines": { 33 | "node": ">= 0.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/test_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQDx3wdzpq2rvwm3Ucun1qAD/ClB+wW+RhR1nVix286QvaNqePAd 3 | CAwwLL82NqXcVQRbQ4s95splQnwvjgkFdKVXFTjPKKJI5aV3wSRN61EBVPdYpCre 4 | 535yfG/uDysZFCnVQdnCZ1tnXAR8BirxCNjHqbVyIyBGjsNoNCEPb2R35QIDAQAB 5 | AoGBAJNem9C4ftrFNGtQ2DB0Udz7uDuucepkErUy4MbFsc947GfENjDKJXr42Kx0 6 | kYx09ImS1vUpeKpH3xiuhwqe7tm4FsCBg4TYqQle14oxxm7TNeBwwGC3OB7hiokb 7 | aAjbPZ1hAuNs6ms3Ybvvj6Lmxzx42m8O5DXCG2/f+KMvaNUhAkEA/ekrOsWkNoW9 8 | 2n3m+msdVuxeek4B87EoTOtzCXb1dybIZUVv4J48VAiM43hhZHWZck2boD/hhwjC 9 | M5NWd4oY6QJBAPPcgBVNdNZSZ8hR4ogI4nzwWrQhl9MRbqqtfOn2TK/tjMv10ALg 10 | lPmn3SaPSNRPKD2hoLbFuHFERlcS79pbCZ0CQQChX3PuIna/gDitiJ8oQLOg7xEM 11 | wk9TRiDK4kl2lnhjhe6PDpaQN4E4F0cTuwqLAoLHtrNWIcOAQvzKMrYdu1MhAkBm 12 | Et3qDMnjDAs05lGT72QeN90/mPAcASf5eTTYGahv21cb6IBxM+AnwAPpqAAsHhYR 13 | 9h13Y7uYbaOjvuF23LRhAkBoI9eaSMn+l81WXOVUHnzh3ZwB4GuTyxMXXNOhuiFd 14 | 0z4LKAMh99Z4xQmqSoEkXsfM4KPpfhYjF/bwIcP5gOei 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /example/connect.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect'), 2 | fbsdk = require('facebook-sdk'); 3 | 4 | var port = 3000; 5 | 6 | connect() 7 | .use(connect.favicon()) 8 | .use(connect.cookieParser()) 9 | .use(connect.bodyParser()) 10 | .use(fbsdk.facebook({ 11 | appId : 'YOUR APP ID', 12 | secret : 'YOUR APP SECRET' 13 | })) 14 | .use(function(req, res, next) { 15 | 16 | if (req.facebook.getSession()) { 17 | 18 | // get my graph api information 19 | req.facebook.api('/me', function(me) { 20 | console.log(me); 21 | 22 | if (me.error) { 23 | res.end('An api error occured, so probably you logged out. Refresh to try it again...'); 24 | } else { 25 | res.end('Logout'); 26 | } 27 | }); 28 | 29 | } else { 30 | res.end('Login'); 31 | } 32 | 33 | }) 34 | .listen(port); 35 | 36 | console.log('Listening for http requests on port ' + port); 37 | -------------------------------------------------------------------------------- /test/test_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDXDCCAsWgAwIBAgIJAKL0UG+mRkSPMA0GCSqGSIb3DQEBBQUAMH0xCzAJBgNV 3 | BAYTAlVLMRQwEgYDVQQIEwtBY2tuYWNrIEx0ZDETMBEGA1UEBxMKUmh5cyBKb25l 4 | czEQMA4GA1UEChMHbm9kZS5qczEdMBsGA1UECxMUVGVzdCBUTFMgQ2VydGlmaWNh 5 | dGUxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0wOTExMTEwOTUyMjJaFw0yOTExMDYw 6 | OTUyMjJaMH0xCzAJBgNVBAYTAlVLMRQwEgYDVQQIEwtBY2tuYWNrIEx0ZDETMBEG 7 | A1UEBxMKUmh5cyBKb25lczEQMA4GA1UEChMHbm9kZS5qczEdMBsGA1UECxMUVGVz 8 | dCBUTFMgQ2VydGlmaWNhdGUxEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG 9 | 9w0BAQEFAAOBjQAwgYkCgYEA8d8Hc6atq78Jt1HLp9agA/wpQfsFvkYUdZ1YsdvO 10 | kL2janjwHQgMMCy/Njal3FUEW0OLPebKZUJ8L44JBXSlVxU4zyiiSOWld8EkTetR 11 | AVT3WKQq3ud+cnxv7g8rGRQp1UHZwmdbZ1wEfAYq8QjYx6m1ciMgRo7DaDQhD29k 12 | d+UCAwEAAaOB4zCB4DAdBgNVHQ4EFgQUL9miTJn+HKNuTmx/oMWlZP9cd4QwgbAG 13 | A1UdIwSBqDCBpYAUL9miTJn+HKNuTmx/oMWlZP9cd4ShgYGkfzB9MQswCQYDVQQG 14 | EwJVSzEUMBIGA1UECBMLQWNrbmFjayBMdGQxEzARBgNVBAcTClJoeXMgSm9uZXMx 15 | EDAOBgNVBAoTB25vZGUuanMxHTAbBgNVBAsTFFRlc3QgVExTIENlcnRpZmljYXRl 16 | MRIwEAYDVQQDEwlsb2NhbGhvc3SCCQCi9FBvpkZEjzAMBgNVHRMEBTADAQH/MA0G 17 | CSqGSIb3DQEBBQUAA4GBADRXXA2xSUK5W1i3oLYWW6NEDVWkTQ9RveplyeS9MOkP 18 | e7yPcpz0+O0ZDDrxR9chAiZ7fmdBBX1Tr+pIuCrG/Ud49SBqeS5aMJGVwiSd7o1n 19 | dhU2Sz3Q60DwJEL1VenQHiVYlWWtqXBThe9ggqRPnCfsCRTP8qifKkjk45zWPcpN 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | node.js facebook-sdk API 2 | ======================== 3 | 4 | facebook.api 5 | ------------ 6 | 7 | ### Setup 8 | 9 | var fbsdk = require('facebook-sdk'); 10 | 11 | A Facebook object with no specific user information, 12 | the following can be used to perform application api 13 | calls such as obtaining [Insites data](https://developers.facebook.com/docs/insights/). 14 | 15 | var facebook = new fbsdk.Facebook({ 16 | appId: 'YOUR APP ID', 17 | secret:'YOUR APP SECRET' 18 | }); 19 | 20 | The Facebook object can take the request/response parameters from an http request, 21 | allowing for more specific graph api interactions regarding the requesting user. 22 | 23 | http.createServer(function (req, res) { 24 | var facebook = new fbsdk.Facebook({ 25 | appId: 'YOUR APP ID', 26 | secret:'YOUR APP SECRET', 27 | request: req, 28 | response:res 29 | }) 30 | }); 31 | 32 | This can be accomplished more easily by using connect middleware. The body and 33 | cookie parsers provide a more efficient/robust interaction with the http headers. 34 | 35 | connect() 36 | .use(connect.bodyParser()) 37 | .use(connect.cookieParser()) 38 | .use(fbsdk.facebook({ 39 | appId: 'YOUR APP ID', 40 | secret:'YOUR APP SECRET', 41 | })) 42 | 43 | ### Simple api call 44 | 45 | facebook.api('/me', function(me) { 46 | console.log(me); 47 | }); 48 | 49 | ### FQL api call 50 | 51 | facebook.api({ 52 | method: 'fql.query', 53 | query: "SELECT name FROM user WHERE uid = me()" 54 | }, function(data) { 55 | console.log(data); 56 | }); 57 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ** This library is unmaintained. Consider using [Facebook's client-side JS library](https://developers.facebook.com/docs/javascript) or a more recent [client side library](https://www.npmjs.com/package/fb). ** 2 | 3 | [node.js Facebook SDK](https://github.com/tenorviol/node-facebook-sdk) 4 | ====================== 5 | 6 | This is a complete port of Facebook's [PHP SDK library](http://github.com/facebook/php-sdk). 7 | 8 | > The [Facebook Platform](http://developers.facebook.com/) is 9 | > a set of APIs that make your application more social. Read more about 10 | > [integrating Facebook with your web site](http://developers.facebook.com/docs/guides/web) 11 | > on the Facebook developer site. 12 | 13 | The node.js Facebook SDK is licensed under the 14 | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html), 15 | as was the original library. 16 | 17 | Install 18 | ------- 19 | 20 | npm install facebook-sdk 21 | 22 | Use as connect middleware 23 | ------------------------- 24 | 25 | The following will attach a new Facebook object to each incoming http request. 26 | For more information on querying Facebook's graph api, see 27 | [developers.facebook.com](http://developers.facebook.com/docs/reference/api/). 28 | 29 | var connect = require('connect'), 30 | fbsdk = require('facebook-sdk'); 31 | 32 | connect() 33 | .use(connect.cookieParser()) 34 | .use(connect.bodyParser()) 35 | .use(fbsdk.facebook({ 36 | appId : 'YOUR APP ID', 37 | secret : 'YOUR API SECRET' 38 | })) 39 | .use(function(req, res, next) { 40 | 41 | if (req.facebook.getSession()) { 42 | res.end('Logout'); 43 | 44 | // get my graph api information 45 | req.facebook.api('/me', function(me) { 46 | console.log(me); 47 | }); 48 | 49 | } else { 50 | res.end('Login'); 51 | } 52 | 53 | }) 54 | .listen(3000); 55 | 56 | Stand alone usage 57 | ----------------- 58 | 59 | var fbsdk = require('facebook-sdk'); 60 | 61 | var facebook = new fbsdk.Facebook({ 62 | appId : 'YOUR APP ID', 63 | secret : 'YOUR API SECRET' 64 | }); 65 | 66 | facebook.api('/YOUR APP ID', function(data) { 67 | console.log(data); 68 | }); 69 | 70 | Tests 71 | ----- 72 | 73 | The tests have been ported to run using nodeunit. This was the easiest way to confirm 74 | the new node.js library works as expected. Some new tests have been added to cover 75 | edge cases, and others not relevant in the new environment have been removed. 76 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2011 Facebook, Inc. 3 | * Copyright 2011 Christopher Johnson 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | var Facebook = require('../lib/facebook').Facebook, 19 | fs = require('fs'), 20 | http = require('http'), 21 | https = require('https'), 22 | querystring = require('querystring'), 23 | connect = require('connect'); 24 | 25 | var APP_ID = '117743971608120'; 26 | var SECRET = '943716006e74d9b9283d4d5d8ab93204'; 27 | 28 | var MIGRATED_APP_ID = '174236045938435'; 29 | var MIGRATED_SECRET = '0073dce2d95c4a5c2922d1827ea0cca6'; 30 | 31 | var VALID_EXPIRED_SESSION = { 32 | access_token : '117743971608120|2.vdCKd4ZIEJlHwwtrkilgKQ__.86400.1281049200-1677846385|NF_2DDNxFBznj2CuwiwabHhTAHc.', 33 | expires : '1281049200', 34 | secret : 'u0QiRGAwaPCyQ7JE_hiz1w__', 35 | session_key : '2.vdCKd4ZIEJlHwwtrkilgKQ__.86400.1281049200-1677846385', 36 | sig : '7a9b063de0bef334637832166948dcad', 37 | uid : '1677846385' 38 | }; 39 | 40 | // cookie copied from testSetSession 41 | var SESSION_COOKIE = 'fbs_117743971608120=%22access_token%3D117743971608120%257C2.vdCKd4ZIEJlHwwtrkilgKQ__.86400.1281049200-1677846385%257CNF_2DDNxFBznj2CuwiwabHhTAHc.%26expires%3D1281049200%26secret%3Du0QiRGAwaPCyQ7JE_hiz1w__%26session_key%3D2.vdCKd4ZIEJlHwwtrkilgKQ__.86400.1281049200-1677846385%26sig%3D7a9b063de0bef334637832166948dcad%26uid%3D1677846385%22'; 42 | // cookie copied from Facebook's Javascript SDK running on Safari 43 | var UNESCAPED_SESSION_COOKIE = 'junk=foo; fbs_117743971608120="access_token=117743971608120%7C2.vdCKd4ZIEJlHwwtrkilgKQ__.86400.1281049200-1677846385%7CNF_2DDNxFBznj2CuwiwabHhTAHc.&expires=1281049200&secret=u0QiRGAwaPCyQ7JE_hiz1w__&session_key=2.vdCKd4ZIEJlHwwtrkilgKQ__.86400.1281049200-1677846385&sig=7a9b063de0bef334637832166948dcad&uid=1677846385"'; 44 | 45 | var VALID_SIGNED_REQUEST = '1sxR88U4SW9m6QnSxwCEw_CObqsllXhnpP5j2pxD97c.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOjEyODEwNTI4MDAsIm9hdXRoX3Rva2VuIjoiMTE3NzQzOTcxNjA4MTIwfDIuVlNUUWpub3hYVVNYd1RzcDB1U2g5d19fLjg2NDAwLjEyODEwNTI4MDAtMTY3Nzg0NjM4NXx4NURORHBtcy1nMUM0dUJHQVYzSVdRX2pYV0kuIiwidXNlcl9pZCI6IjE2Nzc4NDYzODUifQ'; 46 | var NON_TOSSED_SIGNED_REQUEST = 'c0Ih6vYvauDwncv0n0pndr0hP0mvZaJPQDPt6Z43O0k.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiJ9'; 47 | 48 | exports.testConstructor = function(test) { 49 | var facebook = new Facebook({ 50 | appId : APP_ID, 51 | secret : SECRET, 52 | request: {}, 53 | response: {}, 54 | domain : 'fbrell.com', 55 | fileUpload : true 56 | }); 57 | test.equal(facebook.appId, APP_ID, 'Expect the App ID to be set.'); 58 | test.equal(facebook.secret, SECRET, 'Expect the API secret to be set.'); 59 | test.ok(facebook.request); 60 | test.ok(facebook.response); 61 | test.equal(facebook.domain, 'fbrell.com'); 62 | test.ok(facebook.fileUpload); 63 | test.done(); 64 | }; 65 | 66 | // exports.testIgnoreDeleteSetCookie = function(test) { 67 | // var facebook = new Facebook({ 68 | // appId : APP_ID, 69 | // secret : SECRET, 70 | // cookie : true, 71 | // }); 72 | // cookieName = 'fbs_' . APP_ID; 73 | // test.ok(!isset(_COOKIE[cookieName]), 'Expect Cookie to not exist.'); 74 | // facebook.setSession(null); 75 | // test.ok(!isset(_COOKIE[cookieName]), 'Expect Cookie to not exist.'); 76 | // } 77 | 78 | exports.testSetNullSession = function(test) { 79 | var facebook = new Facebook({ 80 | appId : APP_ID, 81 | secret : SECRET 82 | }); 83 | facebook.setSession(null); 84 | test.ok(facebook.getSession() === null, 'Expect null session back.'); 85 | test.done(); 86 | }; 87 | 88 | exports.testNonUserAccessToken = function(test) { 89 | var facebook = new Facebook({ 90 | appId : APP_ID, 91 | secret : SECRET 92 | }); 93 | test.ok(facebook.getAccessToken() == APP_ID+'|'+SECRET, 'Expect appId|secret.'); 94 | test.done(); 95 | }; 96 | 97 | exports.testSetSession = function(test) { 98 | test.expect(4); 99 | 100 | // the setSession below should call this response.setHeader method 101 | var response = { 102 | setHeader: function(name, value) { 103 | // setting the session sets the cookie (copied from a php-sdk instance) 104 | test.equal(name, 'Set-Cookie'); 105 | test.equal(value, SESSION_COOKIE+'; domain=.foo.com; path=/; expires=Thu, 05 Aug 2010 23:00:00 GMT'); 106 | } 107 | }; 108 | 109 | var facebook = new Facebook({ 110 | appId : APP_ID, 111 | secret : SECRET, 112 | response : response, 113 | domain : 'foo.com' 114 | }); 115 | facebook.setSession(VALID_EXPIRED_SESSION); 116 | test.ok(facebook.getUser() == VALID_EXPIRED_SESSION.uid, 'Expect uid back.'); 117 | test.ok(facebook.getAccessToken() == VALID_EXPIRED_SESSION.access_token, 'Expect access token back.'); 118 | test.done(); 119 | }; 120 | 121 | exports.testGetSession = function(test) { 122 | // regression test: the cookie we set should be getSession-able 123 | var request = { 124 | url: '/', 125 | cookies: connect.utils.parseCookie(SESSION_COOKIE) 126 | }; 127 | var facebook = new Facebook({ 128 | appId : APP_ID, 129 | secret : SECRET, 130 | request: request 131 | }); 132 | test.deepEqual(facebook.getSession(), VALID_EXPIRED_SESSION); 133 | test.done(); 134 | }; 135 | 136 | // regression: this is to test cookies that were set using Facebook's client-side Javascript SDK 137 | exports.testGetSessionUnescaped = function(test) { 138 | // regression test: the cookie we set should be getSession-able 139 | var request = { 140 | url: '/', 141 | cookies: connect.utils.parseCookie(UNESCAPED_SESSION_COOKIE) 142 | }; 143 | var facebook = new Facebook({ 144 | appId : APP_ID, 145 | secret : SECRET, 146 | request: request 147 | }); 148 | test.deepEqual(facebook.getSession(), VALID_EXPIRED_SESSION); 149 | test.done(); 150 | }; 151 | 152 | exports.testGetSessionFromCookie = function(test) { 153 | var cookieName = 'fbs_' + APP_ID; 154 | var session = VALID_EXPIRED_SESSION; 155 | var cookie = {}; 156 | cookie[cookieName] = querystring.stringify(session); 157 | 158 | var options = { 159 | headers: { Cookie: querystring.stringify(cookie) } 160 | }; 161 | httpServerTest(options, function(req, res) { 162 | test.deepEqual(req.facebook.getSession(), session, 'Expect session back.'); 163 | test.done(); 164 | }); 165 | }; 166 | 167 | exports.testInvalidGetSessionFromCookie = function(test) { 168 | var cookieName = 'fbs_' + APP_ID; 169 | var session = clone(VALID_EXPIRED_SESSION); 170 | session.uid = 'make me invalid'; 171 | var cookie = {}; 172 | cookie[cookieName] = querystring.stringify(session); 173 | 174 | var options = { 175 | headers: { Cookie: querystring.stringify(cookie) } 176 | }; 177 | httpServerTest(options, function(req, res) { 178 | test.ok(req.facebook.getSession() === null, 'Expect no session back.'); 179 | test.done(); 180 | }); 181 | }; 182 | 183 | exports.testSessionFromQueryString = function(test) { 184 | var options = { 185 | path: '/?' + querystring.stringify({ session: JSON.stringify(VALID_EXPIRED_SESSION) }) 186 | }; 187 | httpServerTest(options, function(req, res) { 188 | test.equal(req.facebook.getUser(), VALID_EXPIRED_SESSION.uid, 'Expect uid back.'); 189 | test.done(); 190 | }); 191 | }; 192 | 193 | exports.testInvalidSessionFromQueryString = function(test) { 194 | var qs = { 195 | fb_sig_in_iframe : 1, 196 | fb_sig_user : '1677846385', 197 | fb_sig_session_key : '2.NdKHtYIuB0EcNSHOvqAKHg__.86400.1258092000-1677846385', 198 | fb_sig_ss : 'AdCOu5nhDiexxRDLwZfqnA__', 199 | fb_sig : '1949f256171f37ecebe00685ce33bf17' 200 | }; 201 | var options = { 202 | path: '/?' + querystring.stringify(qs) 203 | }; 204 | 205 | httpServerTest(options, function(req, res) { 206 | test.equal(req.facebook.getUser(), null, 'Expect no user back.'); 207 | test.done(); 208 | }); 209 | }; 210 | 211 | // https://developers.facebook.com/blog/post/477/ 212 | exports.testSessionFromPost = function(test) { 213 | var options = { 214 | method: 'POST', 215 | post: { session: JSON.stringify(VALID_EXPIRED_SESSION) } 216 | }; 217 | httpServerTest(options, function(req, res) { 218 | test.equal(req.facebook.getUser(), VALID_EXPIRED_SESSION.uid, 'Expect uid back.'); 219 | test.done(); 220 | }); 221 | }; 222 | 223 | exports.testInvalidSessionFromPost = function(test) { 224 | var invalid_session = { 225 | fb_sig_in_iframe : 1, 226 | fb_sig_user : '1677846385', 227 | fb_sig_session_key : '2.NdKHtYIuB0EcNSHOvqAKHg__.86400.1258092000-1677846385', 228 | fb_sig_ss : 'AdCOu5nhDiexxRDLwZfqnA__', 229 | fb_sig : '1949f256171f37ecebe00685ce33bf17' 230 | }; 231 | var options = { 232 | method: 'POST', 233 | post: { session: JSON.stringify(invalid_session) } 234 | }; 235 | 236 | httpServerTest(options, function(req, res) { 237 | test.equal(req.facebook.getUser(), null, 'Expect no user back.'); 238 | test.done(); 239 | }); 240 | }; 241 | 242 | exports.testGetUID = function(test) { 243 | var facebook = new Facebook({ 244 | appId : APP_ID, 245 | secret : SECRET 246 | }); 247 | var session = VALID_EXPIRED_SESSION; 248 | facebook.setSession(session); 249 | test.equal(facebook.getUser(), session.uid, 'Expect dummy uid back.'); 250 | test.done(); 251 | }; 252 | 253 | exports.testAPIWithoutSession = function(test) { 254 | var facebook = new Facebook({ 255 | appId : APP_ID, 256 | secret : SECRET 257 | }); 258 | facebook.api({ 259 | method : 'fql.query', 260 | query : 'SELECT name FROM user WHERE uid=4' 261 | }, function(response) { 262 | test.equal(response.length, 1, 'Expect one row back.'); 263 | test.equal(response[0].name, 'Mark Zuckerberg', 'Expect the name back.'); 264 | test.done(); 265 | }); 266 | }; 267 | 268 | exports.testAPIWithSession = function(test) { 269 | var facebook = new Facebook({ 270 | appId : APP_ID, 271 | secret : SECRET 272 | }); 273 | facebook.setSession(VALID_EXPIRED_SESSION); 274 | 275 | // this is strange in that we are expecting a session invalid error vs a 276 | // signature invalid error. basically we're just making sure session based 277 | // signing is working, not that the api call is returning data. 278 | facebook.api({ 279 | method : 'fql.query', 280 | query : 'SELECT name FROM profile WHERE id=4' 281 | }, function(data) { 282 | test.ok(data.error); 283 | 284 | var msg = 'Exception: 190: Invalid OAuth 2.0 Access Token'; 285 | test.equal(data, msg, 'Expect the invalid session message.'); 286 | 287 | var result = data.getResult(); 288 | test.ok(typeof result == 'object', 'expect a result object'); 289 | test.equal(result.error_code, 190, 'expect code'); 290 | test.done(); 291 | }); 292 | }; 293 | 294 | exports.testAPIGraphPublicData = function(test) { 295 | var facebook = new Facebook({ 296 | appId : APP_ID, 297 | secret : SECRET 298 | }); 299 | 300 | facebook.api('/naitik', function(response) { 301 | test.equal(response.id, '5526183', 'should get expected id.'); 302 | test.done(); 303 | }); 304 | 305 | // regression test: calling api w/o callback throws TypeError 306 | facebook.api('/4'); 307 | }; 308 | 309 | exports.testGraphAPIWithSession = function(test) { 310 | var facebook = new Facebook({ 311 | appId : APP_ID, 312 | secret : SECRET 313 | }); 314 | facebook.setSession(VALID_EXPIRED_SESSION); 315 | 316 | facebook.api('/me', function(data) { 317 | test.ok(data.error); 318 | // means the server got the access token 319 | var msg = 'OAuthException: Error validating access token.'; 320 | test.equal(msg, data, 'Expect the invalid session message.'); 321 | // also ensure the session was reset since it was invalid 322 | test.equal(facebook.getSession(), null, 'Expect the session to be reset.'); 323 | test.done(); 324 | }); 325 | }; 326 | 327 | exports.testGraphAPIMethod = function(test) { 328 | var facebook = new Facebook({ 329 | appId : APP_ID, 330 | secret : SECRET 331 | }); 332 | 333 | facebook.api('/naitik', 'DELETE', function(data) { 334 | test.ok(data.error); 335 | // ProfileDelete means the server understood the DELETE 336 | var msg = 'OAuthException: An access token is required to request this resource.'; 337 | test.equal(msg, data, 'Expect the invalid session message.'); 338 | test.done(); 339 | }); 340 | }; 341 | 342 | exports.testGraphAPIOAuthSpecError = function(test) { 343 | var facebook = new Facebook({ 344 | appId : MIGRATED_APP_ID, 345 | secret : MIGRATED_SECRET 346 | }); 347 | 348 | facebook.api('/me', { client_id: MIGRATED_APP_ID }, function(data) { 349 | test.ok(data.error); 350 | // means the server got the access token 351 | msg = 'invalid_request: An active access token must be used to query information about the current user.'; 352 | test.equal(msg, data, 'Expect the invalid session message.'); 353 | // also ensure the session was reset since it was invalid 354 | test.equal(facebook.getSession(), null, 'Expect the session to be reset.'); 355 | test.done(); 356 | }); 357 | }; 358 | 359 | // TODO: I have done something wrong, or the spec has changed 360 | //exports.testGraphAPIMethodOAuthSpecError = function(test) { 361 | // var facebook = new Facebook({ 362 | // appId : MIGRATED_APP_ID, 363 | // secret : MIGRATED_SECRET 364 | // }); 365 | // 366 | // facebook.api('/daaku.shah', 'DELETE', { client_id: MIGRATED_APP_ID }, function(e) { 367 | // test.ok(e.error); 368 | // // ProfileDelete means the server understood the DELETE 369 | // msg = 'invalid_request: Test account not associated with application: The test account is not associated with this application.'; 370 | // test.equal(msg, e, 'Expect the invalid session message.'); 371 | // test.done(); 372 | // }); 373 | //}; 374 | 375 | exports.testCurlFailure = function(test) { 376 | var facebook = new Facebook({ 377 | appId : APP_ID, 378 | secret : SECRET 379 | }); 380 | 381 | // we dont expect facebook will ever return in 1ms 382 | facebook.timeout = 1; 383 | facebook.api('/naitik', function(data) { 384 | test.ok(data.error); 385 | var CURLE_OPERATION_TIMEDOUT = 28; 386 | test.equal(CURLE_OPERATION_TIMEDOUT, data.code, 'expect timeout'); 387 | test.equal('CurlException', data.getType(), 'expect type'); 388 | test.done(); 389 | }); 390 | }; 391 | 392 | // NOTE: modified to not use an access_token-required api query 393 | exports.testGraphAPIWithOnlyParams = function(test) { 394 | var facebook = new Facebook({ 395 | appId : APP_ID, 396 | secret : SECRET 397 | }); 398 | 399 | facebook.api('/' + APP_ID + '/insights', { limit:1 }, function(response) { 400 | test.equal(1, response.data.length, 'should get one entry'); 401 | test.done(); 402 | }); 403 | }; 404 | 405 | exports.testLoginURLDefaults = function(test) { 406 | var options = { 407 | path: '/examples', 408 | headers: { host : 'fbrell.com' } 409 | }; 410 | httpServerTest(options, function(req, res) { 411 | var encodedUrl = querystring.escape('http://fbrell.com/examples'); 412 | test.ok(req.facebook.getLoginUrl().indexOf(encodedUrl) >= 0, 'Expect the current url to exist.'); 413 | test.done(); 414 | }); 415 | }; 416 | 417 | exports.testUnavailableLoginURLThrowsError = function(test) { 418 | var facebook = new Facebook({ 419 | appId : APP_ID, 420 | secret : SECRET 421 | }); 422 | test.expect(1); 423 | test['throws'](function() { 424 | facebook.getLoginUrl(); 425 | }); 426 | test.done(); 427 | }; 428 | 429 | exports.testLoginURLDefaultsDropSessionQueryParam = function(test) { 430 | var options = { 431 | path: '/examples?session=xx42xx', 432 | headers: { host : 'fbrell.com' } 433 | }; 434 | httpServerTest(options, function(req, res) { 435 | var expectEncodedUrl = querystring.escape('http://fbrell.com/examples'); 436 | test.ok(req.facebook.getLoginUrl().indexOf(expectEncodedUrl) >= 0, 'Expect the current url to exist.'); 437 | test.ok(req.facebook.getLoginUrl().indexOf('xx42xx') == -1, 'Expect the session param to be dropped.'); 438 | test.done(); 439 | }); 440 | }; 441 | 442 | exports.testLoginURLDefaultsDropSessionQueryParamButNotOthers = function(test) { 443 | var options = { 444 | path: '/examples?session=xx42xx&do_not_drop=xx43xx', 445 | headers: { host : 'fbrell.com' } 446 | }; 447 | httpServerTest(options, function(req, res) { 448 | var expectEncodedUrl = querystring.escape('http://fbrell.com/examples'); 449 | test.ok(req.facebook.getLoginUrl().indexOf('xx42xx') == -1, 'Expect the session param to be dropped.'); 450 | test.ok(req.facebook.getLoginUrl().indexOf('xx43xx') >= 0, 'Expect the do_not_drop param to exist.'); 451 | test.done(); 452 | }); 453 | }; 454 | 455 | exports.testLoginURLCustomNext = function(test) { 456 | var options = { 457 | path: '/examples', 458 | headers: { host : 'fbrell.com' } 459 | }; 460 | httpServerTest(options, function(req, res) { 461 | var next = 'http://fbrell.com/custom'; 462 | var loginUrl = req.facebook.getLoginUrl({ 463 | next : next, 464 | cancel_url : next 465 | }); 466 | var currentEncodedUrl = querystring.escape('http://fbrell.com/examples'); 467 | var expectedEncodedUrl = querystring.escape(next); 468 | test.ok(loginUrl.indexOf(expectedEncodedUrl) >= 0, 'Expect the custom url to exist.'); 469 | test.ok(loginUrl.indexOf(currentEncodedUrl) == -1, 'Expect the current url to not exist.'); 470 | test.done(); 471 | }); 472 | }; 473 | 474 | exports.testLogoutURLDefaults = function(test) { 475 | var options = { 476 | path: '/examples', 477 | headers: { host : 'fbrell.com' } 478 | }; 479 | httpServerTest(options, function(req, res) { 480 | var encodedUrl = querystring.escape('http://fbrell.com/examples'); 481 | test.ok(req.facebook.getLogoutUrl().indexOf(encodedUrl) >= 0, 'Expect the current url to exist.'); 482 | test.done(); 483 | }); 484 | }; 485 | 486 | exports.testLoginStatusURLDefaults = function(test) { 487 | var options = { 488 | path: '/examples', 489 | headers: { host : 'fbrell.com' } 490 | }; 491 | httpServerTest(options, function(req, res) { 492 | var encodedUrl = querystring.escape('http://fbrell.com/examples'); 493 | test.ok(req.facebook.getLoginStatusUrl().indexOf(encodedUrl) >= 0, 'Expect the current url to exist.'); 494 | test.done(); 495 | }); 496 | }; 497 | 498 | exports.testLoginStatusURLCustom = function(test) { 499 | var options = { 500 | path: '/examples', 501 | headers: { host : 'fbrell.com' } 502 | }; 503 | httpServerTest(options, function(req, res) { 504 | var encodedUrl1 = querystring.escape('http://fbrell.com/examples'); 505 | var okUrl = 'http://fbrell.com/here1'; 506 | var encodedUrl2 = querystring.escape(okUrl); 507 | var loginStatusUrl = req.facebook.getLoginStatusUrl({ ok_session: okUrl }); 508 | test.ok(loginStatusUrl.indexOf(encodedUrl1) >= 0, 'Expect the current url to exist.'); 509 | test.ok(loginStatusUrl.indexOf(encodedUrl2) >= 0, 'Expect the custom url to exist.'); 510 | test.done(); 511 | }); 512 | }; 513 | 514 | exports.testNonDefaultPort = function(test) { 515 | var options = { 516 | path: '/examples', 517 | headers: { host : 'fbrell.com:8080' } 518 | }; 519 | httpServerTest(options, function(req, res) { 520 | var encodedUrl = querystring.escape('http://fbrell.com:8080/examples'); 521 | test.ok(req.facebook.getLoginUrl().indexOf(encodedUrl) >= 0, 'Expect the current url to exist.'); 522 | test.done(); 523 | }); 524 | }; 525 | 526 | exports.testSecureCurrentUrl = function(test) { 527 | var options = { 528 | https: true, 529 | path: '/examples', 530 | headers: { host : 'fbrell.com' } 531 | }; 532 | httpServerTest(options, function(req, res) { 533 | var encodedUrl = querystring.escape('https://fbrell.com/examples'); 534 | test.ok(req.facebook.getLoginUrl().indexOf(encodedUrl) >= 0, 'Expect the current url to exist.'); 535 | test.done(); 536 | }); 537 | }; 538 | 539 | exports.testSecureCurrentUrlWithNonDefaultPort = function(test) { 540 | var options = { 541 | https: true, 542 | path: '/examples', 543 | headers: { host : 'fbrell.com:8080' } 544 | }; 545 | httpServerTest(options, function(req, res) { 546 | var encodedUrl = querystring.escape('https://fbrell.com:8080/examples'); 547 | test.ok(req.facebook.getLoginUrl().indexOf(encodedUrl) >= 0, 'Expect the current url to exist.'); 548 | test.done(); 549 | }); 550 | }; 551 | 552 | exports.testAppSecretCall = function(test) { 553 | var facebook = new Facebook({ 554 | appId : APP_ID, 555 | secret : SECRET 556 | }); 557 | facebook.api('/' + APP_ID + '/insights', function(response) { 558 | test.ok(response.data.length > 0, 'Expect some data back.'); 559 | test.done(); 560 | }); 561 | }; 562 | 563 | exports.testBase64UrlEncode = function(test) { 564 | var input = 'Facebook rocks'; 565 | var output = 'RmFjZWJvb2sgcm9ja3M'; 566 | test.equal(Facebook.prototype._base64UrlDecode(output), input); 567 | test.done(); 568 | }; 569 | 570 | exports.testSignedToken = function(test) { 571 | var facebook = new Facebook({ 572 | appId : APP_ID, 573 | secret : SECRET 574 | }); 575 | var payload = facebook._parseSignedRequest(VALID_SIGNED_REQUEST); 576 | test.ok(payload, 'Expected token to parse'); 577 | var session = facebook._createSessionFromSignedRequest(payload); 578 | test.equal(session.uid, VALID_EXPIRED_SESSION.uid); 579 | test.equal(facebook.getSignedRequest(), null); 580 | 581 | // test that the actual signed request equals the expected one 582 | var options = { 583 | path: '/?' + querystring.stringify({ signed_request: VALID_SIGNED_REQUEST }) 584 | }; 585 | httpServerTest(options, function(req, res) { 586 | test.deepEqual(req.facebook.getSignedRequest(), payload); 587 | test.done(); 588 | }); 589 | }; 590 | 591 | exports.testSignedTokenInQuery = function(test) { 592 | var options = { 593 | path: '/?' + querystring.stringify({ signed_request: VALID_SIGNED_REQUEST }) 594 | }; 595 | httpServerTest(options, function(req, res) { 596 | test.ok(req.facebook.getSession()); 597 | test.done(); 598 | }); 599 | }; 600 | 601 | exports.testNonTossedSignedtoken = function(test) { 602 | var facebook = new Facebook({ 603 | appId : APP_ID, 604 | secret : SECRET 605 | }); 606 | var payload = facebook._parseSignedRequest(NON_TOSSED_SIGNED_REQUEST); 607 | test.ok(payload, 'Expected token to parse'); 608 | var session = facebook._createSessionFromSignedRequest(payload); 609 | test.ok(!session); 610 | test.ok(!facebook.getSignedRequest()); 611 | 612 | // test an actual http signed request 613 | var options = { 614 | path: '/?' + querystring.stringify({ signed_request: NON_TOSSED_SIGNED_REQUEST }) 615 | }; 616 | httpServerTest(options, function(req, res) { 617 | test.deepEqual(req.facebook.getSignedRequest(), {algorithm : 'HMAC-SHA256'}); 618 | test.done(); 619 | }); 620 | }; 621 | 622 | exports.testSignedTokenPost = function(test) { 623 | var options = { 624 | method: 'POST', 625 | post: { signed_request: VALID_SIGNED_REQUEST } 626 | }; 627 | httpServerTest(options, function(req, res) { 628 | test.ok(req.facebook.getSession()); 629 | test.done(); 630 | }); 631 | }; 632 | 633 | exports.testVideoUpload = function(test) { 634 | var facebook = new Facebook({ 635 | appId : APP_ID, 636 | secret : SECRET 637 | }); 638 | facebook.setSession(VALID_EXPIRED_SESSION); 639 | var url = facebook._getApiUrl('video.upload'); 640 | test.ok(url.indexOf('//api-video.') >= 0, 'video.upload should go against api-video'); 641 | test.done(); 642 | }; 643 | 644 | 645 | // TODO: is it possible or necessary to support this? 646 | // exports.testBundledCACert = function(test) { 647 | // var facebook = new Facebook({ 648 | // appId : APP_ID, 649 | // secret : SECRET, 650 | // }); 651 | // 652 | // // use the bundled cert from the start 653 | // Facebook::CURL_OPTS[CURLOPT_CAINFO] = dirname(__FILE__) . '/../src/fb_ca_chain_bundle.crt'; 654 | // response = facebook.api('/naitik'); 655 | // 656 | // unset(Facebook::CURL_OPTS[CURLOPT_CAINFO]); 657 | // test.equal( 658 | // response.id, '5526183', 'should get expected id.'); 659 | // } 660 | 661 | /** 662 | * Creates an http server using the 'test' handler function, 663 | * makes a request to the server using the options object, 664 | * and uses the 'result' handler function for testing the server response. 665 | */ 666 | function httpServerTest(options, test) { 667 | //options.https = false; 668 | var transport = options.https ? https : http; 669 | 670 | options.host = 'localhost'; 671 | options.port = 8889; 672 | options.path = options.path || '/'; 673 | 674 | if (options.https) { 675 | var server = connect({ 676 | key: fs.readFileSync(__dirname + '/test_key.pem'), 677 | cert: fs.readFileSync(__dirname + '/test_cert.pem') 678 | }); 679 | } else { 680 | var server = connect(); 681 | } 682 | 683 | server.use(connect.cookieParser()); 684 | server.use(connect.bodyParser()); 685 | server.use(Facebook({ 686 | appId : APP_ID, 687 | secret : SECRET 688 | })); 689 | 690 | server.use(function(req, res, next) { 691 | test(req, res); 692 | res.end(); 693 | server.close(); 694 | }); 695 | 696 | server.listen(options.port, function() { 697 | var request = transport.request(options /*, response */ ); 698 | if (options.post) { 699 | request.removeHeader('post'); 700 | var post_data = querystring.stringify(options.post); 701 | request.setHeader('Content-Type', 'application/x-www-form-urlencoded'); 702 | request.setHeader('Content-Length', post_data.length); 703 | request.write(post_data); 704 | } 705 | request.end(); 706 | }); 707 | } 708 | 709 | function clone(object) { 710 | var new_object = {}; 711 | for (var i in object) { 712 | new_object[i] = object[i]; 713 | } 714 | return new_object; 715 | } 716 | -------------------------------------------------------------------------------- /lib/facebook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2011 Facebook, Inc. 3 | * Copyright 2011 Christopher Johnson 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | var crypto = require('crypto'), 19 | http = require('http'), 20 | https = require('https'), 21 | querystring = require('querystring'), 22 | URL = require('url'), 23 | util = require('util'); 24 | 25 | /** 26 | * Thrown when an API call returns an exception. 27 | */ 28 | var FacebookApiException = function(result) { 29 | this.result = result; 30 | 31 | this.error = true; 32 | this.code = result.error_code ? result.error_code : 0; 33 | 34 | if (result.error_description) { 35 | // OAuth 2.0 Draft 10 style 36 | this.message = result.error_description; 37 | } else if (result.error && result.error.message) { 38 | // OAuth 2.0 Draft 00 style 39 | this.message = result.error.message; 40 | } else if (result.error_msg) { 41 | // Rest server style 42 | this.message = result.error_msg; 43 | } else { 44 | this.message = 'Unknown Error. Check getResult()'; 45 | } 46 | }; 47 | 48 | FacebookApiException.prototype = { 49 | // The result from the API server that represents the exception information. 50 | result: null, 51 | 52 | /** 53 | * Return the associated result object returned by the API server. 54 | * 55 | * @return {Object} the result from the API server 56 | */ 57 | getResult: function() { 58 | return this.result; 59 | }, 60 | 61 | /** 62 | * Returns the associated type for the error. This will default to 63 | * 'Exception' when a type is not available. 64 | * 65 | * @return {String} 66 | */ 67 | getType: function() { 68 | if (this.result.error) { 69 | error = this.result.error; 70 | if (typeof error == 'string') { 71 | // OAuth 2.0 Draft 10 style 72 | return error; 73 | } else if (error.type) { 74 | // OAuth 2.0 Draft 00 style 75 | return error.type; 76 | } 77 | } 78 | return 'Exception'; 79 | }, 80 | 81 | /** 82 | * To make debugging easier. 83 | * 84 | * @return {String} the string representation of the error 85 | */ 86 | toString: function() { 87 | str = this.getType() + ': '; 88 | if (this.code != 0) { 89 | str += this.code + ': '; 90 | } 91 | return str + this.message; 92 | } 93 | }; 94 | 95 | /** 96 | * Initialize the Facebook Application, providing access to the Facebook platform API. 97 | * 98 | * The configuration: 99 | * - appId: the application ID 100 | * - secret: the application secret 101 | * - request: (optional) http.ServerRequest for reclaiming sessions 102 | * - response: (optional) http.ServerResponse for writing the cookie to 103 | * - domain: (optional) domain for the cookie 104 | * TODO: 105 | * - fileUpload: (optional) boolean indicating if file uploads are enabled 106 | * 107 | * @param {Object} config the application configuration 108 | */ 109 | var Facebook = exports.facebook = exports.Facebook = function(config) { 110 | var facebook; 111 | if (this instanceof Facebook) { 112 | // instantiation using the 'new' operator 113 | facebook = this; 114 | } else { 115 | // connect style middleware function 116 | // TODO: this should also function as a Facebook object, add prototype 117 | facebook = function(req, res, next) { 118 | req.facebook = new Facebook(config); 119 | req.facebook.request = req; 120 | req.facebook.response = res; 121 | next(); 122 | }; 123 | } 124 | 125 | for (var i in config) { 126 | facebook[i] = config[i]; 127 | } 128 | 129 | return facebook; 130 | }; 131 | 132 | Facebook.prototype = { 133 | 134 | // The Application ID. 135 | appId: null, 136 | 137 | // The Application API Secret. 138 | secret: null, 139 | 140 | // http.ServerRequest for initializing the session 141 | request: null, 142 | 143 | // http.ServerResponse for writing the session cookie 144 | response: null, 145 | 146 | // Base domain for the Cookie. 147 | domain: '', 148 | 149 | // Indicates if the CURL based @ syntax for file uploads is enabled. 150 | fileUpload: false, 151 | 152 | // Milliseconds for connection with Facebook's servers to be established 153 | // TODO: connectTimeout: 10000, 154 | 155 | // Milliseconds for transmition of data from Facebook to complete 156 | timeout: 60000, 157 | 158 | // The active user session, if one is available. 159 | _session: null, 160 | 161 | // The data from the signed_request token. 162 | _signedRequest: null, 163 | 164 | // Indicates that we already loaded the session as best as we could. 165 | _sessionLoaded: false, 166 | 167 | 168 | // List of query parameters that get automatically dropped when rebuilding the current URL 169 | DROP_QUERY_PARAMS: [ 170 | 'session', 171 | 'signed_request' 172 | ], 173 | 174 | // Map of aliases to Facebook domains 175 | DOMAIN_MAP: { 176 | api : 'https://api.facebook.com/', 177 | api_video: 'https://api-video.facebook.com/', 178 | api_read : 'https://api-read.facebook.com/', 179 | graph : 'https://graph.facebook.com/', 180 | www : 'https://www.facebook.com/' 181 | }, 182 | 183 | /** 184 | * Get the data from a signed_request token 185 | * 186 | * @return {Object} 187 | */ 188 | getSignedRequest: function() { 189 | if (!this._signedRequest && this.request) { 190 | var signed_request = this.request.body && this.request.body.signed_request; 191 | signed_request = signed_request || URL.parse(this.request.url, true).query.signed_request; 192 | if (signed_request) { 193 | this._signedRequest = this._parseSignedRequest(signed_request); 194 | } 195 | } 196 | return this._signedRequest; 197 | }, 198 | 199 | /** 200 | * Set the Session. 201 | * 202 | * @param {Object} session the session 203 | * @param {boolean} write_cookie indicate if a cookie should be written. ignored if no response object. 204 | */ 205 | setSession: function(session, write_cookie) { 206 | write_cookie = write_cookie === undefined ? true : write_cookie; 207 | session = this._validateSessionObject(session); 208 | this._sessionLoaded = true; 209 | this._session = session; 210 | if (write_cookie) { 211 | this._setCookieFromSession(session); 212 | } 213 | return this; 214 | }, 215 | 216 | /** 217 | * Get the session object. This will automatically look for a signed session 218 | * sent via the signed_request, Cookie or Query Parameters if needed. 219 | * 220 | * @return {Object} the session 221 | */ 222 | getSession: function() { 223 | if (!this._sessionLoaded) { 224 | var session = null; 225 | var write_cookie = true; 226 | 227 | // try loading session from signed_request in request 228 | signedRequest = this.getSignedRequest(); 229 | if (signedRequest) { 230 | // sig is good, use the signedRequest 231 | session = this._createSessionFromSignedRequest(signedRequest); 232 | } 233 | 234 | // try loading session from request 235 | if (!session && this.request) { 236 | session = this.request.body && this.request.body.session; 237 | if (!session) { 238 | session = URL.parse(this.request.url, true).query.session; 239 | } 240 | if (session) { 241 | session = JSON.parse(session); 242 | session = this._validateSessionObject(session); 243 | } 244 | } 245 | 246 | // try loading session from cookie if necessary 247 | if (!session && this.request) { 248 | var cookie = this._getSessionCookie(); 249 | if (cookie) { 250 | var cookie = cookie.replace(/^"*|"*$/g, ''); 251 | session = querystring.parse(cookie); 252 | session = this._validateSessionObject(session); 253 | // write only if we need to delete a invalid session cookie 254 | write_cookie = !session; 255 | } 256 | } 257 | 258 | this.setSession(session, write_cookie); 259 | } 260 | 261 | return this._session; 262 | }, 263 | 264 | _getSessionCookie: function() { 265 | if (!this.request.cookies) { 266 | return; 267 | } 268 | var cookieName = this._getSessionCookieName(); 269 | return this.request.cookies[cookieName]; 270 | }, 271 | 272 | /** 273 | * Get the UID from the session. 274 | * 275 | * @return {String} the UID if available 276 | */ 277 | getUser: function() { 278 | session = this.getSession(); 279 | return session ? session.uid : null; 280 | }, 281 | 282 | /** 283 | * Gets a OAuth access token. 284 | * 285 | * @return {String} the access token 286 | */ 287 | getAccessToken: function() { 288 | session = this.getSession(); 289 | // either user session signed, or app signed 290 | if (session) { 291 | return session.access_token; 292 | } else { 293 | return this.appId +'|'+ this.secret; 294 | } 295 | }, 296 | 297 | /** 298 | * Get a Login URL for use with redirects. By default, full page redirect is 299 | * assumed. If you are using the generated URL with a window.open() call in 300 | * JavaScript, you can pass in display=popup as part of the params. 301 | * 302 | * The parameters (optional): 303 | * - next: the url to go to after a successful login 304 | * - cancel_url: the url to go to after the user cancels 305 | * - req_perms: comma separated list of requested extended perms 306 | * - display: can be "page" (default, full page) or "popup" 307 | * 308 | * @param {Object} params provide custom parameters 309 | * @return {String} the URL for the login flow 310 | */ 311 | getLoginUrl: function(params) { 312 | params = params || {}; 313 | currentUrl = this._getCurrentUrl(); 314 | 315 | var defaults = { 316 | api_key : this.appId, 317 | cancel_url : currentUrl, 318 | display : 'page', 319 | fbconnect : 1, 320 | next : currentUrl, 321 | return_session : 1, 322 | session_version : 3, 323 | v : '1.0' 324 | }; 325 | for (var i in defaults) { 326 | params[i] = params[i] || defaults[i]; 327 | } 328 | 329 | return this._getUrl('www', 'login.php', params); 330 | }, 331 | 332 | /** 333 | * Get a Logout URL suitable for use with redirects. 334 | * 335 | * The parameters: 336 | * - next: the url to go to after a successful logout 337 | * 338 | * @param {Object} params provide custom parameters 339 | * @return {String} the URL for the logout flow 340 | */ 341 | getLogoutUrl: function(params) { 342 | params = params || {}; 343 | 344 | var defaults = { 345 | next : this._getCurrentUrl(), 346 | access_token : this.getAccessToken() 347 | }; 348 | for (var i in defaults) { 349 | params[i] = params[i] || defaults[i]; 350 | } 351 | 352 | return this._getUrl('www', 'logout.php', params); 353 | }, 354 | 355 | /** 356 | * Get a login status URL to fetch the status from facebook. 357 | * 358 | * The parameters: 359 | * - ok_session: the URL to go to if a session is found 360 | * - no_session: the URL to go to if the user is not connected 361 | * - no_user: the URL to go to if the user is not signed into facebook 362 | * 363 | * @param {Object} params provide custom parameters 364 | * @return {String} the URL for the logout flow 365 | */ 366 | getLoginStatusUrl: function(params) { 367 | params = params || {}; 368 | 369 | var defaults = { 370 | api_key : this.appId, 371 | no_session : this._getCurrentUrl(), 372 | no_user : this._getCurrentUrl(), 373 | ok_session : this._getCurrentUrl(), 374 | session_version : 3 375 | }; 376 | for (var i in defaults) { 377 | params[i] = params[i] || defaults[i]; 378 | } 379 | 380 | return this._getUrl('www', 'extern/login_status.php', params); 381 | }, 382 | 383 | /** 384 | * Make an API call. 385 | */ 386 | api: function(/* polymorphic */) { 387 | if (typeof arguments[0] == 'object') { 388 | this._restserver.apply(this, arguments); 389 | } else { 390 | this._graph.apply(this, arguments); 391 | } 392 | }, 393 | 394 | /** 395 | * Invoke the old restserver.php endpoint. 396 | * 397 | * @param {Object} params method call object 398 | * @param {Function( object )} callback to send the decoded response object 399 | */ 400 | _restserver: function(params, callback) { 401 | // generic application level parameters 402 | params.api_key = this.appId; 403 | params.format = 'json-strings'; 404 | 405 | this._oauthRequest( 406 | this._getApiUrl(params.method), 407 | params, 408 | function(result) { 409 | result = JSON.parse(result); 410 | if (result && result.error_code) { 411 | result = new FacebookApiException(result); 412 | } 413 | callback(result); 414 | }, 415 | callback 416 | ); 417 | }, 418 | 419 | /** 420 | * Invoke the Graph API. 421 | * 422 | * @param {String} path the path (required) 423 | * @param {String} method the http method (default 'GET') 424 | * @param {Object} params the query/post data 425 | * @param {Function( object )} callback to send the decoded response object 426 | */ 427 | _graph: function(path, method, params, callback) { 428 | var self = this; 429 | 430 | if (typeof method != 'string') { 431 | callback = params; 432 | params = method || {}; 433 | method = params.method || 'GET'; 434 | } 435 | if (typeof params == 'function') { 436 | callback = params; 437 | params = {}; 438 | } 439 | params.method = method; 440 | 441 | this._oauthRequest( 442 | this._getUrl('graph', path), 443 | params, 444 | function(result) { 445 | result = JSON.parse(result); 446 | if (result && result.error) { 447 | var result = new FacebookApiException(result); 448 | switch (result.getType()) { 449 | case 'OAuthException': // OAuth 2.0 Draft 00 style 450 | case 'invalid_token': // OAuth 2.0 Draft 10 style 451 | // TODO: test and check if headers have alread been sent 452 | try { 453 | self.setSession(null); 454 | } catch (err) { 455 | console.log(err); 456 | } 457 | } 458 | } 459 | callback && callback(result); 460 | }, 461 | callback 462 | ); 463 | }, 464 | 465 | /** 466 | * Make a OAuth Request 467 | * 468 | * @param {String} path the path (required) 469 | * @param {Object} params the query/post data 470 | * @param {Function( string )} success to send the raw response string 471 | * @param {Function( FacebookApiException )} error to send the error on failure 472 | */ 473 | _oauthRequest: function(url, params, success, error) { 474 | if (!params.access_token) { 475 | params.access_token = this.getAccessToken(); 476 | } 477 | 478 | // json encode all params values that are not strings 479 | // TODO: untested 480 | for (var key in params) { 481 | if (typeof params[key] == 'object') { 482 | params[key] = JSON.stringify(params[key]); 483 | } 484 | } 485 | 486 | this._makeRequest(url, params, success, error); 487 | }, 488 | 489 | /** 490 | * Makes an HTTP request. This method can be overriden by subclasses if 491 | * developers want to do fancier things or use something other than curl to 492 | * make the request. 493 | * 494 | * @param {String} url the URL to make the request to 495 | * @param {Object} params the parameters to use for the POST body 496 | * @param {Function( string )} success callback to send the raw response data 497 | * @param {Function{ FacebookApiException }} error callback to send an error object 498 | */ 499 | _makeRequest: function(url, params, success, error) { 500 | var parts = URL.parse(url); 501 | 502 | var protocol = http; 503 | var port = 80; 504 | if (parts.protocol == 'https:') { 505 | protocol = https; 506 | port = 443; 507 | } 508 | 509 | var options = { 510 | host: parts.hostname, 511 | port: parts.port ? parts.port : port, 512 | path: parts.pathname, 513 | method: 'POST', 514 | agent: false 515 | }; 516 | 517 | // TODO: header 'Expect: 100-continue'? This was a part of the original curl makeRequest 518 | 519 | var request = protocol.request(options, function(result) { 520 | result.setEncoding('utf8'); 521 | 522 | var body = ''; 523 | result.on('data', function(chunk) { 524 | body += chunk; 525 | }); 526 | 527 | result.on('end', function() { 528 | clearTimeout(timeout); 529 | success(body); 530 | }); 531 | }); 532 | 533 | // TODO? 534 | // if (this.useFileUploadSupport()) { 535 | // opts[CURLOPT_POSTFIELDS] = params; 536 | // } else { 537 | // opts[CURLOPT_POSTFIELDS] = http_build_query(params, null, '&'); 538 | // } 539 | 540 | request.write(querystring.stringify(params)); 541 | request.end(); 542 | 543 | var timeout = setTimeout(function() { 544 | request.abort(); 545 | var e = new FacebookApiException({ 546 | error_code : 28 /* CURLE_OPERATION_TIMEDOUT */, 547 | error : { 548 | message : 'timeout', 549 | type : 'CurlException' 550 | } 551 | }); 552 | error && error(e); 553 | }, this.timeout); 554 | }, 555 | 556 | /** 557 | * The name of the Cookie that contains the session. 558 | * 559 | * @return {String} the cookie name 560 | */ 561 | _getSessionCookieName: function() { 562 | return 'fbs_' + this.appId; 563 | }, 564 | 565 | /** 566 | * Set a JS Cookie based on the _passed in_ session. It does not use the 567 | * currently stored session -- you need to explicitly pass it in. 568 | * 569 | * @param {Object} session the session to use for setting the cookie 570 | */ 571 | _setCookieFromSession: function(session) { 572 | if (!this.response) { 573 | return; 574 | } 575 | 576 | var name = this._getSessionCookieName(); 577 | var value = 'deleted'; 578 | var expires = new Date(Date.now() - 3600000); 579 | var domain = this.domain; 580 | if (session) { 581 | value = '"' + querystring.stringify(session) + '"'; 582 | if (session.base_domain) { 583 | domain = session.base_domain; 584 | } 585 | expires = new Date(session.expires * 1000); 586 | } 587 | 588 | // prepend dot if a domain is found 589 | if (domain) { 590 | domain = '.' + domain; 591 | } 592 | 593 | // if an existing cookie is not set, we dont need to delete it 594 | // TODO: how do we know the cookie does not exist? 595 | //if (value == 'deleted' && empty(_COOKIE[cookieName])) { 596 | // return; 597 | //} 598 | 599 | // TODO: statusCode check does not work, write proper test for this 600 | //if (this.response.statusCode) { 601 | // this._errorLog('Could not set cookie. Headers already sent.'); 602 | //} else { 603 | var cookie = require('connect').utils.serializeCookie(name, value, { 604 | domain: domain, 605 | path: '/', 606 | expires: expires 607 | }); 608 | this.response.setHeader('Set-Cookie', cookie); 609 | //} 610 | }, 611 | 612 | /** 613 | * Validates a session_version=3 style session object. 614 | * 615 | * @param {Object} session the session object 616 | * @return {Object} the session object if it validates, null otherwise 617 | */ 618 | _validateSessionObject: function(session) { 619 | // make sure some essential fields exist 620 | if (session && 621 | session.uid && 622 | session.access_token && 623 | session.sig) { 624 | expected_sig = this._generateSignature(session, this.secret); 625 | if (session.sig != expected_sig) { 626 | this._errorLog('Got invalid session signature in cookie.'); 627 | session = null; 628 | } 629 | // TODO: check expiry time? this was never implemented in the original php lib 630 | } else { 631 | session = null; 632 | } 633 | return session; 634 | }, 635 | 636 | /** 637 | * Returns something that looks like our JS session object from the 638 | * signed token's data 639 | * 640 | * TODO: Nuke this once the login flow uses OAuth2 641 | * 642 | * @param {Object} data the output of getSignedRequest 643 | * @return {Object} Something that will work as a session 644 | */ 645 | _createSessionFromSignedRequest: function(data) { 646 | if (!data.oauth_token) { 647 | return null; 648 | } 649 | 650 | session = { 651 | uid : data.user_id, 652 | access_token : data.oauth_token, 653 | expires : data.expires 654 | }; 655 | 656 | // put a real sig, so that validateSignature works 657 | session.sig = this._generateSignature(session, this.secret); 658 | 659 | return session; 660 | }, 661 | 662 | /** 663 | * Parses a signed_request and validates the signature. 664 | * Then saves it in this.signed_data 665 | * 666 | * @param {String} signed_request A signed token 667 | * @return {Object} the payload inside it or null if the sig is wrong 668 | */ 669 | _parseSignedRequest: function(signed_request) { 670 | var split = signed_request.split('.', 2); 671 | if (split.length != 2) { 672 | return null; 673 | } 674 | var encoded_sig = split[0]; 675 | var payload = split[1]; 676 | 677 | // decode the data 678 | sig = this._base64UrlDecode(encoded_sig); 679 | data = JSON.parse(this._base64UrlDecode(payload)); 680 | 681 | if (data.algorithm.toUpperCase() !== 'HMAC-SHA256') { 682 | this._errorLog('Unknown algorithm. Expected HMAC-SHA256'); 683 | return null; 684 | } 685 | 686 | // check sig 687 | var hmac = crypto.createHmac('sha256', this.secret); 688 | hmac.update(payload); 689 | expected_sig = hmac.digest(); 690 | if (sig !== expected_sig) { 691 | this._errorLog('Bad Signed JSON signature!'); 692 | return null; 693 | } 694 | 695 | return data; 696 | }, 697 | 698 | /** 699 | * Build the URL for api given parameters. 700 | * 701 | * @param {String} method the method name. 702 | * @return {String} the URL for the given parameters 703 | */ 704 | _getApiUrl: function(method) { 705 | const READ_ONLY_CALLS = { 706 | 'admin.getallocation' : 1, 707 | 'admin.getappproperties' : 1, 708 | 'admin.getbannedusers' : 1, 709 | 'admin.getlivestreamvialink' : 1, 710 | 'admin.getmetrics' : 1, 711 | 'admin.getrestrictioninfo' : 1, 712 | 'application.getpublicinfo' : 1, 713 | 'auth.getapppublickey' : 1, 714 | 'auth.getsession' : 1, 715 | 'auth.getsignedpublicsessiondata' : 1, 716 | 'comments.get' : 1, 717 | 'connect.getunconnectedfriendscount' : 1, 718 | 'dashboard.getactivity' : 1, 719 | 'dashboard.getcount' : 1, 720 | 'dashboard.getglobalnews' : 1, 721 | 'dashboard.getnews' : 1, 722 | 'dashboard.multigetcount' : 1, 723 | 'dashboard.multigetnews' : 1, 724 | 'data.getcookies' : 1, 725 | 'events.get' : 1, 726 | 'events.getmembers' : 1, 727 | 'fbml.getcustomtags' : 1, 728 | 'feed.getappfriendstories' : 1, 729 | 'feed.getregisteredtemplatebundlebyid' : 1, 730 | 'feed.getregisteredtemplatebundles' : 1, 731 | 'fql.multiquery' : 1, 732 | 'fql.query' : 1, 733 | 'friends.arefriends' : 1, 734 | 'friends.get' : 1, 735 | 'friends.getappusers' : 1, 736 | 'friends.getlists' : 1, 737 | 'friends.getmutualfriends' : 1, 738 | 'gifts.get' : 1, 739 | 'groups.get' : 1, 740 | 'groups.getmembers' : 1, 741 | 'intl.gettranslations' : 1, 742 | 'links.get' : 1, 743 | 'notes.get' : 1, 744 | 'notifications.get' : 1, 745 | 'pages.getinfo' : 1, 746 | 'pages.isadmin' : 1, 747 | 'pages.isappadded' : 1, 748 | 'pages.isfan' : 1, 749 | 'permissions.checkavailableapiaccess' : 1, 750 | 'permissions.checkgrantedapiaccess' : 1, 751 | 'photos.get' : 1, 752 | 'photos.getalbums' : 1, 753 | 'photos.gettags' : 1, 754 | 'profile.getinfo' : 1, 755 | 'profile.getinfooptions' : 1, 756 | 'stream.get' : 1, 757 | 'stream.getcomments' : 1, 758 | 'stream.getfilters' : 1, 759 | 'users.getinfo' : 1, 760 | 'users.getloggedinuser' : 1, 761 | 'users.getstandardinfo' : 1, 762 | 'users.hasapppermission' : 1, 763 | 'users.isappuser' : 1, 764 | 'users.isverified' : 1, 765 | 'video.getuploadlimits' : 1 766 | }; 767 | var name = 'api'; 768 | method = method.toLowerCase(); 769 | if (READ_ONLY_CALLS[method]) { 770 | name = 'api_read'; 771 | } else if (method === 'video.upload') { 772 | name = 'api_video'; 773 | } 774 | return this._getUrl(name, 'restserver.php'); 775 | }, 776 | 777 | /** 778 | * Build the URL for given domain alias, path and parameters. 779 | * 780 | * @param {String} name the name of the domain 781 | * @param {String} path optional path (without a leading slash) 782 | * @param {Object} params optional query parameters 783 | * @return {String} the URL for the given parameters 784 | */ 785 | _getUrl: function(name, path, params) { 786 | var url = this.DOMAIN_MAP[name]; 787 | if (path) { 788 | if (path[0] === '/') { 789 | path = path.substr(1); 790 | } 791 | url += path; 792 | } 793 | if (params) { 794 | url += '?' + querystring.stringify(params); 795 | } 796 | return url; 797 | }, 798 | 799 | /** 800 | * Returns the Current URL, stripping it of known FB parameters that should 801 | * not persist. 802 | * 803 | * @return {String} the current URL 804 | */ 805 | _getCurrentUrl: function() { 806 | if (this.request && this.request.headers.host) { 807 | var site = { 808 | protocol: this.request.connection.encrypted ? 'https:' : 'http:', 809 | host: this.request.headers.host 810 | }; 811 | } else { 812 | throw new Error('No request host available'); 813 | } 814 | 815 | var url = URL.parse(this.request.url, true); 816 | 817 | // drop known fb params 818 | this.DROP_QUERY_PARAMS.forEach(function(key) { 819 | delete url.query[key]; 820 | }); 821 | 822 | var currentUrl = site.protocol + '//' + site.host + url.pathname; 823 | if (url.query) { 824 | currentUrl += '?' + querystring.stringify(url.query); 825 | } 826 | 827 | return currentUrl; 828 | }, 829 | 830 | /** 831 | * Generate a signature for the given params and secret. 832 | * 833 | * @param {Object} params the parameters to sign 834 | * @param {String} secret the secret to sign with 835 | * @return {String} the generated signature 836 | */ 837 | _generateSignature: function(params, secret) { 838 | var md5 = crypto.createHash('md5'); 839 | Object.keys(params).sort().forEach(function(key) { 840 | if (key !== 'sig') { 841 | md5.update(key + '=' + params[key]); 842 | } 843 | }); 844 | md5.update(secret); 845 | return md5.digest('hex'); 846 | }, 847 | 848 | /** 849 | * Prints to the error log if you aren't in command line mode. 850 | * 851 | * @param {String} msg log message 852 | */ 853 | _errorLog: function(msg) { 854 | console.log(msg); 855 | }, 856 | 857 | /** 858 | * Base64 encoding that doesn't need to be urlencode()ed. 859 | * Exactly the same as base64_encode except it uses 860 | * - instead of + 861 | * _ instead of / 862 | * 863 | * @param {String} input base64UrlEncodeded string 864 | * @param {String} decoded 865 | */ 866 | _base64UrlDecode: function(input) { 867 | var buffer = new Buffer(input.replace('-', '+').replace('_', '/'), 'base64'); 868 | return buffer.toString('binary'); 869 | } 870 | }; 871 | --------------------------------------------------------------------------------