PageRenderTime 26ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/server.js

https://github.com/gijs/streamie
JavaScript | 360 lines | 275 code | 50 blank | 35 comment | 56 complexity | 59ced5f1ba7f2d61a9627dd3deda59f7 MD5 | raw file
  1. var http = require('http'),
  2. sys = require('sys'),
  3. io = require('socket.io'),
  4. static = require('node-static'),
  5. httpProxy = require('http-proxy'),
  6. oauth = require('./oauth'),
  7. sha1 = require('./sha1'),
  8. twitterClient = require('./twitter-stream-client'),
  9. twitterProxy = require('./twitter-rest-proxy'),
  10. lessHandler = require('./less-handler'),
  11. stats = require('./stats');
  12. // change this to a configuration file
  13. var host = process.ARGV[2];
  14. var key = process.ARGV[3];
  15. var secret = process.ARGV[4];
  16. var port = parseInt(process.ARGV[5], 10) || 8888;
  17. var IMGUR_KEY = process.ARGV[6];
  18. if(!host || !key || !secret) {
  19. sys.error("USAGE node process.js HOST(optional incl. portnumer for non-80) KEY SECRET PORT [imgur.com key]");
  20. process.exit();
  21. }
  22. // the static file server
  23. var file = new(static.Server)('../public', { cache: 0 });
  24. var Token = {}; // Temp store for Tokens. TODO: Make this not a memory leak and scale to multiple nodes
  25. var TransferURL = {};
  26. var server = http.createServer(function (request, response) {
  27. function respondError(error) {
  28. console.log(error);
  29. response.writeHead(501, {
  30. 'Content-Type': "text/plain"
  31. });
  32. response.end(error);
  33. }
  34. function getTwitterApiClient(cb) {
  35. var match = (request.headers.cookie || "").match(/token=([^;]+)/)
  36. if(!match) {
  37. return respondError("Cant find access token cookie in "+request.headers.cookie)
  38. }
  39. console.log("Token "+match[1]);
  40. var token = match[1];
  41. oauth.reuse(key, secret, token, function (err, requester) {
  42. if(err) {
  43. respondError("no_auth")
  44. } else {
  45. cb(err, requester);
  46. }
  47. });
  48. }
  49. // Please somebody put in a middleware/router thing!
  50. var url = require('url').parse(request.url, true);
  51. console.log(JSON.stringify(request.headers))
  52. // /access
  53. // Prepare oauth negotiation and send user to Twitter
  54. if(url.pathname.match(/^\/access$/)) {
  55. var otherhost = request.headers.host;
  56. var re = new RegExp(host+"$"); // only allow hosts that are sub domains of the host name from the command line
  57. var targetHost = host;
  58. if(otherhost.match(re)) {
  59. targetHost = otherhost;
  60. }
  61. var oauth_callback_url = "http://"+host+"/oauth_callback?host="+targetHost;
  62. oauth.request(key, secret, oauth_callback_url, function (error, token, url, cont) {
  63. if(error) {
  64. respondError(error);
  65. } else {
  66. var temp = sha1.HMACSHA1(token,""+Math.random());
  67. TransferURL[temp] = {
  68. url: url,
  69. host: targetHost
  70. };
  71. var transferURL = "http://"+host+"/transfer?token="+encodeURIComponent(temp);
  72. console.log("Transfer via "+transferURL)
  73. console.log("oauth step 1 success");
  74. Token[token] = cont;
  75. response.writeHead(302, { // onto Twitter
  76. 'Location': transferURL
  77. });
  78. response.end()
  79. }
  80. })
  81. }
  82. // /transfer
  83. // ...
  84. else if(url.pathname.match(/^\/transfer/)) {
  85. var otherhost = request.headers.host; // Cannot be overriden in Ajax. See http://www.w3.org/TR/2006/WD-XMLHttpRequest-20060405/#dfn-setrequestheader
  86. // only allow requests to the main host set via command line
  87. if(host != otherhost) {
  88. return respondError("Illegal host");
  89. }
  90. var transfer = url.query.token;
  91. if(!transfer) {
  92. return respondError("Cant find transfer token "+transfer);
  93. }
  94. var info = TransferURL[transfer];
  95. if(!info) {
  96. return respondError("Cant find transfer url "+transfer);
  97. }
  98. if(info.host == host) {
  99. response.writeHead(302, { // onto Twitter
  100. 'Location': info.url
  101. });
  102. response.end()
  103. return
  104. }
  105. response.writeHead(200, { // onto Twitter
  106. 'Content-Type': "text/html"
  107. });
  108. response.end('<html>The Streamie client <b>'+info.host+'</b> wants to access your Twitter account.</b><br>'+
  109. '<form><input type="button" value="Cancel" onclick="location=\'http://www.google.com\'"><input type="button" value="OK" onclick="location=\''+info.url+'\'"></form></html>')
  110. }
  111. // /oauth_callback
  112. // Handle response from Twitter after user allowed us
  113. // After success send user to /oauth_transfer to allow muliple sub domains
  114. else if(url.pathname.match(/^\/oauth_callback/)) {
  115. console.log(JSON.stringify(Token));
  116. var token = url.query.oauth_token;
  117. var oauth_token_verifier = url.query.oauth_verifier;
  118. var cont = Token[token];
  119. if(!cont) {
  120. respondError("Cannot find token "+token)
  121. } else {
  122. console.log("oauth step 2 success");
  123. cont(oauth_token_verifier, function (error, requester) {
  124. if(error) {
  125. respondError(error);
  126. console.log(error);
  127. } else {
  128. console.log("oauth step 3 success");
  129. var target = "http://"+url.query.host+"/oauth_transfer?oauth_token="+url.query.oauth_token;
  130. response.writeHead(302, {
  131. 'Location': target
  132. });
  133. response.end()
  134. }
  135. })
  136. }
  137. }
  138. // /oauth_transfer
  139. // set oauth token cookie and send user to homepage
  140. else if(url.pathname.match(/^\/oauth_transfer/)) {
  141. response.writeHead(302, {
  142. 'Location': "/",
  143. 'Set-Cookie': "token="+url.query.oauth_token+"; expires=Fri, 31-Jul-2011 23:59:59 GMT;" // TODO make this dynamic
  144. });
  145. response.end()
  146. }
  147. // /twitter
  148. // Twitter REST API proxy
  149. else if(url.pathname.match(/^\/twitter/)) {
  150. request.url = request.url.replace(/^\/twitter/, ""); // now proxy make request to sub dir
  151. console.log("Twitter API call" + request.url);
  152. var generic = request.url.replace(/\d+/g, "").replace(/\?.*/, "").replace(/\.\w+$/, "");
  153. stats.inc(generic);
  154. var data = [];
  155. request.on("data", function (chunk) {
  156. data.push(chunk.toString('ascii')); // no support for streaming request bodies!
  157. });
  158. request.on("end", function () {
  159. getTwitterApiClient(function (err, requester) {
  160. console.log("Proxying API call "+request.url);
  161. twitterProxy.proxy(requester, request, response, data.join(""))
  162. });
  163. })
  164. }
  165. // /imgur
  166. // imgur.com REST API -> simple proxy
  167. else if(url.pathname.match(/^\/imgur/)) {
  168. // we do this to autorize the user to be a valid twitter user
  169. var proxy = new httpProxy.HttpProxy(request, response);
  170. getTwitterApiClient(function (err, requester) {
  171. if(!err) {
  172. console.log("Onto imgur...");
  173. request.url = request.url.replace(/^\/imgur/, ""); // now proxy make request to sub dir
  174. stats.inc(request.url);
  175. request.url+="?key="+IMGUR_KEY; // pass the key via query string
  176. var domain = "api.imgur.com"; // change the host header
  177. request.headers.host = domain;
  178. delete request.headers.cookie; // we do not want to pass on session secrets
  179. proxy.proxyRequest(80, domain, request, response);
  180. }
  181. });
  182. }
  183. // less.js server side compilation
  184. else if(url.pathname.match(/css\/\w+\.less\.css/)) {
  185. lessHandler.handle("../public"+url.pathname, request, response);
  186. }
  187. // All other requests
  188. else {
  189. var curhost = request.headers.host;
  190. var sub = curhost.match(/(.*)\..*\..*/);
  191. if(sub) { // Requests to sub domains
  192. sub = sub[1];
  193. if(sub == "www") { // redirect www to host without www
  194. response.writeHead(302, { // onto Twitter
  195. 'Location': "http://"+host
  196. });
  197. response.end()
  198. }
  199. // Proxy static requests to the github page of the user of the same name as the sub domain
  200. else {
  201. var domain = sub+".github.com"
  202. request.url = "/streamie/public" + request.url;
  203. request.headers.host = domain;
  204. console.log("Proxy to http://"+domain+request.url);
  205. var proxy = new httpProxy.HttpProxy(request, response);
  206. proxy.proxyRequest(80, domain, request, response);
  207. }
  208. }
  209. // serve from the local disk
  210. else {
  211. console.log("Static" +url.pathname);
  212. request.addListener('end', function () {
  213. file.serve(request, response);
  214. });
  215. }
  216. }
  217. });
  218. server.listen(port);
  219. var socket = io.listen(server);
  220. var clients = {};
  221. var dummies = [
  222. '{"text":"@fearphage sitll missing the point. if MS makes the OS, and IE is the default browser, advertising MS products to that person is decent bet.","geo":null,"retweet_count":null,"truncated":false,"in_reply_to_user_id":8749632,"created_at":"Tue Aug 24 19:21:18 +0000 2010","coordinates":null,"in_reply_to_status_id":22024527110,"source":"web","retweeted":false,"favorited":false,"place":null,"user":{"profile_use_background_image":true,"location":"Austin TX","geo_enabled":false,"friends_count":364,"profile_link_color":"2FC2EF","verified":false,"notifications":null,"follow_request_sent":null,"favourites_count":11,"created_at":"Fri Oct 10 17:27:28 +0000 2008","profile_sidebar_fill_color":"252429","profile_image_url":"http://a1.twimg.com/profile_images/251788021/getify-logo_normal.gif","description":"Getify Solutions. JavaScript, or I\'m bored by it. -- warning: i prematurely optimize, so deal with it.","url":"http://blog.getify.com","time_zone":"Central Time (US & Canada)","profile_sidebar_border_color":"181A1E","screen_name":"getify","lang":"en","listed_count":148,"following":null,"profile_background_image_url":"http://s.twimg.com/a/1281738360/images/themes/theme9/bg.gif","protected":false,"statuses_count":8141,"profile_background_color":"1A1B1F","name":"Kyle Simpson","show_all_inline_media":false,"profile_background_tile":false,"id":16686076,"contributors_enabled":false,"utc_offset":-21600,"profile_text_color":"666666","followers_count":854},"id":22024650484,"contributors":null,"in_reply_to_screen_name":"fearphage"}',
  223. '{"text":"@unscriptable CVC is a pattern, not a framework. but my *code* could kick your code\'s butt without even entering the ring. :) lol.","geo":null,"retweet_count":null,"truncated":false,"in_reply_to_user_id":15899501,"created_at":"Tue Aug 24 18:59:27 +0000 2010","coordinates":null,"in_reply_to_status_id":22023145153,"source":"web","retweeted":false,"favorited":false,"place":null,"user":{"profile_use_background_image":true,"location":"Austin TX","geo_enabled":false,"friends_count":364,"profile_link_color":"2FC2EF","verified":false,"notifications":null,"follow_request_sent":null,"favourites_count":11,"created_at":"Fri Oct 10 17:27:28 +0000 2008","profile_sidebar_fill_color":"252429","profile_image_url":"http://a1.twimg.com/profile_images/251788021/getify-logo_normal.gif","description":"Getify Solutions. JavaScript, or I\'m bored by it. -- warning: i prematurely optimize, so deal with it.","url":"http://blog.getify.com","time_zone":"Central Time (US & Canada)","profile_sidebar_border_color":"181A1E","screen_name":"getify","lang":"en","listed_count":148,"following":null,"profile_background_image_url":"http://s.twimg.com/a/1282351897/images/themes/theme9/bg.gif","protected":false,"statuses_count":8132,"profile_background_color":"1A1B1F","name":"Kyle Simpson","show_all_inline_media":false,"profile_background_tile":false,"id":16686076,"contributors_enabled":false,"utc_offset":-21600,"profile_text_color":"666666","followers_count":854},"id":22023322662,"contributors":null,"in_reply_to_screen_name":"unscriptable"}'
  224. ];
  225. function now() {
  226. return (new Date).getTime();
  227. }
  228. // On Socket.io connections
  229. socket.on('connection', function(client){
  230. console.log("Connection");
  231. var tweety, interval;
  232. // We always send JSON
  233. function send(data) {
  234. client.send(JSON.stringify(data))
  235. }
  236. var last = now();
  237. client.on('message', function(msg){
  238. var data = JSON.parse(msg);
  239. last = now();
  240. if(data == "ping") {
  241. send("pong")
  242. }
  243. else if(data.token) { // only interested in messages with tokens
  244. // try to find the token in the databse and reuse the credentials
  245. oauth.reuse(key, secret, data.token, function (err, requester, info) {
  246. if(err) {
  247. console.log("Cant find "+data.token+": "+err);
  248. // we need a new auth from the client
  249. send({
  250. error: "no_auth"
  251. });
  252. } else {
  253. // all good, lets go
  254. send({
  255. action: "auth_ok",
  256. info: info
  257. })
  258. var open = true;
  259. interval = setInterval(function () {
  260. if(now() - last > 10000) {
  261. clearInterval(interval)
  262. if(tweety && open) {
  263. console.log("Client Connection Timeout. Close Stream")
  264. tweety.end();
  265. }
  266. }
  267. }, 10000)
  268. // connect to backend twitter stream
  269. tweety = twitterClient.connect(requester, function (err, data) {
  270. console.log("Stream response "+err+data)
  271. if(err) {
  272. open = false;
  273. console.log(err);
  274. send({
  275. streamError: err
  276. });
  277. if(!err.connection == "close") {
  278. tweety.end();
  279. }
  280. } else {
  281. dummies.push(data);
  282. send({
  283. tweet: data
  284. });
  285. console.log(data);
  286. }
  287. });
  288. }
  289. });
  290. }
  291. })
  292. // send testing data
  293. /*setInterval(function () {var data = dummies[Math.floor(dummies.length * Math.random())];
  294. console.log("Mock "+data)
  295. client.send(JSON.stringify({
  296. tweet: data
  297. }))
  298. }, 2000);*/
  299. client.send(JSON.stringify("hello world"));
  300. stats.inc("con");
  301. client.on('disconnect', function(){
  302. stats.dec("con");
  303. if(interval) {
  304. clearInterval(interval);
  305. }
  306. if(tweety) {
  307. tweety.end();
  308. }
  309. })
  310. });