From ab4324148af7c8e3be34179a60b194cb37561574 Mon Sep 17 00:00:00 2001 From: Joshua Spence Date: Wed, 11 Jun 2014 10:16:31 -0700 Subject: [PATCH] Make the Aphlict server more resilient. Summary: Currently, the Aphlict server will crash if invalid JSON data is `POST`ed to it. I have fixed this to, instead, return a 400. Also made some minor formatting changes. Ref T4324. Ref T5284. Also, modify the data structure that is passed around (i.e. `POST`ed to the Aphlict server and broadcast to the Aphlict clients) to include the subscribers. Initially, I figured that we shouldn't expose this information to the clients... however, it is necessary for T4324 that the `AphlictMaster` is able to route a notification to the appropriate clients. Test Plan: Making the following `curl` request: `curl --data "{" http://localhost:22281/`. **Before** ``` sudo ./bin/aphlict debug Starting Aphlict server in foreground... Launching server: $ 'nodejs' '/usr/src/phabricator/src/applications/aphlict/management/../../../../support/aphlict/server/aphlict_server.js' --port='22280' --admin='22281' --host='localhost' --user='aphlict' [Wed Jun 11 2014 17:07:51 GMT+0000 (UTC)] Started Server (PID 2033) [Wed Jun 11 2014 17:07:55 GMT+0000 (UTC)] <<< UNCAUGHT EXCEPTION! >>> SyntaxError: Unexpected end of input >>> Server exited! ``` **After** (No output... the bad JSON is caught and a 400 is returned) Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: epriestley, Korvin Maniphest Tasks: T4324, T5284 Differential Revision: https://secure.phabricator.com/D9480 --- resources/celerity/map.php | 2 +- .../feed/PhabricatorFeedStoryPublisher.php | 6 +- .../client/PhabricatorNotificationClient.php | 2 +- support/aphlict/server/aphlict_server.js | 53 ++++++++++-------- .../server/lib/AphlictFlashPolicyServer.js | 4 +- support/aphlict/server/lib/AphlictListener.js | 16 +++--- .../aphlict/server/lib/AphlictListenerList.js | 28 +++++---- support/aphlict/server/lib/AphlictLog.js | 14 ++--- support/aphlict/server/lib/javelin.js | 2 +- webroot/rsrc/swf/aphlict.swf | Bin 5499 -> 5502 bytes 10 files changed, 65 insertions(+), 62 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 4d4f942332..9a30568d5d 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -476,7 +476,7 @@ return array( 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 'rsrc/js/phuix/PHUIXActionView.js' => '6e8cefa4', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca', - 'rsrc/swf/aphlict.swf' => 'b7c2d7aa', + 'rsrc/swf/aphlict.swf' => 'f45c3edc', ), 'symbols' => array( diff --git a/src/applications/feed/PhabricatorFeedStoryPublisher.php b/src/applications/feed/PhabricatorFeedStoryPublisher.php index 3c0dc0b808..274f0d8df6 100644 --- a/src/applications/feed/PhabricatorFeedStoryPublisher.php +++ b/src/applications/feed/PhabricatorFeedStoryPublisher.php @@ -176,10 +176,8 @@ final class PhabricatorFeedStoryPublisher { private function sendNotification($chrono_key) { $data = array( - 'data' => array( - 'key' => (string)$chrono_key, - 'type' => 'notification', - ), + 'key' => (string)$chrono_key, + 'type' => 'notification', 'subscribers' => $this->subscribedPHIDs, ); diff --git a/src/applications/notification/client/PhabricatorNotificationClient.php b/src/applications/notification/client/PhabricatorNotificationClient.php index ab7ee1c6d0..4c433c9b4f 100644 --- a/src/applications/notification/client/PhabricatorNotificationClient.php +++ b/src/applications/notification/client/PhabricatorNotificationClient.php @@ -2,7 +2,7 @@ final class PhabricatorNotificationClient { - const EXPECT_VERSION = 5; + const EXPECT_VERSION = 6; public static function getServerStatus() { $uri = PhabricatorEnv::getEnvConfig('notification.server-uri'); diff --git a/support/aphlict/server/aphlict_server.js b/support/aphlict/server/aphlict_server.js index d2212fcfa1..74374ea132 100644 --- a/support/aphlict/server/aphlict_server.js +++ b/support/aphlict/server/aphlict_server.js @@ -25,10 +25,10 @@ if (config.logfile) { function parse_command_line_arguments(argv) { var config = { - port : 22280, - admin : 22281, - host : '127.0.0.1', - user : null, + port: 22280, + admin: 22281, + host: '127.0.0.1', + user: null, log: '/var/log/aphlict.log' }; @@ -36,10 +36,10 @@ function parse_command_line_arguments(argv) { var arg = argv[ii]; var matches = arg.match(/^--([^=]+)=(.*)$/); if (!matches) { - throw new Error("Unknown argument '"+arg+"'!"); + throw new Error("Unknown argument '" + arg + "'!"); } if (!(matches[1] in config)) { - throw new Error("Unknown argument '"+matches[1]+"'!"); + throw new Error("Unknown argument '" + matches[1] + "'!"); } config[matches[1]] = matches[2]; } @@ -52,19 +52,19 @@ function parse_command_line_arguments(argv) { if (process.getuid() !== 0) { console.log( - "ERROR: "+ - "This server must be run as root because it needs to bind to privileged "+ - "port 843 to start a Flash policy server. It will downgrade to run as a "+ - "less-privileged user after binding if you pass a user in the command "+ + "ERROR: " + + "This server must be run as root because it needs to bind to privileged " + + "port 843 to start a Flash policy server. It will downgrade to run as a " + + "less-privileged user after binding if you pass a user in the command " + "line arguments with '--user=alincoln'."); process.exit(1); } var net = require('net'); -var http = require('http'); +var http = require('http'); var url = require('url'); -process.on('uncaughtException', function (err) { +process.on('uncaughtException', function(err) { debug.log("\n<<< UNCAUGHT EXCEPTION! >>>\n\n" + err); process.exit(1); }); @@ -95,7 +95,7 @@ var send_server = net.createServer(function(socket) { debug.log('<%s> Ended Connection', listener.getDescription()); }); - socket.on('error', function (e) { + socket.on('error', function(e) { debug.log('<%s> Error: %s', listener.getDescription(), e); }); @@ -107,23 +107,29 @@ var messages_in = 0; var start_time = new Date().getTime(); var receive_server = http.createServer(function(request, response) { - response.writeHead(200, {'Content-Type' : 'text/plain'}); - // Publishing a notification. if (request.method == 'POST') { var body = ''; - request.on('data', function (data) { + request.on('data', function(data) { body += data; }); - request.on('end', function () { - ++messages_in; + request.on('end', function() { + try { + var msg = JSON.parse(body); - var msg = JSON.parse(body); - debug.log('notification: ' + JSON.stringify(msg)); - broadcast(msg.data); - response.end(); + debug.log('notification: ' + JSON.stringify(msg)); + ++messages_in; + broadcast(msg); + + response.writeHead(200, {'Content-Type': 'text/plain'}); + } catch (err) { + response.statusCode = 400; + response.write('400 Bad Request'); + } finally { + response.end(); + } }); } else if (request.url == '/status/') { request.on('data', function(data) { @@ -139,9 +145,10 @@ var receive_server = http.createServer(function(request, response) { 'messages.in': messages_in, 'messages.out': messages_out, 'log': config.log, - 'version': 5 + 'version': 6 }; + response.writeHead(200, {'Content-Type': 'text/plain'}); response.write(JSON.stringify(status)); response.end(); }); diff --git a/support/aphlict/server/lib/AphlictFlashPolicyServer.js b/support/aphlict/server/lib/AphlictFlashPolicyServer.js index 77cb6311f8..8651d643a8 100644 --- a/support/aphlict/server/lib/AphlictFlashPolicyServer.js +++ b/support/aphlict/server/lib/AphlictFlashPolicyServer.js @@ -17,12 +17,12 @@ JX.install('AphlictFlashPolicyServer', { _accessPort: null, _debug: null, - setDebugLog : function(log) { + setDebugLog: function(log) { this._debug = log; return this; }, - setAccessPort : function(port) { + setAccessPort: function(port) { this._accessPort = port; return this; }, diff --git a/support/aphlict/server/lib/AphlictListener.js b/support/aphlict/server/lib/AphlictListener.js index 119ca18c2b..c148f437b7 100644 --- a/support/aphlict/server/lib/AphlictListener.js +++ b/support/aphlict/server/lib/AphlictListener.js @@ -1,28 +1,28 @@ var JX = require('javelin').JX; JX.install('AphlictListener', { - construct : function(id, socket) { + construct: function(id, socket) { this._id = id; this._socket = socket; }, - members : { - _id : null, - _socket : null, + members: { + _id: null, + _socket: null, - getID : function() { + getID: function() { return this._id; }, - getSocket : function() { + getSocket: function() { return this._socket; }, - getDescription : function() { + getDescription: function() { return 'Listener/' + this.getID(); }, - writeMessage : function(message) { + writeMessage: function(message) { var serial = JSON.stringify(message); var length = Buffer.byteLength(serial, 'utf8'); diff --git a/support/aphlict/server/lib/AphlictListenerList.js b/support/aphlict/server/lib/AphlictListenerList.js index 1520e49fce..9ffe64ff55 100644 --- a/support/aphlict/server/lib/AphlictListenerList.js +++ b/support/aphlict/server/lib/AphlictListenerList.js @@ -2,20 +2,18 @@ var JX = require('javelin').JX; JX.require('AphlictListener', __dirname); JX.install('AphlictListenerList', { - construct : function() { + construct: function() { this._listeners = {}; }, - members : { - _listeners : null, - _nextID : 0, - _activeListenerCount : 0, - _totalListenerCount : 0, + members: { + _listeners: null, + _nextID: 0, + _activeListenerCount: 0, + _totalListenerCount: 0, - addListener : function(socket) { - var listener = new JX.AphlictListener( - this._generateNextID(), - socket); + addListener: function(socket) { + var listener = new JX.AphlictListener(this._generateNextID(), socket); this._listeners[listener.getID()] = listener; this._activeListenerCount++; @@ -24,7 +22,7 @@ JX.install('AphlictListenerList', { return listener; }, - removeListener : function(listener) { + removeListener: function(listener) { var id = listener.getID(); if (id in this._listeners) { delete this._listeners[id]; @@ -32,19 +30,19 @@ JX.install('AphlictListenerList', { } }, - getListeners : function() { + getListeners: function() { return this._listeners; }, - getActiveListenerCount : function() { + getActiveListenerCount: function() { return this._activeListenerCount; }, - getTotalListenerCount : function() { + getTotalListenerCount: function() { return this._totalListenerCount; }, - _generateNextID : function() { + _generateNextID: function() { do { this._nextID = ((this._nextID + 1) % 1000000000000); } while (this._nextID in this._listeners); diff --git a/support/aphlict/server/lib/AphlictLog.js b/support/aphlict/server/lib/AphlictLog.js index d773b9ad63..259c03fada 100644 --- a/support/aphlict/server/lib/AphlictLog.js +++ b/support/aphlict/server/lib/AphlictLog.js @@ -4,16 +4,16 @@ var fs = require('fs'); var util = require('util'); JX.install('AphlictLog', { - construct : function() { + construct: function() { this._writeToLogs = []; this._writeToConsoles = []; }, - members : { - _writeToConsoles : null, - _writeToLogs : null, + members: { + _writeToConsoles: null, + _writeToLogs: null, - addLogfile : function(path) { + addLogfile: function(path) { var options = { flags: 'a', encoding: 'utf8', @@ -27,12 +27,12 @@ JX.install('AphlictLog', { return this; }, - addConsole : function(console) { + addConsole: function(console) { this._writeToConsoles.push(console); return this; }, - log : function(pattern) { + log: function(pattern) { var str = util.format.apply(null, arguments); var date = new Date().toLocaleString(); str = '[' + date + '] ' + str; diff --git a/support/aphlict/server/lib/javelin.js b/support/aphlict/server/lib/javelin.js index e90986a56c..9a4636f9cc 100644 --- a/support/aphlict/server/lib/javelin.js +++ b/support/aphlict/server/lib/javelin.js @@ -6,7 +6,7 @@ JX.require('core/install'); // NOTE: This is faking out a piece of code in JX.install which waits for // Stratcom before running static initializers. -JX.Stratcom = {ready : true}; +JX.Stratcom = {ready: true}; JX.require('core/Event'); JX.require('core/Stratcom'); diff --git a/webroot/rsrc/swf/aphlict.swf b/webroot/rsrc/swf/aphlict.swf index 589e046c3c6d384478ebb268fc98742f0e18f8fe..37b5d23776e3a925a8d277c65a5a8eaf213506f4 100644 GIT binary patch delta 5398 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