From dedd8e55f27e52b5b3e93921a9ae2cb8ab87345a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Legan=C3=A9s=20Combarro=20=22piranna?= Date: Thu, 11 Feb 2016 14:18:25 +0100 Subject: [PATCH 01/16] Emit `error` event when calling `EventSource.close()` --- lib/eventsource.js | 1 + test/eventsource_test.js | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/eventsource.js b/lib/eventsource.js index 7fb0a8b8..da2c7ad0 100644 --- a/lib/eventsource.js +++ b/lib/eventsource.js @@ -187,6 +187,7 @@ function EventSource(url, eventSourceInitDict) { readyState = EventSource.CLOSED; if (req.abort) req.abort(); if (req.xhr && req.xhr.abort) req.xhr.abort(); + _emit('error', new Event('error')); }; function parseEventStreamLine(buf, pos, fieldLength, lineLength) { diff --git a/test/eventsource_test.js b/test/eventsource_test.js index 79f0709a..c2e7a4ab 100644 --- a/test/eventsource_test.js +++ b/test/eventsource_test.js @@ -462,6 +462,8 @@ describe('HTTP Request', function () { var es = new EventSource(server.url); es.onerror = function (err) { + if(this.readyState === EventSource.CLOSED) return + assert.equal(err.status, status); server.close(done); }; @@ -588,6 +590,8 @@ describe('Reconnection', function () { es.reconnectInterval = 0; es.onerror = function (e) { + if(this.readyState === EventSource.CLOSED) return + assert.equal(e.status, 204); server.close(function (err) { if(err) return done(err); @@ -695,6 +699,8 @@ describe('readyState', function () { var es = new EventSource('http://localhost:' + _port); assert.equal(EventSource.CONNECTING, es.readyState); es.onerror = function () { + if(this.readyState === EventSource.CLOSED) return + es.close(); done(); } @@ -813,10 +819,12 @@ describe('Events', function () { es.addEventListener('open', function () { es.close(); process.nextTick(function () { - server.close(done); + server.close(); }); }); es.addEventListener('error', function () { + if(this.readyState === EventSource.CLOSED) return done() + done(new Error('error should not be emitted')); }); }); From ef27552abc5329e89f421faa454234e516f711a0 Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 14:51:45 +0200 Subject: [PATCH 02/16] Revert #47 --- lib/eventsource.js | 1 - test/eventsource_test.js | 10 +--------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/eventsource.js b/lib/eventsource.js index 8948c7b9..f97eb8e7 100644 --- a/lib/eventsource.js +++ b/lib/eventsource.js @@ -192,7 +192,6 @@ function EventSource(url, eventSourceInitDict) { readyState = EventSource.CLOSED; if (req.abort) req.abort(); if (req.xhr && req.xhr.abort) req.xhr.abort(); - _emit('error', new Event('error')); }; function parseEventStreamLine(buf, pos, fieldLength, lineLength) { diff --git a/test/eventsource_test.js b/test/eventsource_test.js index b33baefa..79548e36 100644 --- a/test/eventsource_test.js +++ b/test/eventsource_test.js @@ -463,8 +463,6 @@ describe('HTTP Request', function () { var es = new EventSource(server.url); es.onerror = function (err) { - if(this.readyState === EventSource.CLOSED) return - assert.equal(err.status, status); server.close(done); }; @@ -591,8 +589,6 @@ describe('Reconnection', function () { es.reconnectInterval = 0; es.onerror = function (e) { - if(this.readyState === EventSource.CLOSED) return - assert.equal(e.status, 204); server.close(function (err) { if(err) return done(err); @@ -700,8 +696,6 @@ describe('readyState', function () { var es = new EventSource('http://localhost:' + _port); assert.equal(EventSource.CONNECTING, es.readyState); es.onerror = function () { - if(this.readyState === EventSource.CLOSED) return - es.close(); done(); } @@ -842,12 +836,10 @@ describe('Events', function () { es.addEventListener('open', function () { es.close(); process.nextTick(function () { - server.close(); + server.close(done); }); }); es.addEventListener('error', function () { - if(this.readyState === EventSource.CLOSED) return done() - done(new Error('error should not be emitted')); }); }); From cb532e7a6d6a06a5e1d57a702471e34bf19306b5 Mon Sep 17 00:00:00 2001 From: Pirolf Date: Tue, 23 Feb 2016 22:30:41 -0800 Subject: [PATCH 03/16] add remove event listener so when using event source streams can be properly closed --- lib/eventsource.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/eventsource.js b/lib/eventsource.js index f97eb8e7..a386766b 100644 --- a/lib/eventsource.js +++ b/lib/eventsource.js @@ -294,6 +294,13 @@ EventSource.prototype.addEventListener = function addEventListener(method, liste } }; +EventSource.prototype.removeEventListener = function removeEventListener(method, listener) { + if (typeof listener === 'function') { + listener._listener = undefined; + this.off(method, listener); + } +}; + /** * W3C Event * From 33aec480b29eba8fa2784ecc43b3a80e4bcae7da Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 15:03:35 +0200 Subject: [PATCH 04/16] Fix incorrect event emitter usage in removeEventListener --- lib/eventsource.js | 19 ++++++++++++++----- test/eventsource_test.js | 24 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/lib/eventsource.js b/lib/eventsource.js index a386766b..2ea326db 100644 --- a/lib/eventsource.js +++ b/lib/eventsource.js @@ -280,24 +280,33 @@ Object.defineProperty(EventSource, 'CLOSED', { enumerable: true, value: 2}); /** * Emulates the W3C Browser based WebSocket interface using addEventListener. * - * @param {String} method Listen for an event + * @param {String} type A string representing the event type to listen out for * @param {Function} listener callback * @see https://developer.mozilla.org/en/DOM/element.addEventListener * @see http://dev.w3.org/html5/websockets/#the-websocket-interface * @api public */ -EventSource.prototype.addEventListener = function addEventListener(method, listener) { +EventSource.prototype.addEventListener = function addEventListener(type, listener) { if (typeof listener === 'function') { // store a reference so we can return the original function again listener._listener = listener; - this.on(method, listener); + this.on(type, listener); } }; -EventSource.prototype.removeEventListener = function removeEventListener(method, listener) { +/** + * Emulates the W3C Browser based WebSocket interface using removeEventListener. + * + * @param {String} type A string representing the event type to remove + * @param {Function} listener callback + * @see https://developer.mozilla.org/en/DOM/element.removeEventListener + * @see http://dev.w3.org/html5/websockets/#the-websocket-interface + * @api public + */ +EventSource.prototype.removeEventListener = function removeEventListener(type, listener) { if (typeof listener === 'function') { listener._listener = undefined; - this.off(method, listener); + this.removeListener(type, listener); } }; diff --git a/test/eventsource_test.js b/test/eventsource_test.js index 79548e36..c75e17db 100644 --- a/test/eventsource_test.js +++ b/test/eventsource_test.js @@ -232,6 +232,30 @@ describe('Parser', function () { }); }); + it('allows removal of event listeners', function (done) { + createServer(function (err, server) { + if (err) return done(err); + + server.on('request', writeEvents(["event: greeting\ndata: Hello\n\n", "event: greeting\ndata: World\n\n"])); + var es = new EventSource(server.url); + var numCalled = 0; + + function onGreeting(m) { + numCalled++; + assert.equal("Hello", m.data); + es.removeEventListener('greeting', onGreeting, false); + process.nextTick(scheduleDisconnect); + } + + function scheduleDisconnect() { + assert.equal(1, numCalled); + server.close(done); + } + + es.addEventListener('greeting', onGreeting, false); + }); + }); + it('ignores comments', function (done) { createServer(function (err, server) { if (err) return done(err); From af84476b519a01e61b8c80727261df52ae40022c Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 15:33:07 +0200 Subject: [PATCH 05/16] Don't attempt reconnect unless status code is 200 --- lib/eventsource.js | 3 +-- test/eventsource_test.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/eventsource.js b/lib/eventsource.js index 2ea326db..24faa5be 100644 --- a/lib/eventsource.js +++ b/lib/eventsource.js @@ -105,8 +105,7 @@ function EventSource(url, eventSourceInitDict) { if (res.statusCode !== 200) { _emit('error', new Event('error', {status: res.statusCode})); - if (res.statusCode == 204) return self.close(); - return + return self.close(); } readyState = EventSource.OPEN; diff --git a/test/eventsource_test.js b/test/eventsource_test.js index c75e17db..585c7174 100644 --- a/test/eventsource_test.js +++ b/test/eventsource_test.js @@ -600,7 +600,7 @@ describe('Reconnection', function () { }); }); - it('is not attempted when server responds with HTTP 204', function (done) { + it('is not attempted when server responds with non-200', function (done) { createServer(function (err, server) { if(err) return done(err); From 505628e2cf19d31c5c292ccdaf98bde77df1c5fc Mon Sep 17 00:00:00 2001 From: Rafael Alfaro Date: Mon, 29 Feb 2016 14:35:25 -0600 Subject: [PATCH 06/16] Added options map to eventSourceInitDict object to setup http/https connection options (specifically for certificate configuration) --- lib/eventsource.js | 10 ++++++++ test/client_certs/cacert.crt | 13 +++++++++++ test/client_certs/client_cert.crt | 13 +++++++++++ test/client_certs/client_key.pem | 18 ++++++++++++++ test/client_certs/server_cert.crt | 13 +++++++++++ test/client_certs/server_key.pem | 18 ++++++++++++++ test/eventsource_test.js | 39 +++++++++++++++++++++++++++++++ 7 files changed, 124 insertions(+) create mode 100644 test/client_certs/cacert.crt create mode 100644 test/client_certs/client_cert.crt create mode 100644 test/client_certs/client_key.pem create mode 100644 test/client_certs/server_cert.crt create mode 100644 test/client_certs/server_key.pem diff --git a/lib/eventsource.js b/lib/eventsource.js index 24faa5be..19eae747 100644 --- a/lib/eventsource.js +++ b/lib/eventsource.js @@ -89,6 +89,16 @@ function EventSource(url, eventSourceInitDict) { options.port = proxy.port; } + //If specify options, include all properties in the http/https request options + if (eventSourceInitDict && eventSourceInitDict.options && isPlainObject(eventSourceInitDict.options)){ + for (var optName in eventSourceInitDict.options){ + var option = eventSourceInitDict.options[optName]; + if (option !== undefined) { + options[optName] = option; + } + } + } + req = (isSecure ? https : http).request(options, function (res) { // Handle HTTP redirects if (res.statusCode == 301 || res.statusCode == 307) { diff --git a/test/client_certs/cacert.crt b/test/client_certs/cacert.crt new file mode 100644 index 00000000..a37a23eb --- /dev/null +++ b/test/client_certs/cacert.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICBzCCAXACCQDe3kCVnwXlKTANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQGEwJV +UzELMAkGA1UECBMCVkExFDASBgNVBAoTC0V2ZW50U291cmNlMRYwFAYDVQQDEw1F +dmVudFNvdXJjZUNBMB4XDTE2MDIyOTIwMTMyNloXDTI2MDIyNjIwMTMyNlowSDEL +MAkGA1UEBhMCVVMxCzAJBgNVBAgTAlZBMRQwEgYDVQQKEwtFdmVudFNvdXJjZTEW +MBQGA1UEAxMNRXZlbnRTb3VyY2VDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC +gYEAujWjBi18dawJfTPMd1vtozoE0VRD5aP32d2UU3kps8nDfKlwcWcaYcKtGhlT +KXYKn4zrXd6wa5J3RdWEwukN5aAkLYDPCJX12w8KacDOoqdYyHi635QXXq1N+7rK +nDKaDwtg015fPsOumLDqk6x4VBpFDbjrcblT0ILqsurd0OMCAwEAATANBgkqhkiG +9w0BAQUFAAOBgQABgZZnsnsDsGctScBD7vSoTM9+aOetlUlPZx6N52ADe1L77rP7 +0rPxL/+yD3VGVTAhAHrtC1JpJpo+JBssuC2EkwZ2RWFU/rkYQBO7wbgRFAoEWylT +j3PaeDzUZumSL5ZuVyQC94XNCodNeWBDK+WzQbjPa0BPd6vflUnAb/stwg== +-----END CERTIFICATE----- diff --git a/test/client_certs/client_cert.crt b/test/client_certs/client_cert.crt new file mode 100644 index 00000000..201218cb --- /dev/null +++ b/test/client_certs/client_cert.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICAjCCAWsCCQDcY7oXL2AdXjANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQGEwJV +UzELMAkGA1UECBMCVkExFDASBgNVBAoTC0V2ZW50U291cmNlMRYwFAYDVQQDEw1F +dmVudFNvdXJjZUNBMB4XDTE2MDIyOTIwMTU0MVoXDTI2MDIyNjIwMTU0MVowQzEL +MAkGA1UEBhMCVVMxCzAJBgNVBAgTAlZBMRQwEgYDVQQKEwtFdmVudFNvdXJjZTER +MA8GA1UEAxMIdGVzdHVzZXIwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAPKw +mifd5tbJg5ZDkoabQsgqqu24MsNbJMtrbTkH/DtK12Qg6m5n8TR2aUt/CfpXBNkh +C40KzbFirkoTwLtLon39f6HFqaQCvdRZOb9e3SmHapm1W/ROx2M4L17DdKaVdkVn +7HA4zBL+kfZSI914dWTI4s0l9ohwJdVmBBBsmEO9AgMBAAEwDQYJKoZIhvcNAQEF +BQADgYEAdDfnGB9/n3Q2Hao4t8svM5s/Wyvy/sc8iZUn4R6PwFKSmeeuOS6NjkIy +rq59z3D4qHTES5RFoO7d8km884UcYCz6mq081hAm+Dlcd3uZhsWyboHCgkWlhVIS +XGEeNAUBir6aGvBkfU69HFdXfprTiTvHlt2NukwTPCYoqPQQH3w= +-----END CERTIFICATE----- diff --git a/test/client_certs/client_key.pem b/test/client_certs/client_key.pem new file mode 100644 index 00000000..bdaa1710 --- /dev/null +++ b/test/client_certs/client_key.pem @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,6040D9A5C438CD94 + +nIsbCSSu74WqCDkABopP4WfV1qt1Om0Se27KNWYQdPxBgD7APTcf2OBb3QZH5IpX +esGvGcCAfmE9VXmT9z3Kpwaloo1kZ3kdEEdYIH7QWc0mbIlMLghgvjfYy43LupoG +GD9Hlhvtpynhrc+VE+R3DKKboA8NnT//NuekPkiVdp5qVM7uKNLCiYrW2iKHZDiA +mRCbmWmLAC9YPou9mEu0wJxk+MULXjIjgls8IdCcNdcEJO6sRyzPOgBv/6WipGxq +15xYJViBUVl8W33JdkGWFTrlodJs49X7/AjHw55GqCixONrPzhzycsgFo8FOCROI +BqPHD5N/itGtMu61JGCmvBuXyhUYd9xiBrinalVOxJV+XUJWLGw7KurNQrPa/RwU +m9E0cSxwyYWEalpr0fTMpvo2NYcKmJLs30jj9G/Kpqs/7LWjrp7Tm/stHFSpa4Jm +Qt1nX7x8Vjt1cnMup0f/dK8eMsx4JsF0YqyklYObyJdJhe1akoz2YhSnD4CNE0T+ +oIoxrU74w6iK0PF33+w7sc2ZRODZdJcA09r9yZfh68yhGPkcp91koaZso+0uHlHw +sN204cy1jhS2s7EJ/J8abHdOnznEw1g/1YIPkhfgEh3ccpGCWPbpAHc12eth+fEl +SL5rmFrQtowic2lOYOabWbkP+ZDd5deSlTouOB/iYtsRZP7UINBlqcoYvThx40ba +yHKeFE1pitNB3oh3AVGGu8xbzObzjVC32s8caMZqr2QwIyk18oIGQPD4rhcbkRQ8 +WIeog03dLVEBa+qhgaOZX7zJrYX6eilRGBXmjZueV57VOl3pTQCnpg== +-----END RSA PRIVATE KEY----- diff --git a/test/client_certs/server_cert.crt b/test/client_certs/server_cert.crt new file mode 100644 index 00000000..45dd8031 --- /dev/null +++ b/test/client_certs/server_cert.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICAzCCAWwCCQDcY7oXL2AdXTANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQGEwJV +UzELMAkGA1UECBMCVkExFDASBgNVBAoTC0V2ZW50U291cmNlMRYwFAYDVQQDEw1F +dmVudFNvdXJjZUNBMB4XDTE2MDIyOTIwMTQyMVoXDTI2MDIyNjIwMTQyMVowRDEL +MAkGA1UEBhMCVVMxCzAJBgNVBAgTAlZBMRQwEgYDVQQKEwtFdmVudFNvdXJjZTES +MBAGA1UEAxMJbG9jYWxob3N0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJ +5BXHRbLlvlMGg19SQ1Bg4iDA8Tt9IJ4P/tocgwamQyCjFpngOi+uDmnXLZFf1x7P +ueXEtKVLLGlehcqM3LNS3Z4SUUc+OkcO2ztFZHjWell7FOuDTUuus3BjcFQDDNhd +GBNBKI79/oxRVjRFcDdMvTWG0r1UIALvlECTj8uwswIDAQABMA0GCSqGSIb3DQEB +BQUAA4GBAFqvIsJWnsV/drGNeuftEMG/zwD+5j8Xe9xCUIewMH5Er20/MXK0owLi +V3XI84LpVKi9hfUwJji91EW6Qi18Z4LKdA/bXvLdWwtZMCybYTTGKnLmUELhqIyn +VZbgEXyYpiUzRUnIotjbOwQIpP1aj+8Gys6DrHgBEbqrMuI6tiKF +-----END CERTIFICATE----- diff --git a/test/client_certs/server_key.pem b/test/client_certs/server_key.pem new file mode 100644 index 00000000..4818aaee --- /dev/null +++ b/test/client_certs/server_key.pem @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,5A4FE8B32AF89EBF + +Falz9rSy6LdpFlmXQKkmIGVzYqkmKJ8rA1E/xt5YFOGk7tO6kwCKm5/wOrWv3TrH +6v90CTNNJHerwFe7QinEAMiwKaLssR+YUxdNplJrYCbTTRrOo/DER0qVO8bw4yht +MawMDY1+pI1YyDpkwNt8QsVn80DFUBTqnXb2A+u0Pk+PaL/hji7t1piGD+yRkPsC +Fbx54F3IzZByFyhMQ2YoTzT8OHEyMDEzqxjla7DsWC9PGuFTtp7h6NWA4kT1FBb5 +kkEIYai/DuN/ng750+7eAVv6K3Fz+byb9t4VMpb3FHpreqk32D8StCVG1N1vUVK7 +gN8D3X1GBDRKmOEP/hSJOoDZj8W9UXq4SQ9GzRtyVGq2ARMr32P/e/EOq5L4tSy1 +WVJk0pBVIIUrY2Z0UO5tpK7+pkZvjOCWr68qnXIWcyEptyPqEiFZNqoztRbtaHyf +7m8FEDXSRBKQpFyi5p9AWut1hlaf6YyKbw3z8Gqfd1nqbzcqqH2jSN7WdZcJj0Ma +JWTMaFBT1wZYQ/mQleLk4TakQf+X/wlq/B9gfrEWR0Zkmc7tuXwDaMU8muQdXx6d +Olu73ICqrsajrmILX2tmJo2HsSvfNjxOXC/VORqVwD+Hp09p3UZnrxxNohCm8Qka +Dgghd+oU0o1RfdBa+c5ZL6l41ghk2a3zMi/GCGl+KprmMonlRIfUSkMc3tdRQiZV +GubJSwASXFxVDrIb64fDdyEryHFxKjyFeHOc9fFyP11TawLJgCKPaiqSnUivvvPr +PBei3pkaFiq9mpF3pF3GO7I6ENGpBmU0O/6rVh7hYtg= +-----END RSA PRIVATE KEY----- diff --git a/test/eventsource_test.js b/test/eventsource_test.js index 585c7174..a47f01fc 100644 --- a/test/eventsource_test.js +++ b/test/eventsource_test.js @@ -28,6 +28,19 @@ function createHttpsServer(callback) { configureServer(server, 'https', _port++, callback); } +function createHttpsServerWithClientAuth(callback) { + var options = { + key: fs.readFileSync(__dirname + '/client_certs/server_key.pem'), + cert: fs.readFileSync(__dirname + '/client_certs/server_cert.crt'), + ca: fs.readFileSync(__dirname + '/client_certs/cacert.crt'), + passphrase:'test1234$', + requestCert: true, + rejectAuthorized:true + }; + var server = https.createServer(options); + configureServer(server, 'https', _port++, callback); +} + function configureServer(server, protocol, port, callback) { var responses = []; @@ -511,6 +524,32 @@ describe('HTTPS Support', function () { }); }); +describe('HTTPS Client Certificate Support', function () { + it('uses client certificate for https urls', function (done) { + this.timeout(1500000); + createHttpsServerWithClientAuth(function (err, server) { + if(err) return done(err); + + server.on('request', writeEvents(["data: hello\n\n"])); + var es = new EventSource(server.url, + { + options:{ + key: fs.readFileSync(__dirname + '/client_certs/client_key.pem'), + cert: fs.readFileSync(__dirname + '/client_certs/client_cert.crt'), + ca: fs.readFileSync(__dirname + '/client_certs/cacert.crt'), + passphrase:'test1234$', + rejectUnauthorized: true + } + } + ); + es.onmessage = function (m) { + assert.equal("hello", m.data); + server.close(done); + } + }); + }); +}); + describe('Reconnection', function () { it('is attempted when server is down', function (done) { var es = new EventSource('http://localhost:' + _port); From 873788579e1760b6438fc3e09339de43824cc2d6 Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 15:54:58 +0200 Subject: [PATCH 07/16] Rename 'options' to 'https' and only allow whitelisted options --- lib/eventsource.js | 19 +++++++++++++++---- test/eventsource_test.js | 14 +++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/eventsource.js b/lib/eventsource.js index 19eae747..fe6557ff 100644 --- a/lib/eventsource.js +++ b/lib/eventsource.js @@ -5,6 +5,11 @@ var original = require('original') , http = require('http') , util = require('util'); +var httpsOptions = [ + 'pfx', 'key', 'passphrase', 'cert', 'ca', 'ciphers', + 'rejectUnauthorized', 'secureProtocol', 'servername' +] + /** * Creates a new EventSource object * @@ -76,6 +81,8 @@ function EventSource(url, eventSourceInitDict) { } } + // Legacy: this should be specified as `eventSourceInitDict.https.rejectUnauthorized`, + // but for now exists as a backwards-compatibility layer options.rejectUnauthorized = !(eventSourceInitDict && eventSourceInitDict.rejectUnauthorized == false); // If specify http proxy, make the request to sent to the proxy server, @@ -89,10 +96,14 @@ function EventSource(url, eventSourceInitDict) { options.port = proxy.port; } - //If specify options, include all properties in the http/https request options - if (eventSourceInitDict && eventSourceInitDict.options && isPlainObject(eventSourceInitDict.options)){ - for (var optName in eventSourceInitDict.options){ - var option = eventSourceInitDict.options[optName]; + // If https options are specified, merge them into the request options + if (eventSourceInitDict && eventSourceInitDict.https) { + for (var optName in eventSourceInitDict.https) { + if (httpsOptions.indexOf(optName) === -1) { + continue; + } + + var option = eventSourceInitDict.https[optName]; if (option !== undefined) { options[optName] = option; } diff --git a/test/eventsource_test.js b/test/eventsource_test.js index a47f01fc..022ad129 100644 --- a/test/eventsource_test.js +++ b/test/eventsource_test.js @@ -30,9 +30,9 @@ function createHttpsServer(callback) { function createHttpsServerWithClientAuth(callback) { var options = { - key: fs.readFileSync(__dirname + '/client_certs/server_key.pem'), - cert: fs.readFileSync(__dirname + '/client_certs/server_cert.crt'), - ca: fs.readFileSync(__dirname + '/client_certs/cacert.crt'), + key: fs.readFileSync(path.join(__dirname, 'client_certs', 'server_key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'client_certs', 'server_cert.crt')), + ca: fs.readFileSync(path.join(__dirname, 'client_certs', 'cacert.crt')), passphrase:'test1234$', requestCert: true, rejectAuthorized:true @@ -533,10 +533,10 @@ describe('HTTPS Client Certificate Support', function () { server.on('request', writeEvents(["data: hello\n\n"])); var es = new EventSource(server.url, { - options:{ - key: fs.readFileSync(__dirname + '/client_certs/client_key.pem'), - cert: fs.readFileSync(__dirname + '/client_certs/client_cert.crt'), - ca: fs.readFileSync(__dirname + '/client_certs/cacert.crt'), + https:{ + key: fs.readFileSync(path.join(__dirname, 'client_certs', 'client_key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'client_certs', 'client_cert.crt')), + ca: fs.readFileSync(path.join(__dirname, 'client_certs', 'cacert.crt')), passphrase:'test1234$', rejectUnauthorized: true } From 6a412bc3b14ddb8a73d411e62a3bd6e0df8657a4 Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 15:56:02 +0200 Subject: [PATCH 08/16] Move server certs to match client certs --- test/eventsource_test.js | 4 ++-- test/{fixtures => server_certs}/certificate.pem | 0 test/{fixtures => server_certs}/key.pem | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename test/{fixtures => server_certs}/certificate.pem (100%) rename test/{fixtures => server_certs}/key.pem (100%) diff --git a/test/eventsource_test.js b/test/eventsource_test.js index 022ad129..fa970389 100644 --- a/test/eventsource_test.js +++ b/test/eventsource_test.js @@ -21,8 +21,8 @@ function createServer(callback) { function createHttpsServer(callback) { var options = { - key: fs.readFileSync(path.join(__dirname, 'fixtures', 'key.pem')), - cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'certificate.pem')) + key: fs.readFileSync(path.join(__dirname, 'server_certs', 'key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'server_certs', 'certificate.pem')) }; var server = https.createServer(options); configureServer(server, 'https', _port++, callback); diff --git a/test/fixtures/certificate.pem b/test/server_certs/certificate.pem similarity index 100% rename from test/fixtures/certificate.pem rename to test/server_certs/certificate.pem diff --git a/test/fixtures/key.pem b/test/server_certs/key.pem similarity index 100% rename from test/fixtures/key.pem rename to test/server_certs/key.pem From 99f5f7bfb72a324a3dbe9e252bf83b8ca065deba Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 15:57:32 +0200 Subject: [PATCH 09/16] Update readme to reflect https option renaming --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 256f4423..1d32f94a 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,10 @@ var es = new EventSource(url, eventSourceInitDict); ### Allow unauthorized HTTPS requests By default, https requests that cannot be authorized will cause connection to fail and an exception -to be emitted. You can override this behaviour: +to be emitted. You can override this behaviour, along with other https options: ```javascript -var eventSourceInitDict = {rejectUnauthorized: false}; +var eventSourceInitDict = {https: {rejectUnauthorized: false}}; var es = new EventSource(url, eventSourceInitDict); ``` @@ -80,5 +80,5 @@ es.onerror = function (err) { You can define a `proxy` option for the HTTP request to be used. This is typically useful if you are behind a corporate firewall. ```javascript -var es = new EventSource(url, { proxy: 'http://your.proxy.com' }); +var es = new EventSource(url, {proxy: 'http://your.proxy.com'}); ``` From 4f1bed98dd1cf9f93bcace58e60a96669a342b3f Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 17:45:46 +0200 Subject: [PATCH 10/16] Use standard.js coding standard --- example/sse-client.js | 6 +- example/sse-server.js | 21 +- lib/eventsource-polyfill.js | 2 +- lib/eventsource.js | 289 +++++----- package.json | 8 +- test/eventsource_test.js | 1087 ++++++++++++++++++----------------- 6 files changed, 714 insertions(+), 699 deletions(-) diff --git a/example/sse-client.js b/example/sse-client.js index 6d8347e3..72c49a91 100644 --- a/example/sse-client.js +++ b/example/sse-client.js @@ -1,5 +1,5 @@ var EventSource = require('..') -var es = new EventSource('http://localhost:8080/sse'); +var es = new EventSource('http://localhost:8080/sse') es.addEventListener('server-time', function (e) { - console.log(e.data); -}); + console.log(e.data) +}) diff --git a/example/sse-server.js b/example/sse-server.js index 99afb860..ac4ed8cd 100644 --- a/example/sse-server.js +++ b/example/sse-server.js @@ -1,26 +1,27 @@ -var express = require('express'); +var express = require('express') var serveStatic = require('serve-static') var SSE = require('sse') var app = express() app.use(serveStatic(__dirname)) -server = app.listen(8080, function (err) { - if(err) throw err; - console.log("server ready on http://localhost:8080") -}); +var server = app.listen(8080, function (err) { + if (err) throw err + console.log('server ready on http://localhost:8080') +}) + var sse = new SSE(server) sse.on('connection', function (connection) { - console.log('new connection'); + console.log('new connection') var pusher = setInterval(function () { connection.send({ event: 'server-time', data: new Date().toTimeString() }) - }, 1000); + }, 1000) connection.on('close', function () { - console.log('lost connection'); - clearInterval(pusher); - }); + console.log('lost connection') + clearInterval(pusher) + }) }) diff --git a/lib/eventsource-polyfill.js b/lib/eventsource-polyfill.js index 0cd100e6..dcdcb66b 100644 --- a/lib/eventsource-polyfill.js +++ b/lib/eventsource-polyfill.js @@ -1,2 +1,2 @@ -window.EventSourcePolyfill = require('./eventsource'); +window.EventSourcePolyfill = require('./eventsource') window.EventSource = window.EventSource || window.EventSourcePolyfill diff --git a/lib/eventsource.js b/lib/eventsource.js index fe6557ff..160b0b06 100644 --- a/lib/eventsource.js +++ b/lib/eventsource.js @@ -1,9 +1,9 @@ var original = require('original') - , parse = require('url').parse - , events = require('events') - , https = require('https') - , http = require('http') - , util = require('util'); +var parse = require('url').parse +var events = require('events') +var https = require('https') +var http = require('http') +var util = require('util') var httpsOptions = [ 'pfx', 'key', 'passphrase', 'cert', 'ca', 'ciphers', @@ -17,250 +17,251 @@ var httpsOptions = [ * @param {Object} [eventSourceInitDict] extra init params. See README for details. * @api public **/ -function EventSource(url, eventSourceInitDict) { - var readyState = EventSource.CONNECTING; +function EventSource (url, eventSourceInitDict) { + var readyState = EventSource.CONNECTING Object.defineProperty(this, 'readyState', { get: function () { - return readyState; + return readyState } - }); + }) Object.defineProperty(this, 'url', { get: function () { - return url; + return url } - }); + }) - var self = this; - self.reconnectInterval = 1000; + var self = this + self.reconnectInterval = 1000 - function onConnectionClosed() { - if (readyState === EventSource.CLOSED) return; - readyState = EventSource.CONNECTING; - _emit('error', new Event('error')); + function onConnectionClosed () { + if (readyState === EventSource.CLOSED) return + readyState = EventSource.CONNECTING + _emit('error', new Event('error')) // The url may have been changed by a temporary // redirect. If that's the case, revert it now. if (reconnectUrl) { - url = reconnectUrl; - reconnectUrl = null; + url = reconnectUrl + reconnectUrl = null } setTimeout(function () { if (readyState !== EventSource.CONNECTING) { - return; + return } - connect(); - }, self.reconnectInterval); + connect() + }, self.reconnectInterval) } - var req; - var lastEventId = ''; + var req + var lastEventId = '' if (eventSourceInitDict && eventSourceInitDict.headers && eventSourceInitDict.headers['Last-Event-ID']) { - lastEventId = eventSourceInitDict.headers['Last-Event-ID']; - delete eventSourceInitDict.headers['Last-Event-ID']; + lastEventId = eventSourceInitDict.headers['Last-Event-ID'] + delete eventSourceInitDict.headers['Last-Event-ID'] } var discardTrailingNewline = false - , data = '' - , eventName = ''; + var data = '' + var eventName = '' - var reconnectUrl = null; + var reconnectUrl = null - function connect() { - - var options = parse(url); - var isSecure = options.protocol == 'https:'; - options.headers = { 'Cache-Control': 'no-cache', 'Accept': 'text/event-stream' }; - if (lastEventId) options.headers['Last-Event-ID'] = lastEventId; + function connect () { + var options = parse(url) + var isSecure = options.protocol === 'https:' + options.headers = { 'Cache-Control': 'no-cache', 'Accept': 'text/event-stream' } + if (lastEventId) options.headers['Last-Event-ID'] = lastEventId if (eventSourceInitDict && eventSourceInitDict.headers) { for (var i in eventSourceInitDict.headers) { - var header = eventSourceInitDict.headers[i]; + var header = eventSourceInitDict.headers[i] if (header) { - options.headers[i] = header; + options.headers[i] = header } } } // Legacy: this should be specified as `eventSourceInitDict.https.rejectUnauthorized`, // but for now exists as a backwards-compatibility layer - options.rejectUnauthorized = !(eventSourceInitDict && eventSourceInitDict.rejectUnauthorized == false); + options.rejectUnauthorized = !(eventSourceInitDict && !eventSourceInitDict.rejectUnauthorized) // If specify http proxy, make the request to sent to the proxy server, // and include the original url in path and Host headers if (eventSourceInitDict && eventSourceInitDict.proxy) { - var proxy = parse(eventSourceInitDict.proxy); - options.path = url; - options.headers.Host = options.host; - options.hostname = proxy.hostname; - options.host = proxy.host; - options.port = proxy.port; + var proxy = parse(eventSourceInitDict.proxy) + options.path = url + options.headers.Host = options.host + options.hostname = proxy.hostname + options.host = proxy.host + options.port = proxy.port } // If https options are specified, merge them into the request options if (eventSourceInitDict && eventSourceInitDict.https) { for (var optName in eventSourceInitDict.https) { if (httpsOptions.indexOf(optName) === -1) { - continue; + continue } - var option = eventSourceInitDict.https[optName]; + var option = eventSourceInitDict.https[optName] if (option !== undefined) { - options[optName] = option; + options[optName] = option } } } req = (isSecure ? https : http).request(options, function (res) { // Handle HTTP redirects - if (res.statusCode == 301 || res.statusCode == 307) { + if (res.statusCode === 301 || res.statusCode === 307) { if (!res.headers.location) { // Server sent redirect response without Location header. - _emit('error', new Event('error', {status: res.statusCode})); - return; + _emit('error', new Event('error', {status: res.statusCode})) + return } - if (res.statusCode == 307) reconnectUrl = url; - url = res.headers.location; - process.nextTick(connect); - return; + if (res.statusCode === 307) reconnectUrl = url + url = res.headers.location + process.nextTick(connect) + return } if (res.statusCode !== 200) { - _emit('error', new Event('error', {status: res.statusCode})); - return self.close(); + _emit('error', new Event('error', {status: res.statusCode})) + return self.close() } - readyState = EventSource.OPEN; - res.on('close', function() { - res.removeAllListeners('close'); - res.removeAllListeners('end'); - onConnectionClosed(); - }); + readyState = EventSource.OPEN + res.on('close', function () { + res.removeAllListeners('close') + res.removeAllListeners('end') + onConnectionClosed() + }) - res.on('end', function() { - res.removeAllListeners('close'); - res.removeAllListeners('end'); - onConnectionClosed(); - }); - _emit('open', new Event('open')); + res.on('end', function () { + res.removeAllListeners('close') + res.removeAllListeners('end') + onConnectionClosed() + }) + _emit('open', new Event('open')) // text/event-stream parser adapted from webkit's // Source/WebCore/page/EventSource.cpp - var buf = ''; + var buf = '' res.on('data', function (chunk) { - buf += chunk; + buf += chunk var pos = 0 - , length = buf.length; + var length = buf.length + while (pos < length) { if (discardTrailingNewline) { if (buf[pos] === '\n') { - ++pos; + ++pos } - discardTrailingNewline = false; + discardTrailingNewline = false } var lineLength = -1 - , fieldLength = -1 - , c; + var fieldLength = -1 + var c for (var i = pos; lineLength < 0 && i < length; ++i) { - c = buf[i]; + c = buf[i] if (c === ':') { if (fieldLength < 0) { - fieldLength = i - pos; + fieldLength = i - pos } } else if (c === '\r') { - discardTrailingNewline = true; - lineLength = i - pos; + discardTrailingNewline = true + lineLength = i - pos } else if (c === '\n') { - lineLength = i - pos; + lineLength = i - pos } } if (lineLength < 0) { - break; + break } - parseEventStreamLine(buf, pos, fieldLength, lineLength); + parseEventStreamLine(buf, pos, fieldLength, lineLength) - pos += lineLength + 1; + pos += lineLength + 1 } if (pos === length) { - buf = ''; + buf = '' } else if (pos > 0) { - buf = buf.slice(pos); + buf = buf.slice(pos) } - }); - }); + }) + }) - req.on('error', onConnectionClosed); - if (req.setNoDelay) req.setNoDelay(true); - req.end(); + req.on('error', onConnectionClosed) + if (req.setNoDelay) req.setNoDelay(true) + req.end() } - connect(); + connect() - function _emit() { + function _emit () { if (self.listeners(arguments[0]).length > 0) { - self.emit.apply(self, arguments); + self.emit.apply(self, arguments) } } this.close = function () { - if (readyState == EventSource.CLOSED) return; - readyState = EventSource.CLOSED; - if (req.abort) req.abort(); - if (req.xhr && req.xhr.abort) req.xhr.abort(); - }; + if (readyState === EventSource.CLOSED) return + readyState = EventSource.CLOSED + if (req.abort) req.abort() + if (req.xhr && req.xhr.abort) req.xhr.abort() + } - function parseEventStreamLine(buf, pos, fieldLength, lineLength) { + function parseEventStreamLine (buf, pos, fieldLength, lineLength) { if (lineLength === 0) { if (data.length > 0) { - var type = eventName || 'message'; + var type = eventName || 'message' _emit(type, new MessageEvent(type, { data: data.slice(0, -1), // remove trailing newline lastEventId: lastEventId, origin: original(url) - })); - data = ''; + })) + data = '' } - eventName = void 0; + eventName = void 0 } else if (fieldLength > 0) { var noValue = fieldLength < 0 - , step = 0 - , field = buf.slice(pos, pos + (noValue ? lineLength : fieldLength)); + var step = 0 + var field = buf.slice(pos, pos + (noValue ? lineLength : fieldLength)) if (noValue) { - step = lineLength; + step = lineLength } else if (buf[pos + fieldLength + 1] !== ' ') { - step = fieldLength + 1; + step = fieldLength + 1 } else { - step = fieldLength + 2; + step = fieldLength + 2 } - pos += step; + pos += step + var valueLength = lineLength - step - , value = buf.slice(pos, pos + valueLength); + var value = buf.slice(pos, pos + valueLength) if (field === 'data') { - data += value + '\n'; + data += value + '\n' } else if (field === 'event') { - eventName = value; + eventName = value } else if (field === 'id') { - lastEventId = value; + lastEventId = value } else if (field === 'retry') { - var retry = parseInt(value, 10); + var retry = parseInt(value, 10) if (!Number.isNaN(retry)) { - self.reconnectInterval = retry; + self.reconnectInterval = retry } } } } } -module.exports = EventSource; +module.exports = EventSource -util.inherits(EventSource, events.EventEmitter); +util.inherits(EventSource, events.EventEmitter) EventSource.prototype.constructor = EventSource; // make stacktraces readable ['open', 'error', 'message'].forEach(function (method) { @@ -271,9 +272,9 @@ EventSource.prototype.constructor = EventSource; // make stacktraces readable * @return {Mixed} the set function or undefined * @api private */ - get: function get() { - var listener = this.listeners(method)[0]; - return listener ? (listener._listener ? listener._listener : listener) : undefined; + get: function get () { + var listener = this.listeners(method)[0] + return listener ? (listener._listener ? listener._listener : listener) : undefined }, /** @@ -283,19 +284,19 @@ EventSource.prototype.constructor = EventSource; // make stacktraces readable * @return {Mixed} the set function or undefined * @api private */ - set: function set(listener) { - this.removeAllListeners(method); - this.addEventListener(method, listener); + set: function set (listener) { + this.removeAllListeners(method) + this.addEventListener(method, listener) } - }); -}); + }) +}) /** * Ready states */ -Object.defineProperty(EventSource, 'CONNECTING', { enumerable: true, value: 0}); -Object.defineProperty(EventSource, 'OPEN', { enumerable: true, value: 1}); -Object.defineProperty(EventSource, 'CLOSED', { enumerable: true, value: 2}); +Object.defineProperty(EventSource, 'CONNECTING', {enumerable: true, value: 0}) +Object.defineProperty(EventSource, 'OPEN', {enumerable: true, value: 1}) +Object.defineProperty(EventSource, 'CLOSED', {enumerable: true, value: 2}) /** * Emulates the W3C Browser based WebSocket interface using addEventListener. @@ -306,13 +307,13 @@ Object.defineProperty(EventSource, 'CLOSED', { enumerable: true, value: 2}); * @see http://dev.w3.org/html5/websockets/#the-websocket-interface * @api public */ -EventSource.prototype.addEventListener = function addEventListener(type, listener) { +EventSource.prototype.addEventListener = function addEventListener (type, listener) { if (typeof listener === 'function') { // store a reference so we can return the original function again - listener._listener = listener; - this.on(type, listener); + listener._listener = listener + this.on(type, listener) } -}; +} /** * Emulates the W3C Browser based WebSocket interface using removeEventListener. @@ -323,12 +324,12 @@ EventSource.prototype.addEventListener = function addEventListener(type, listene * @see http://dev.w3.org/html5/websockets/#the-websocket-interface * @api public */ -EventSource.prototype.removeEventListener = function removeEventListener(type, listener) { - if (typeof listener === 'function') { - listener._listener = undefined; - this.removeListener(type, listener); - } -}; +EventSource.prototype.removeEventListener = function removeEventListener (type, listener) { + if (typeof listener === 'function') { + listener._listener = undefined + this.removeListener(type, listener) + } +} /** * W3C Event @@ -336,12 +337,12 @@ EventSource.prototype.removeEventListener = function removeEventListener(type, l * @see http://www.w3.org/TR/DOM-Level-3-Events/#interface-Event * @api private */ -function Event(type, optionalProperties) { - Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true }); +function Event (type, optionalProperties) { + Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true }) if (optionalProperties) { for (var f in optionalProperties) { if (optionalProperties.hasOwnProperty(f)) { - Object.defineProperty(this, f, { writable: false, value: optionalProperties[f], enumerable: true }); + Object.defineProperty(this, f, { writable: false, value: optionalProperties[f], enumerable: true }) } } } @@ -353,11 +354,11 @@ function Event(type, optionalProperties) { * @see http://www.w3.org/TR/webmessaging/#event-definitions * @api private */ -function MessageEvent(type, eventInitDict) { - Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true }); +function MessageEvent (type, eventInitDict) { + Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true }) for (var f in eventInitDict) { if (eventInitDict.hasOwnProperty(f)) { - Object.defineProperty(this, f, { writable: false, value: eventInitDict[f], enumerable: true }); + Object.defineProperty(this, f, { writable: false, value: eventInitDict[f], enumerable: true }) } } } diff --git a/package.json b/package.json index d39c7f4d..d836bb3e 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,11 @@ "mocha": "^3.2.0", "serve-static": "^1.10.2", "sse": "^0.0.6", + "standard": "^10.0.2", "webpack": "^2.4.1" }, "scripts": { - "test": "mocha --reporter spec", + "test": "mocha --reporter spec && standard", "polyfill": "webpack lib/eventsource-polyfill.js example/eventsource-polyfill.js", "postpublish": "git push && git push --tags" }, @@ -46,5 +47,10 @@ }, "dependencies": { "original": "^1.0.0" + }, + "standard": { + "ignore": [ + "example/eventsource-polyfill.js" + ] } } diff --git a/test/eventsource_test.js b/test/eventsource_test.js index fa970389..36aa3ea3 100644 --- a/test/eventsource_test.js +++ b/test/eventsource_test.js @@ -1,664 +1,667 @@ +/* eslint-disable no-new */ var EventSource = require('../lib/eventsource') - , path = require('path') - , http = require('http') - , https = require('https') - , fs = require('fs') - , assert = require('assert') - , u = require('url'); - -var _port = 20000; -var servers = 0; +var path = require('path') +var http = require('http') +var https = require('https') +var fs = require('fs') +var mocha = require('mocha') +var assert = require('assert') +var u = require('url') + +var it = mocha.it +var describe = mocha.describe + +var _port = 20000 +var servers = 0 process.on('exit', function () { - if (servers != 0) { - console.error("************ Didn't kill all servers - there is still %d running.", servers); + if (servers > 0) { + console.error("************ Didn't kill all servers - there is still %d running.", servers) } -}); +}) -function createServer(callback) { - var server = http.createServer(); - configureServer(server, 'http', _port++, callback); +function createServer (callback) { + var server = http.createServer() + configureServer(server, 'http', _port++, callback) } -function createHttpsServer(callback) { +function createHttpsServer (callback) { var options = { key: fs.readFileSync(path.join(__dirname, 'server_certs', 'key.pem')), cert: fs.readFileSync(path.join(__dirname, 'server_certs', 'certificate.pem')) - }; - var server = https.createServer(options); - configureServer(server, 'https', _port++, callback); + } + var server = https.createServer(options) + configureServer(server, 'https', _port++, callback) } -function createHttpsServerWithClientAuth(callback) { +function createHttpsServerWithClientAuth (callback) { var options = { key: fs.readFileSync(path.join(__dirname, 'client_certs', 'server_key.pem')), cert: fs.readFileSync(path.join(__dirname, 'client_certs', 'server_cert.crt')), ca: fs.readFileSync(path.join(__dirname, 'client_certs', 'cacert.crt')), - passphrase:'test1234$', + passphrase: 'test1234$', requestCert: true, - rejectAuthorized:true - }; - var server = https.createServer(options); - configureServer(server, 'https', _port++, callback); + rejectAuthorized: true + } + var server = https.createServer(options) + configureServer(server, 'https', _port++, callback) } -function configureServer(server, protocol, port, callback) { - var responses = []; +function configureServer (server, protocol, port, callback) { + var responses = [] - var oldClose = server.close; - server.close = function() { + var oldClose = server.close + server.close = function () { responses.forEach(function (res) { - res.end(); - }); - servers--; - oldClose.apply(this, arguments); - }; + res.end() + }) + servers-- + oldClose.apply(this, arguments) + } server.on('request', function (req, res) { - responses.push(res); - }); + responses.push(res) + }) - server.url = protocol + '://localhost:' + port; + server.url = protocol + '://localhost:' + port - server.listen(port, function onOpen(err) { - servers++; - callback(err, server); - }); + server.listen(port, function onOpen (err) { + servers++ + callback(err, server) + }) } -function writeEvents(chunks) { +function writeEvents (chunks) { return function (req, res) { - res.writeHead(200, {'Content-Type': 'text/event-stream'}); + res.writeHead(200, {'Content-Type': 'text/event-stream'}) chunks.forEach(function (chunk) { - res.write(chunk); - }); - res.write(':'); // send a dummy comment to ensure that the head is flushed - }; + res.write(chunk) + }) + res.write(':') // send a dummy comment to ensure that the head is flushed + } } describe('Parser', function () { it('parses multibyte characters', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["id: 1\ndata: €豆腐\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['id: 1\ndata: €豆腐\n\n'])) + var es = new EventSource(server.url) es.onmessage = function (m) { - assert.equal("€豆腐", m.data); - server.close(done); - }; - }); - }); + assert.equal('€豆腐', m.data) + server.close(done) + } + }) + }) it('parses empty lines with multibyte characters', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["\n\n\n\nid: 1\ndata: 我現在都看實況不玩遊戲\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['\n\n\n\nid: 1\ndata: 我現在都看實況不玩遊戲\n\n'])) + var es = new EventSource(server.url) es.onmessage = function (m) { - assert.equal("我現在都看實況不玩遊戲", m.data); - server.close(done); - }; - }); - }); + assert.equal('我現在都看實況不玩遊戲', m.data) + server.close(done) + } + }) + }) it('parses one one-line message in one chunk', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: Hello\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['data: Hello\n\n'])) + var es = new EventSource(server.url) es.onmessage = function (m) { - assert.equal("Hello", m.data); - server.close(done); - }; - }); - }); + assert.equal('Hello', m.data) + server.close(done) + } + }) + }) it('parses one one-line message in two chunks', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: Hel", "lo\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['data: Hel', 'lo\n\n'])) + var es = new EventSource(server.url) es.onmessage = function (m) { - assert.equal("Hello", m.data); - server.close(done); - }; - }); - }); + assert.equal('Hello', m.data) + server.close(done) + } + }) + }) it('parses two one-line messages in one chunk', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: Hello\n\n", "data: World\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['data: Hello\n\n', 'data: World\n\n'])) + var es = new EventSource(server.url) - es.onmessage = first; + es.onmessage = first - function first(m) { - assert.equal("Hello", m.data); - es.onmessage = second; + function first (m) { + assert.equal('Hello', m.data) + es.onmessage = second } - function second(m) { - assert.equal("World", m.data); - server.close(done); + function second (m) { + assert.equal('World', m.data) + server.close(done) } - }); - }); + }) + }) it('parses one two-line message in one chunk', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: Hello\ndata:World\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['data: Hello\ndata:World\n\n'])) + var es = new EventSource(server.url) es.onmessage = function (m) { - assert.equal("Hello\nWorld", m.data); - server.close(done); - }; - }); - }); + assert.equal('Hello\nWorld', m.data) + server.close(done) + } + }) + }) it('parses really chopped up unicode data', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - var chopped = "data: Aslak\n\ndata: Hellesøy\n\n".split(""); - server.on('request', writeEvents(chopped)); - var es = new EventSource(server.url); + var chopped = 'data: Aslak\n\ndata: Hellesøy\n\n'.split('') + server.on('request', writeEvents(chopped)) + var es = new EventSource(server.url) - es.onmessage = first; + es.onmessage = first - function first(m) { - assert.equal("Aslak", m.data); - es.onmessage = second; + function first (m) { + assert.equal('Aslak', m.data) + es.onmessage = second } - function second(m) { - assert.equal("Hellesøy", m.data); - server.close(done); + function second (m) { + assert.equal('Hellesøy', m.data) + server.close(done) } - }); - }); + }) + }) it('accepts CRLF as separator', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - var chopped = "data: Aslak\r\n\r\ndata: Hellesøy\r\n\r\n".split(""); - server.on('request', writeEvents(chopped)); - var es = new EventSource(server.url); + var chopped = 'data: Aslak\r\n\r\ndata: Hellesøy\r\n\r\n'.split('') + server.on('request', writeEvents(chopped)) + var es = new EventSource(server.url) - es.onmessage = first; + es.onmessage = first - function first(m) { - assert.equal("Aslak", m.data); - es.onmessage = second; + function first (m) { + assert.equal('Aslak', m.data) + es.onmessage = second } - function second(m) { - assert.equal("Hellesøy", m.data); - server.close(done); + function second (m) { + assert.equal('Hellesøy', m.data) + server.close(done) } - }); - }); + }) + }) it('accepts CR as separator', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - var chopped = "data: Aslak\r\rdata: Hellesøy\r\r".split(""); - server.on('request', writeEvents(chopped)); - var es = new EventSource(server.url); + var chopped = 'data: Aslak\r\rdata: Hellesøy\r\r'.split('') + server.on('request', writeEvents(chopped)) + var es = new EventSource(server.url) - es.onmessage = first; + es.onmessage = first - function first(m) { - assert.equal("Aslak", m.data); - es.onmessage = second; + function first (m) { + assert.equal('Aslak', m.data) + es.onmessage = second } - function second(m) { - assert.equal("Hellesøy", m.data); - server.close(done); + function second (m) { + assert.equal('Hellesøy', m.data) + server.close(done) } - }); - }); + }) + }) it('delivers message with explicit event', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["event: greeting\ndata: Hello\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['event: greeting\ndata: Hello\n\n'])) + var es = new EventSource(server.url) es.addEventListener('greeting', function (m) { - assert.equal("Hello", m.data); - server.close(done); - }); - }); - }); + assert.equal('Hello', m.data) + server.close(done) + }) + }) + }) it('allows removal of event listeners', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["event: greeting\ndata: Hello\n\n", "event: greeting\ndata: World\n\n"])); - var es = new EventSource(server.url); - var numCalled = 0; + server.on('request', writeEvents(['event: greeting\ndata: Hello\n\n', 'event: greeting\ndata: World\n\n'])) + var es = new EventSource(server.url) + var numCalled = 0 - function onGreeting(m) { - numCalled++; - assert.equal("Hello", m.data); - es.removeEventListener('greeting', onGreeting, false); - process.nextTick(scheduleDisconnect); + function onGreeting (m) { + numCalled++ + assert.equal('Hello', m.data) + es.removeEventListener('greeting', onGreeting, false) + process.nextTick(scheduleDisconnect) } - function scheduleDisconnect() { - assert.equal(1, numCalled); - server.close(done); + function scheduleDisconnect () { + assert.equal(1, numCalled) + server.close(done) } - es.addEventListener('greeting', onGreeting, false); - }); - }); + es.addEventListener('greeting', onGreeting, false) + }) + }) it('ignores comments', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: Hello\n\n:nothing to see here\n\ndata: World\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['data: Hello\n\n:nothing to see here\n\ndata: World\n\n'])) + var es = new EventSource(server.url) - es.onmessage = first; + es.onmessage = first - function first(m) { - assert.equal("Hello", m.data); - es.onmessage = second; + function first (m) { + assert.equal('Hello', m.data) + es.onmessage = second } - function second(m) { - assert.equal("World", m.data); - server.close(done); + function second (m) { + assert.equal('World', m.data) + server.close(done) } - }); - }); + }) + }) it('ignores empty comments', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: Hello\n\n:\n\ndata: World\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['data: Hello\n\n:\n\ndata: World\n\n'])) + var es = new EventSource(server.url) - es.onmessage = first; + es.onmessage = first - function first(m) { - assert.equal("Hello", m.data); - es.onmessage = second; + function first (m) { + assert.equal('Hello', m.data) + es.onmessage = second } - function second(m) { - assert.equal("World", m.data); - server.close(done); + function second (m) { + assert.equal('World', m.data) + server.close(done) } - }); - }); + }) + }) it('does not ignore multilines strings', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: line one\ndata:\ndata: line two\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['data: line one\ndata:\ndata: line two\n\n'])) + var es = new EventSource(server.url) es.onmessage = function (m) { - assert.equal('line one\n\nline two', m.data); - server.close(done); - }; - }); - }); + assert.equal('line one\n\nline two', m.data) + server.close(done) + } + }) + }) it('does not ignore multilines strings even in data beginning', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data:\ndata:line one\ndata: line two\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['data:\ndata:line one\ndata: line two\n\n'])) + var es = new EventSource(server.url) es.onmessage = function (m) { - assert.equal('\nline one\nline two', m.data); - server.close(done); - }; - }); - }); + assert.equal('\nline one\nline two', m.data) + server.close(done) + } + }) + }) it('causes entire event to be ignored for empty event field', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["event:\n\ndata: Hello\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['event:\n\ndata: Hello\n\n'])) + var es = new EventSource(server.url) - var originalEmit = es.emit; + var originalEmit = es.emit es.emit = function (event) { - assert.ok(event === 'message' || event === 'newListener'); - return originalEmit.apply(this, arguments); - }; + assert.ok(event === 'message' || event === 'newListener') + return originalEmit.apply(this, arguments) + } es.onmessage = function (m) { - assert.equal('Hello', m.data); - server.close(done); - }; - }); - }); + assert.equal('Hello', m.data) + server.close(done) + } + }) + }) it('parses relatively huge messages efficiently', function (done) { - this.timeout(1000); + this.timeout(1000) createServer(function (err, server) { - if (err) return done(err); - var longMessage = "data: " + new Array(100000).join('a') + "\n\n"; - server.on('request', writeEvents([longMessage])); + if (err) return done(err) + var longMessage = 'data: ' + new Array(100000).join('a') + '\n\n' + server.on('request', writeEvents([longMessage])) - var es = new EventSource(server.url); + var es = new EventSource(server.url) es.onmessage = function () { - server.close(done); - }; - }); - }); -}); + server.close(done) + } + }) + }) +}) describe('HTTP Request', function () { it('passes cache-control: no-cache to server', function (done) { createServer(function (err, server) { - if (err) return done(err); + if (err) return done(err) server.on('request', function (req) { - assert.equal('no-cache', req.headers['cache-control']); - server.close(done); - }); - new EventSource(server.url); - }); - }); + assert.equal('no-cache', req.headers['cache-control']) + server.close(done) + }) + + EventSource(server.url) + }) + }) it('sets request headers', function (done) { - var server = createServer(function (err, server) { - if (err) return done(err); + createServer(function (err, server) { + if (err) return done(err) server.on('request', function (req) { - assert.equal(req.headers['user-agent'], 'test'); - assert.equal(req.headers['cookie'], 'test=test'); - assert.equal(req.headers['last-event-id'], '99'); - server.close(done); - }); + assert.equal(req.headers['user-agent'], 'test') + assert.equal(req.headers['cookie'], 'test=test') + assert.equal(req.headers['last-event-id'], '99') + server.close(done) + }) var headers = { 'User-Agent': 'test', 'Cookie': 'test=test', 'Last-Event-ID': '99' - }; - new EventSource(server.url, {headers: headers}); - }); - }); + } + new EventSource(server.url, {headers: headers}) + }) + }) it("does not set request headers that don't have a value", function (done) { - var server = createServer(function (err, server) { - if (err) return done(err); + createServer(function (err, server) { + if (err) return done(err) server.on('request', function (req) { - assert.equal(req.headers['user-agent'], 'test'); - assert.equal(req.headers['cookie'], 'test=test'); - assert.equal(req.headers['last-event-id'], '99'); - assert.equal(req.headers['X-Something'], undefined); - server.close(done); - }); + assert.equal(req.headers['user-agent'], 'test') + assert.equal(req.headers['cookie'], 'test=test') + assert.equal(req.headers['last-event-id'], '99') + assert.equal(req.headers['X-Something'], undefined) + server.close(done) + }) var headers = { 'User-Agent': 'test', 'Cookie': 'test=test', 'Last-Event-ID': '99', 'X-Something': null - }; + } assert.doesNotThrow( - function() { - new EventSource(server.url, {headers: headers}); + function () { + new EventSource(server.url, {headers: headers}) } - ); - }); + ) + }) }); [301, 307].forEach(function (status) { it('follows http ' + status + ' redirect', function (done) { - var redirectSuffix = '/foobar'; - var clientRequestedRedirectUrl = false; + var redirectSuffix = '/foobar' + var clientRequestedRedirectUrl = false createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) server.on('request', function (req, res) { if (req.url === '/') { res.writeHead(status, { 'Connection': 'Close', 'Location': server.url + redirectSuffix - }); - res.end(); + }) + res.end() } else if (req.url === redirectSuffix) { - clientRequestedRedirectUrl = true; - res.writeHead(200, {'Content-Type': 'text/event-stream'}); - res.end(); + clientRequestedRedirectUrl = true + res.writeHead(200, {'Content-Type': 'text/event-stream'}) + res.end() } - }); + }) - var es = new EventSource(server.url); + var es = new EventSource(server.url) es.onopen = function () { - assert.ok(clientRequestedRedirectUrl); - assert.equal(server.url + redirectSuffix, es.url); - server.close(done); - }; - }); - }); - + assert.ok(clientRequestedRedirectUrl) + assert.equal(server.url + redirectSuffix, es.url) + server.close(done) + } + }) + }) it('causes error event when response is ' + status + ' with missing location', function (done) { - var redirectSuffix = '/foobar'; - var clientRequestedRedirectUrl = false; createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) server.on('request', function (req, res) { res.writeHead(status, { 'Connection': 'Close' - }); - res.end(); - }); + }) + res.end() + }) - var es = new EventSource(server.url); + var es = new EventSource(server.url) es.onerror = function (err) { - assert.equal(err.status, status); - server.close(done); - }; - }); - }); + assert.equal(err.status, status) + server.close(done) + } + }) + }) }); [401, 403].forEach(function (status) { it('causes error event when response status is ' + status, function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) server.on('request', function (req, res) { - res.writeHead(status, {'Content-Type': 'text/html'}); - res.end(); - }); + res.writeHead(status, {'Content-Type': 'text/html'}) + res.end() + }) - var es = new EventSource(server.url); + var es = new EventSource(server.url) es.onerror = function (err) { - assert.equal(err.status, status); - server.close(done); - }; - }); - }); - }); -}); + assert.equal(err.status, status) + server.close(done) + } + }) + }) + }) +}) describe('HTTPS Support', function () { it('uses https for https urls', function (done) { createHttpsServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: hello\n\n"])); - var es = new EventSource(server.url, {rejectUnauthorized: false}); + server.on('request', writeEvents(['data: hello\n\n'])) + var es = new EventSource(server.url, {rejectUnauthorized: false}) es.onmessage = function (m) { - assert.equal("hello", m.data); - server.close(done); + assert.equal('hello', m.data) + server.close(done) } - }); - }); -}); + }) + }) +}) describe('HTTPS Client Certificate Support', function () { it('uses client certificate for https urls', function (done) { - this.timeout(1500000); + this.timeout(1500000) createHttpsServerWithClientAuth(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: hello\n\n"])); + server.on('request', writeEvents(['data: hello\n\n'])) var es = new EventSource(server.url, - { - https:{ - key: fs.readFileSync(path.join(__dirname, 'client_certs', 'client_key.pem')), - cert: fs.readFileSync(path.join(__dirname, 'client_certs', 'client_cert.crt')), - ca: fs.readFileSync(path.join(__dirname, 'client_certs', 'cacert.crt')), - passphrase:'test1234$', - rejectUnauthorized: true - } + { + https: { + key: fs.readFileSync(path.join(__dirname, 'client_certs', 'client_key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'client_certs', 'client_cert.crt')), + ca: fs.readFileSync(path.join(__dirname, 'client_certs', 'cacert.crt')), + passphrase: 'test1234$', + rejectUnauthorized: true } - ); + } + ) es.onmessage = function (m) { - assert.equal("hello", m.data); - server.close(done); + assert.equal('hello', m.data) + server.close(done) } - }); - }); -}); + }) + }) +}) describe('Reconnection', function () { it('is attempted when server is down', function (done) { - var es = new EventSource('http://localhost:' + _port); - es.reconnectInterval = 0; + var es = new EventSource('http://localhost:' + _port) + es.reconnectInterval = 0 es.onerror = function () { - es.onerror = null; + es.onerror = null createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: hello\n\n"])); + server.on('request', writeEvents(['data: hello\n\n'])) es.onmessage = function (m) { - assert.equal("hello", m.data); - server.close(done); + assert.equal('hello', m.data) + server.close(done) } - }); - }; - }); + }) + } + }) it('is attempted when server goes down after connection', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: hello\n\n"])); - var es = new EventSource(server.url); - es.reconnectInterval = 0; + server.on('request', writeEvents(['data: hello\n\n'])) + var es = new EventSource(server.url) + es.reconnectInterval = 0 es.onmessage = function (m) { - assert.equal("hello", m.data); + assert.equal('hello', m.data) server.close(function (err) { - if(err) return done(err); + if (err) return done(err) - var port = u.parse(es.url).port; + var port = u.parse(es.url).port configureServer(http.createServer(), 'http', port, function (err, server2) { - if(err) return done(err); + if (err) return done(err) - server2.on('request', writeEvents(["data: world\n\n"])); + server2.on('request', writeEvents(['data: world\n\n'])) es.onmessage = function (m) { - assert.equal("world", m.data); - server2.close(done); - }; - }); - }); - }; - }); - }); + assert.equal('world', m.data) + server2.close(done) + } + }) + }) + } + }) + }) it('is stopped when server goes down and eventsource is being closed', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: hello\n\n"])); - var es = new EventSource(server.url); - es.reconnectInterval = 0; + server.on('request', writeEvents(['data: hello\n\n'])) + var es = new EventSource(server.url) + es.reconnectInterval = 0 es.onmessage = function (m) { - assert.equal("hello", m.data); + assert.equal('hello', m.data) server.close(function (err) { - if(err) return done(err); + if (err) return done(err) // The server has closed down. es.onerror should now get called, // because es's remote connection was dropped. - }); - }; + }) + } es.onerror = function () { // We received an error because the remote connection was closed. // We close es, so we do not want es to reconnect. - es.close(); + es.close() - var port = u.parse(es.url).port; + var port = u.parse(es.url).port configureServer(http.createServer(), 'http', port, function (err, server2) { - if(err) return done(err); - server2.on('request', writeEvents(["data: world\n\n"])); + if (err) return done(err) + server2.on('request', writeEvents(['data: world\n\n'])) es.onmessage = function (m) { - return done(new Error("Unexpected message: " + m.data)); - }; + return done(new Error('Unexpected message: ' + m.data)) + } setTimeout(function () { // We have not received any message within 100ms, we can // presume this works correctly. - server2.close(done); - }, 100); - }); - }; - }); - }); + server2.close(done) + }, 100) + }) + } + }) + }) it('is not attempted when server responds with non-200', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) server.on('request', function (req, res) { - res.writeHead(204); - res.end(); - }); + res.writeHead(204) + res.end() + }) - var es = new EventSource(server.url); - es.reconnectInterval = 0; + var es = new EventSource(server.url) + es.reconnectInterval = 0 es.onerror = function (e) { - assert.equal(e.status, 204); + assert.equal(e.status, 204) server.close(function (err) { - if(err) return done(err); + if (err) return done(err) - var port = u.parse(es.url).port; + var port = u.parse(es.url).port configureServer(http.createServer(), 'http', port, function (err, server2) { - if(err) return done(err); + if (err) return done(err) // this will be verified by the readyState // going from CONNECTING to CLOSED, @@ -667,295 +670,299 @@ describe('Reconnection', function () { // it's next to impossible to write a fail-safe // test for this, though. var ival = setInterval(function () { - if (es.readyState == EventSource.CLOSED) { - clearInterval(ival); - server2.close(done); + if (es.readyState === EventSource.CLOSED) { + clearInterval(ival) + server2.close(done) } - }, 5); - }); - }); - }; - }); - }); + }, 5) + }) + }) + } + }) + }) it('sends Last-Event-ID http header when it has previously been passed in an event from the server', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(['id: 10\ndata: Hello\n\n'])); + server.on('request', writeEvents(['id: 10\ndata: Hello\n\n'])) - var es = new EventSource(server.url); - es.reconnectInterval = 0; + var es = new EventSource(server.url) + es.reconnectInterval = 0 es.onmessage = function () { server.close(function (err) { - if(err) return done(err); + if (err) return done(err) - var port = u.parse(es.url).port; + var port = u.parse(es.url).port configureServer(http.createServer(), 'http', port, function (err, server2) { + if (err) return done(err) + server2.on('request', function (req, res) { - assert.equal('10', req.headers['last-event-id']); - server2.close(done); - }); - }); - }); - }; - }); - }); + assert.equal('10', req.headers['last-event-id']) + server2.close(done) + }) + }) + }) + } + }) + }) it('sends correct Last-Event-ID http header when an initial Last-Event-ID header was specified in the constructor', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) server.on('request', function (req, res) { - assert.equal('9', req.headers['last-event-id']); - server.close(done); - }); + assert.equal('9', req.headers['last-event-id']) + server.close(done) + }) - new EventSource(server.url, {headers: {'Last-Event-ID': '9'}}); - }); - }); + new EventSource(server.url, {headers: {'Last-Event-ID': '9'}}) + }) + }) it('does not send Last-Event-ID http header when it has not been previously sent by the server', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(['data: Hello\n\n'])); + server.on('request', writeEvents(['data: Hello\n\n'])) - var es = new EventSource(server.url); - es.reconnectInterval = 0; + var es = new EventSource(server.url) + es.reconnectInterval = 0 es.onmessage = function () { server.close(function (err) { - if(err) return done(err); + if (err) return done(err) - var port = u.parse(es.url).port; + var port = u.parse(es.url).port configureServer(http.createServer(), 'http', port, function (err, server2) { + if (err) return done(err) + server2.on('request', function (req, res) { - assert.equal(undefined, req.headers['last-event-id']); - server2.close(done); - }); - }); - }); - }; - }); - }); -}); + assert.equal(undefined, req.headers['last-event-id']) + server2.close(done) + }) + }) + }) + } + }) + }) +}) describe('readyState', function () { it('has CONNECTING constant', function () { - assert.equal(0, EventSource.CONNECTING); - }); + assert.equal(0, EventSource.CONNECTING) + }) it('has OPEN constant', function () { - assert.equal(1, EventSource.OPEN); - }); + assert.equal(1, EventSource.OPEN) + }) it('has CLOSED constant', function () { - assert.equal(2, EventSource.CLOSED); - }); + assert.equal(2, EventSource.CLOSED) + }) it('is CONNECTING before connection has been established', function (done) { - var es = new EventSource('http://localhost:' + _port); - assert.equal(EventSource.CONNECTING, es.readyState); + var es = new EventSource('http://localhost:' + _port) + assert.equal(EventSource.CONNECTING, es.readyState) es.onerror = function () { - es.close(); - done(); + es.close() + done() } - }); + }) it('is CONNECTING when server has closed the connection', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents([])); - var es = new EventSource(server.url); - es.reconnectInterval = 0; + server.on('request', writeEvents([])) + var es = new EventSource(server.url) + es.reconnectInterval = 0 es.onopen = function (m) { server.close(function (err) { - if(err) return done(err); + if (err) return done(err) es.onerror = function () { - es.onerror = null; - assert.equal(EventSource.CONNECTING, es.readyState); - done(); - }; - }); - }; - }); - }); + es.onerror = null + assert.equal(EventSource.CONNECTING, es.readyState) + done() + } + }) + } + }) + }) it('is OPEN when connection has been established', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents([])); - var es = new EventSource(server.url); + server.on('request', writeEvents([])) + var es = new EventSource(server.url) es.onopen = function () { - assert.equal(EventSource.OPEN, es.readyState); - server.close(done); + assert.equal(EventSource.OPEN, es.readyState) + server.close(done) } - }); - }); + }) + }) it('is CLOSED after connection has been closed', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents([])); - var es = new EventSource(server.url); + server.on('request', writeEvents([])) + var es = new EventSource(server.url) es.onopen = function () { - es.close(); - assert.equal(EventSource.CLOSED, es.readyState); - server.close(done); + es.close() + assert.equal(EventSource.CLOSED, es.readyState) + server.close(done) } - }); - }); -}); + }) + }) +}) describe('Properties', function () { it('url exposes original request url', function () { - var url = 'http://localhost:' + _port; - var es = new EventSource(url); - assert.equal(url, es.url); - }); -}); + var url = 'http://localhost:' + _port + var es = new EventSource(url) + assert.equal(url, es.url) + }) +}) describe('Events', function () { it('calls onopen when connection is established', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents([])); - var es = new EventSource(server.url); + server.on('request', writeEvents([])) + var es = new EventSource(server.url) es.onopen = function (event) { - assert.equal(event.type, 'open'); - server.close(done); + assert.equal(event.type, 'open') + server.close(done) } - }); - }); + }) + }) it('supplies the correct origin', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: hello\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['data: hello\n\n'])) + var es = new EventSource(server.url) es.onmessage = function (event) { - assert.equal(event.origin, server.url); - server.close(done); + assert.equal(event.origin, server.url) + server.close(done) } - }); - }); + }) + }) it('emits open event when connection is established', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents([])); - var es = new EventSource(server.url); + server.on('request', writeEvents([])) + var es = new EventSource(server.url) es.addEventListener('open', function (event) { - assert.equal(event.type, 'open'); - server.close(done); - }); - }); - }); + assert.equal(event.type, 'open') + server.close(done) + }) + }) + }) it('does not double reconnect when connection is closed by server', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - var numConnections = 0; + var numConnections = 0 server.on('request', function (req, res) { - numConnections++; + numConnections++ writeEvents([])(req, res) if (numConnections > 2) done(new Error('reopening too many connections')) // destroy only the first connection - expected only 1 other reconnect - if (numConnections == 1){ - process.nextTick(function (){ + if (numConnections === 1) { + process.nextTick(function () { req.destroy() }) } - }); - var es = new EventSource(server.url); - }); - setTimeout(done, 1500); + }) + new EventSource(server.url) + }) + setTimeout(done, 1500) }) it('does not emit error when connection is closed by client', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents([])); - var es = new EventSource(server.url); + server.on('request', writeEvents([])) + var es = new EventSource(server.url) es.addEventListener('open', function () { - es.close(); + es.close() process.nextTick(function () { - server.close(done); - }); - }); + server.close(done) + }) + }) es.addEventListener('error', function () { - done(new Error('error should not be emitted')); - }); - }); - }); + done(new Error('error should not be emitted')) + }) + }) + }) it('populates message\'s lastEventId correctly when the last event has an associated id', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["id: 123\ndata: hello\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['id: 123\ndata: hello\n\n'])) + var es = new EventSource(server.url) es.onmessage = function (m) { - assert.equal(m.lastEventId, "123"); - server.close(done); - }; - }); - }); + assert.equal(m.lastEventId, '123') + server.close(done) + } + }) + }) it('populates message\'s lastEventId correctly when the last event doesn\'t have an associated id', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["id: 123\ndata: Hello\n\n", "data: World\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['id: 123\ndata: Hello\n\n', 'data: World\n\n'])) + var es = new EventSource(server.url) - es.onmessage = first; + es.onmessage = first - function first() { - es.onmessage = second; + function first () { + es.onmessage = second } - function second(m) { - assert.equal(m.data, "World"); - assert.equal(m.lastEventId, "123"); //expect to get back the previous event id - server.close(done); + function second (m) { + assert.equal(m.data, 'World') + assert.equal(m.lastEventId, '123') // expect to get back the previous event id + server.close(done) } - }); - }); + }) + }) it('populates messages with enumerable properties so they can be inspected via console.log().', function (done) { createServer(function (err, server) { - if(err) return done(err); + if (err) return done(err) - server.on('request', writeEvents(["data: World\n\n"])); - var es = new EventSource(server.url); + server.on('request', writeEvents(['data: World\n\n'])) + var es = new EventSource(server.url) es.onmessage = function (m) { - var enumerableAttributes = Object.keys(m); - assert.notEqual(enumerableAttributes.indexOf("data"), -1); - assert.notEqual(enumerableAttributes.indexOf("type"), -1); - server.close(done); - }; - }); - }); -}); + var enumerableAttributes = Object.keys(m) + assert.notEqual(enumerableAttributes.indexOf('data'), -1) + assert.notEqual(enumerableAttributes.indexOf('type'), -1) + server.close(done) + } + }) + }) +}) From d388bd94149c6cf573677e218425bf348a5f2ab7 Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 17:47:17 +0200 Subject: [PATCH 11/16] Add editorconfig to match coding standard --- .editorconfig | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..88f10ae8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# http://editorconfig.org +root = true + +[*] +# Use hard or soft tabs +indent_style = space + +# Size of a single indent +indent_size = tab + +# Number of columns representing a tab character +tab_width = 2 + +# Use line-feed as EOL indicator +end_of_line = lf + +# Use UTF-8 character encoding for all files +charset = utf-8 + +# Remove any whitespace characters preceding newline characters +trim_trailing_whitespace = true + +# Ensure file ends with a newline when saving +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false From f35e15d417f575c70e890c3929165e5fa2c3b49f Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 17:56:04 +0200 Subject: [PATCH 12/16] Add readystate constants on instances (closes #66) --- lib/eventsource.js | 4 ++++ test/eventsource_test.js | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/lib/eventsource.js b/lib/eventsource.js index 160b0b06..082656fd 100644 --- a/lib/eventsource.js +++ b/lib/eventsource.js @@ -298,6 +298,10 @@ Object.defineProperty(EventSource, 'CONNECTING', {enumerable: true, value: 0}) Object.defineProperty(EventSource, 'OPEN', {enumerable: true, value: 1}) Object.defineProperty(EventSource, 'CLOSED', {enumerable: true, value: 2}) +EventSource.prototype.CONNECTING = 0 +EventSource.prototype.OPEN = 1 +EventSource.prototype.CLOSED = 2 + /** * Emulates the W3C Browser based WebSocket interface using addEventListener. * diff --git a/test/eventsource_test.js b/test/eventsource_test.js index 36aa3ea3..2c25a4d0 100644 --- a/test/eventsource_test.js +++ b/test/eventsource_test.js @@ -762,6 +762,18 @@ describe('readyState', function () { assert.equal(2, EventSource.CLOSED) }) + it('has readystate constants on instances', function (done) { + var es = new EventSource('http://localhost:' + _port) + assert.equal(EventSource.CONNECTING, es.CONNECTING, 'constant CONNECTING missing/invalid') + assert.equal(EventSource.OPEN, es.OPEN, 'constant OPEN missing/invalid') + assert.equal(EventSource.CLOSED, es.CLOSED, 'constant CLOSED missing/invalid') + + es.onerror = function () { + es.close() + done() + } + }) + it('is CONNECTING before connection has been established', function (done) { var es = new EventSource('http://localhost:' + _port) assert.equal(EventSource.CONNECTING, es.readyState) From 9c56e3c232ba6d3a2672aee17251040bea6429ed Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 18:51:11 +0200 Subject: [PATCH 13/16] Add note on license to readme --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d32f94a..6d3ea9ed 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ You can use it with Node.js or as a browser polyfill for node ./example/sse-server.js node ./example/sse-client.js # Node.js client open http://localhost:8080 # Browser client - both native and polyfill - curl http://localhost:8080/sse # Enjoy the simplicity of SSE) + curl http://localhost:8080/sse # Enjoy the simplicity of SSE ## Browser Polyfill @@ -82,3 +82,8 @@ You can define a `proxy` option for the HTTP request to be used. This is typical ```javascript var es = new EventSource(url, {proxy: 'http://your.proxy.com'}); ``` + + +## License + +MIT-licensed. See LICENSE From 925e9814a0ff536e73fae5a85a027ecb20e58ce2 Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 19:27:58 +0200 Subject: [PATCH 14/16] Update HISTORY --- HISTORY.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 7a687fb0..f5fbdc3c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,10 @@ +# [1.0.0](https://github.com/EventSource/eventsource/compare/v0.2.3...v1.0.0) + +* Add missing `removeEventListener`-method. ([#51](https://github.com/EventSource/eventsource/pull/51) Yucheng Tu / Espen Hovlandsdal) +* Fix EventSource reconnecting on non-200 responses. ([af84476](https://github.com/EventSource/eventsource/commit/af84476b519a01e61b8c80727261df52ae40022c) Espen Hovlandsdal) +* Add ability to customize https options. ([#53](https://github.com/EventSource/eventsource/pull/53) Rafael Alfaro) +* Add readyState constants to EventSource instances. ([#66](https://github.com/EventSource/eventsource/pull/66) Espen Hovlandsdal) + # [0.2.3](https://github.com/EventSource/eventsource/compare/v0.2.2...v0.2.3) * Fix `onConnectionClosed` firing multiple times resulting in multiple connections. ([#61](https://github.com/EventSource/eventsource/pull/61) Phil Strong / Duncan Wong) From 3d17140fe9f0eb0551fbbce5660951924e0a41cf Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 19:33:24 +0200 Subject: [PATCH 15/16] Add nyc code coverage reporting --- .gitignore | 2 ++ package.json | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 417972db..9753109f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ npm-debug.log .DS_Store yarn.lock +.nyc_output +/coverage diff --git a/package.json b/package.json index d836bb3e..6c8ef7f4 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "devDependencies": { "express": "^4.13.4", "mocha": "^3.2.0", + "nyc": "^10.2.0", "serve-static": "^1.10.2", "sse": "^0.0.6", "standard": "^10.0.2", @@ -40,10 +41,11 @@ "scripts": { "test": "mocha --reporter spec && standard", "polyfill": "webpack lib/eventsource-polyfill.js example/eventsource-polyfill.js", - "postpublish": "git push && git push --tags" + "postpublish": "git push && git push --tags", + "coverage": "nyc --reporter=html --reporter=text _mocha --reporter spec" }, "engines": { - "node": ">=0.8.0" + "node": ">=0.12.0" }, "dependencies": { "original": "^1.0.0" From 3a4445f431560ef67c9cdbe74c238c94a5aecf3b Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 17 Apr 2017 19:35:27 +0200 Subject: [PATCH 16/16] 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c8ef7f4..a99515ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eventsource", - "version": "0.2.3", + "version": "1.0.0", "description": "W3C compliant EventSource client for Node.js and browser (polyfill)", "keywords": [ "eventsource",