PageRenderTime 61ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/pumpsocket.js

https://gitlab.com/fuzzynemesis/pump.io
JavaScript | 375 lines | 322 code | 28 blank | 25 comment | 47 complexity | a71458d5cd96f596d2511239dde98f2a MD5 | raw file
Possible License(s): Apache-2.0
  1. // pumpsocket.js
  2. //
  3. // Our own socket.io application interface
  4. //
  5. // Copyright 2011-2012, E14N https://e14n.com/
  6. //
  7. // Licensed under the Apache License, Version 2.0 (the "License");
  8. // you may not use this file except in compliance with the License.
  9. // You may obtain a copy of the License at
  10. //
  11. // http://www.apache.org/licenses/LICENSE-2.0
  12. //
  13. // Unless required by applicable law or agreed to in writing, software
  14. // distributed under the License is distributed on an "AS IS" BASIS,
  15. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. // See the License for the specific language governing permissions and
  17. // limitations under the License.
  18. var sockjs = require("sockjs"),
  19. cluster = require("cluster"),
  20. uuid = require("node-uuid"),
  21. Step = require("step"),
  22. _ = require("underscore"),
  23. oauth = require("oauth-evanp"),
  24. randomString = require("./randomstring").randomString,
  25. URLMaker = require("./urlmaker").URLMaker,
  26. finishers = require("./finishers"),
  27. Activity = require("./model/activity").Activity,
  28. addFollowed = finishers.addFollowed,
  29. addLiked = finishers.addLiked,
  30. addLikers = finishers.addLikers,
  31. addShared = finishers.addShared,
  32. firstFewReplies = finishers.firstFewReplies,
  33. firstFewShares = finishers.firstFewShares;
  34. var connect = function(app, log) {
  35. var slog = log.child({component: "sockjs"}),
  36. options = {
  37. sockjs_url: "/javascript/sockjs.min.js",
  38. prefix: "/main/realtime/sockjs",
  39. log: function(severity, message) {
  40. if (_.isFunction(slog[severity])) {
  41. slog[severity](message);
  42. } else {
  43. slog.info(message);
  44. }
  45. }
  46. },
  47. server,
  48. id2url = {},
  49. url2id = {},
  50. id2conn = {},
  51. follow = function(url, id) {
  52. if (!_.has(url2id, url)) {
  53. cluster.worker.send({cmd: "follow", url: url});
  54. url2id[url] = [id];
  55. }
  56. if (!_.contains(url2id[url], id)) {
  57. url2id[url].push(id);
  58. }
  59. if (!_.contains(id2url[id], url)) {
  60. id2url[id].push(url);
  61. }
  62. },
  63. unfollow = function(url, id) {
  64. if (_.has(url2id, url) && _.contains(url2id[url], id)) {
  65. url2id[url].splice(url2id[url].indexOf(id), 1);
  66. if (url2id[url].length === 0) {
  67. cluster.worker.send({cmd: "unfollow", url: url});
  68. delete url2id[url];
  69. }
  70. }
  71. if (_.contains(id2url[id], url)) {
  72. id2url[id].splice(id2url[id].indexOf(url), 1);
  73. }
  74. },
  75. challenge = function(conn) {
  76. Step(
  77. function() {
  78. randomString(8, this);
  79. },
  80. function(err, str) {
  81. var url;
  82. if (err) {
  83. // <sad trombone>
  84. conn.log.error({err: err}, "Error creating random string");
  85. conn.close();
  86. } else {
  87. url = URLMaker.makeURL("/main/realtime/sockjs/"+str+"/challenge");
  88. conn.challengeURL = url;
  89. conn.write(JSON.stringify({cmd: "challenge",
  90. url: url,
  91. method: "GET"}));
  92. }
  93. }
  94. );
  95. },
  96. rise = function(conn, message) {
  97. var client,
  98. params = _.object(message.parameters);
  99. conn.log.debug({riseMessage: message}, "Client rose to challenge");
  100. if (message.action != conn.challengeURL) {
  101. conn.log.error({challenge: conn.challengeURL,
  102. response: message.action},
  103. "Bad challenge URL");
  104. conn.close();
  105. return;
  106. }
  107. // Wipe the URL so we don't recheck
  108. conn.challengeURL = null;
  109. if (_.has(params, "oauth_token")) {
  110. validateUser(message, function(err, user, client) {
  111. if (err) {
  112. conn.log.error({err: err},
  113. "Failed user authentication");
  114. conn.close();
  115. } else {
  116. conn.log.info({user: user,
  117. client: client},
  118. "User authentication succeeded.");
  119. conn.user = user;
  120. conn.client = client;
  121. conn.write(JSON.stringify({cmd: "authenticated",
  122. user: user.nickname}));
  123. }
  124. });
  125. } else {
  126. validateClient(message, function(err, client) {
  127. if (err) {
  128. conn.log.error({err: err},
  129. "Failed client authentication");
  130. conn.close();
  131. } else {
  132. conn.log.info({client: client},
  133. "Client authentication succeeded.");
  134. if (_.has(conn, "user")) {
  135. delete conn.user;
  136. }
  137. conn.client = client;
  138. conn.write(JSON.stringify({cmd: "authenticated"}));
  139. }
  140. });
  141. }
  142. },
  143. checkSignature = function(message, client, token, cb) {
  144. var params = _.object(message.parameters),
  145. oa = new oauth.OAuth(null,
  146. null,
  147. params.oauth_consumer_key,
  148. client.secret,
  149. null,
  150. null,
  151. params.oauth_signature_method),
  152. sent = params.oauth_signature,
  153. signature,
  154. copy = _.clone(params),
  155. normalized;
  156. // Remove the signature
  157. delete copy.oauth_signature;
  158. // Normalize into a string
  159. normalized = oa._normaliseRequestParams(copy);
  160. signature = oa._getSignature(message.method,
  161. message.action,
  162. normalized,
  163. token ? token.token_secret : null);
  164. if (signature == sent) {
  165. cb(null);
  166. } else {
  167. cb(new Error("Bad OAuth signature"));
  168. }
  169. },
  170. validateClient = function(message, cb) {
  171. var params = _.object(message.parameters),
  172. client;
  173. Step(
  174. function() {
  175. server.provider.validateNotReplayClient(params.oauth_consumer_key,
  176. null,
  177. params.oauth_timestamp,
  178. params.oauth_nonce,
  179. this);
  180. },
  181. function(err, result) {
  182. if (err) throw err;
  183. if (!result) throw new Error("Seen this nonce before!");
  184. server.provider.applicationByConsumerKey(params.oauth_consumer_key,
  185. this);
  186. },
  187. function(err, application) {
  188. if (err) throw err;
  189. client = application;
  190. checkSignature(message, application, null, this);
  191. },
  192. function(err) {
  193. if (err) {
  194. cb(err, null);
  195. } else {
  196. cb(null, client);
  197. }
  198. }
  199. );
  200. },
  201. validateUser = function(message, cb) {
  202. var params = _.object(message.parameters),
  203. client,
  204. token,
  205. user;
  206. Step(
  207. function() {
  208. server.provider.validToken(params.oauth_token, this);
  209. },
  210. function(err, result) {
  211. if (err) throw err;
  212. token = result;
  213. server.provider.validateNotReplayClient(params.oauth_consumer_key,
  214. params.oauth_token,
  215. params.oauth_timestamp,
  216. params.oauth_nonce,
  217. this);
  218. },
  219. function(err, result) {
  220. if (err) throw err;
  221. if (!result) throw new Error("Seen this nonce before!");
  222. server.provider.applicationByConsumerKey(params.oauth_consumer_key,
  223. this);
  224. },
  225. function(err, application) {
  226. if (err) throw err;
  227. client = application;
  228. checkSignature(message, client, token, this);
  229. },
  230. function(err) {
  231. if (err) throw err;
  232. server.provider.userIdByToken(params.oauth_token, this);
  233. },
  234. function(err, doc) {
  235. if (err) {
  236. cb(err, null, null);
  237. } else {
  238. cb(null, doc.user, doc.client);
  239. }
  240. }
  241. );
  242. };
  243. cluster.worker.on("message", function(msg) {
  244. var ids;
  245. if (msg.cmd == "update") {
  246. ids = url2id[msg.url];
  247. slog.info({activity: msg.activity.id, connections: (ids) ? ids.length : 0}, "Delivering activity to connections");
  248. if (ids && ids.length) {
  249. _.each(ids, function(id) {
  250. var act, profile, conn = id2conn[id];
  251. if (!conn) {
  252. return;
  253. }
  254. act = new Activity(msg.activity);
  255. Step(
  256. function() {
  257. var profile = (conn.user) ? conn.user.profile : null;
  258. act.checkRecipient(profile, this);
  259. },
  260. function(err, ok) {
  261. if (err) throw err;
  262. if (!ok) {
  263. conn.log.info({activity: msg.activity.id}, "No access; not delivering activity");
  264. return;
  265. }
  266. addLiked(profile, [act.object], this.parallel());
  267. addLikers(profile, [act.object], this.parallel());
  268. addShared(profile, [act.object], this.parallel());
  269. firstFewReplies(profile, [act.object], this.parallel());
  270. firstFewShares(profile, [act.object], this.parallel());
  271. if (act.object.isFollowable()) {
  272. addFollowed(profile, [act.object], this.parallel());
  273. }
  274. },
  275. function(err) {
  276. var tosend;
  277. if (err) {
  278. conn.log.error({err: err}, "Error finishing object");
  279. } else {
  280. tosend = _.pick(msg, "cmd", "url");
  281. tosend.activity = act;
  282. conn.log.info({activity: msg.activity.id}, "Delivering activity");
  283. conn.write(JSON.stringify(tosend));
  284. }
  285. }
  286. );
  287. });
  288. }
  289. }
  290. });
  291. server = sockjs.createServer(options);
  292. // Note this is a utility for us; SockJS uses the log() function
  293. // we pass in through options
  294. server.log = slog;
  295. server.log.debug("Setting up sockjs server.");
  296. // snatch the provider
  297. server.provider = app.provider;
  298. server.on("connection", function(conn) {
  299. if (conn === null) {
  300. server.log.info ("Connection event without a connection.");
  301. return;
  302. }
  303. var id = conn.id;
  304. conn.log = server.log.child({"connection_id": id, component: "sockjs"});
  305. conn.log.info("Connected");
  306. id2conn[id] = conn;
  307. id2url[id] = [];
  308. conn.on("close", function() {
  309. _.each(id2url[id], function(url) {
  310. unfollow(url, id);
  311. });
  312. delete id2url[id];
  313. delete id2conn[id];
  314. id = null;
  315. conn.log.info("Disconnected");
  316. });
  317. conn.on("data", function(message) {
  318. var data = JSON.parse(message);
  319. switch (data.cmd) {
  320. case "follow":
  321. conn.log.info({url: data.url}, "Follow");
  322. follow(data.url, id);
  323. break;
  324. case "unfollow":
  325. conn.log.info({url: data.url}, "Unfollow");
  326. unfollow(data.url, id);
  327. break;
  328. case "rise":
  329. conn.log.info({url: data.message.action}, "Rise");
  330. rise(conn, data.message);
  331. break;
  332. case "request":
  333. conn.log.info("Request");
  334. challenge(conn);
  335. break;
  336. default:
  337. conn.log.warn({cmd: data.cmd}, "Unrecognized command; ignoring.");
  338. break;
  339. }
  340. return;
  341. });
  342. // Send a challenge on connection
  343. challenge(conn);
  344. });
  345. server.installHandlers(app, options);
  346. };
  347. exports.connect = connect;