diff --git a/cometd-nodejs-client.d.ts b/cometd-nodejs-client.d.ts index 0b74f9a..6df6e32 100644 --- a/cometd-nodejs-client.d.ts +++ b/cometd-nodejs-client.d.ts @@ -19,9 +19,15 @@ export interface HttpProxy { excludes?: string[]; } +export interface Cookies { + storeCookie?(uri: any, header: string, callback: (failure: Error | null, cookie: any) => void): void; + retrieveCookies?(context: any, uri: any, callback: (failure: Error | null, cookies: string[]) => void): void; +} + export interface Options { logLevel?: 'debug' | 'info'; httpProxy?: HttpProxy; + cookies?: Cookies; } export function adapt(options?: Options): void; diff --git a/cometd-nodejs-client.js b/cometd-nodejs-client.js index a03380a..f39ebb3 100644 --- a/cometd-nodejs-client.js +++ b/cometd-nodejs-client.js @@ -56,17 +56,82 @@ module.exports = { return /^https/i.test(uri.protocol); } + function _storeServerCookie(uri, header, callback) { + if (options && options.cookies && options.cookies.storeCookie) { + options.cookies.storeCookie(uri, header, callback); + } else { + const host = uri.hostname; + let jar = _globalCookies[host]; + if (jar === undefined) { + _globalCookies[host] = jar = {}; + } + const parts = header.split(';'); + // Ignore domain, path, expiration, etc. + const nameValue = parts[0].trim(); + const equal = nameValue.indexOf('='); + if (equal > 0) { + const name = nameValue.substring(0, equal); + jar[name] = nameValue; + } + callback(); + } + } + + function _retrieveServerCookies(context, uri, callback) { + if (options && options.cookies && options.cookies.retrieveCookies) { + options.cookies.retrieveCookies(context, uri, callback); + } else { + let globalCookies = context && context.cookieStore; + if (!globalCookies) { + globalCookies = _globalCookies; + } + let cookies = []; + const jar = globalCookies[uri.hostname]; + if (jar) { + for (let name in jar) { + if (jar.hasOwnProperty(name)) { + cookies.push(jar[name]); + } + } + } + callback(null, cookies); + } + } + + function _asyncForEach(array, index, operation, callback) { + while (index < array.length) { + let complete = false; + let proceed = false; + operation(array[index], () => { + complete = true; + if (proceed) { + ++index; + _asyncForEach(array, index, operation, callback); + } + }); + if (complete) { + ++index; + } else { + proceed = true; + break; + } + } + if (index === array.length) { + callback(); + } + } + // Bare minimum XMLHttpRequest implementation that works with CometD. window.XMLHttpRequest = function() { const _localCookies = {}; let _config; let _request; - function _storeCookie(cookieStore, value) { + function _storeCookie(value) { const host = _config.hostname; - let jar = cookieStore[host]; + let jar = _localCookies[host]; if (jar === undefined) { - cookieStore[host] = jar = {}; + _localCookies[host] = jar = {}; } const cookies = value.split(';'); for (let i = 0; i < cookies.length; ++i) { @@ -78,16 +143,13 @@ module.exports = { } } - function _concatCookies(cookieStore) { - let cookies = ''; - const jar = cookieStore[_config.hostname]; + function _retrieveCookies() { + const cookies = []; + const jar = _localCookies[_config.hostname]; if (jar) { for (let name in jar) { if (jar.hasOwnProperty(name)) { - if (cookies) { - cookies += '; '; - } - cookies += jar[name]; + cookies.push(jar[name]); } } } @@ -144,83 +206,81 @@ module.exports = { this.setRequestHeader = (name, value) => { if (/^cookie$/i.test(name)) { - _storeCookie(_localCookies, value) + _storeCookie(value); } else { _config.headers[name] = value; } }; this.send = data => { - let globalCookies = this.context && this.context.cookieStore; - if (!globalCookies) { - globalCookies = _globalCookies; - } - const cookies1 = _concatCookies(globalCookies); - const cookies2 = _concatCookies(_localCookies); - const delim = (cookies1 && cookies2) ? '; ' : ''; - const cookies = cookies1 + delim + cookies2; - if (cookies) { - _config.headers['Cookie'] = cookies; - } + _retrieveServerCookies(this.context, _config, (x, cookies) => { + const cookies1 = x ? '' : cookies.join('; '); + const cookies2 = _retrieveCookies().join('; '); + const delim = (cookies1 && cookies2) ? '; ' : ''; + const allCookies = cookies1 + delim + cookies2; + if (allCookies) { + _config.headers['Cookie'] = allCookies; + } - const http = _secure(_config) ? https : httpc; - _request = http.request(_config, response => { - let success = false; - this.status = response.statusCode; - this.statusText = response.statusMessage; - this.readyState = window.XMLHttpRequest.HEADERS_RECEIVED; - const headers = response.headers; - for (let name in headers) { - if (headers.hasOwnProperty(name)) { - if (/^set-cookie$/i.test(name)) { - const header = headers[name]; - for (let i = 0; i < header.length; ++i) { - const whole = header[i]; - const parts = whole.split(';'); - const cookie = parts[0]; - _storeCookie(globalCookies, cookie); + const http = _secure(_config) ? https : httpc; + _request = http.request(_config, response => { + let success = false; + this.status = response.statusCode; + this.statusText = response.statusMessage; + this.readyState = window.XMLHttpRequest.HEADERS_RECEIVED; + const setCookies = []; + const headers = response.headers; + for (let name in headers) { + if (headers.hasOwnProperty(name)) { + if (/^set-cookie$/i.test(name)) { + const header = headers[name]; + setCookies.push.apply(setCookies, header); } } } - } - response.on('data', chunk => { - this.readyState = window.XMLHttpRequest.LOADING; - this.responseText += chunk; - }); - response.on('end', () => { - success = true; - this.readyState = window.XMLHttpRequest.DONE; - if (this.onload) { - this.onload(); - } + _asyncForEach(setCookies, 0, (element, callback) => { + _storeServerCookie(_config, element, callback); + }, () => { + response.on('data', chunk => { + this.readyState = window.XMLHttpRequest.LOADING; + this.responseText += chunk; + }); + response.on('end', () => { + success = true; + this.readyState = window.XMLHttpRequest.DONE; + if (this.onload) { + this.onload(); + } + }); + response.on('close', () => { + if (!success) { + this.readyState = window.XMLHttpRequest.DONE; + if (this.onerror) { + this.onerror(); + } + } + }); + }); }); - response.on('close', () => { - if (!success) { + ['abort', 'aborted', 'error'].forEach(event => { + _request.on(event, x => { this.readyState = window.XMLHttpRequest.DONE; - if (this.onerror) { - this.onerror(); + if (x) { + const error = x.message; + if (error) { + this.statusText = error; + } } - } - }); - }); - ['abort', 'aborted', 'error'].forEach(event => { - _request.on(event, x => { - this.readyState = window.XMLHttpRequest.DONE; - if (x) { - const error = x.message; - if (error) { - this.statusText = error; + if (this.onerror) { + this.onerror(x); } - } - if (this.onerror) { - this.onerror(x); - } + }); }); + if (data) { + _request.write(data); + } + _request.end(); }); - if (data) { - _request.write(data); - } - _request.end(); }; this.abort = () => { diff --git a/package.json b/package.json index a560451..0aba638 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "url": "https://github.com/cometd/cometd-nodejs-client.git" }, "scripts": { - "test": "tsc && mocha --exit" + "test": "mocha --exit --require ts-node/register --extension ts --extension js" }, "dependencies": { "cometd": ">=3.1.2", @@ -28,9 +28,12 @@ "ws": ">=7.2.0" }, "devDependencies": { - "mocha": "latest", - "typescript": "latest", + "@types/mocha": "latest", "@types/node": "latest", - "@types/mocha": "latest" + "@types/tough-cookie": "^4.0.1", + "mocha": "latest", + "tough-cookie": "latest", + "ts-node": "latest", + "typescript": "latest" } } diff --git a/test/cookies.js b/test/cookies.ts similarity index 59% rename from test/cookies.js rename to test/cookies.ts index 05f1393..a52453e 100644 --- a/test/cookies.js +++ b/test/cookies.ts @@ -13,20 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -'use strict'; - -const assert = require('assert'); -const cometd = require('..'); -const http = require('http'); +import * as assert from 'assert'; +import * as nodeCometD from '..'; +import * as http from 'http'; +import {AddressInfo} from 'net'; +import * as tough from 'tough-cookie'; describe('cookies', () => { - let _runtime; - let _server; - - beforeEach(() => { - cometd.adapt(); - _runtime = global.window; - }); + let _server: http.Server; afterEach(() => { if (_server) { @@ -35,26 +29,28 @@ describe('cookies', () => { }); it('receives, stores and sends cookie', done => { + nodeCometD.adapt(); const cookie = 'a=b'; _server = http.createServer((request, response) => { - if (/\/1$/.test(request.url)) { + const uri: string = request.url || '/'; + if (/\/1$/.test(uri)) { response.setHeader('Set-Cookie', cookie); response.end(); - } else if (/\/2$/.test(request.url)) { + } else if (/\/2$/.test(uri)) { assert.strictEqual(request.headers['cookie'], cookie); response.end(); } }); _server.listen(0, 'localhost', () => { - const port = _server.address().port; + const port = (_server.address() as AddressInfo).port console.log('listening on localhost:' + port); const uri = 'http://localhost:' + port; - const xhr1 = new _runtime.XMLHttpRequest(); + const xhr1 = new window.XMLHttpRequest(); xhr1.open('GET', uri + '/1'); xhr1.onload = () => { assert.strictEqual(xhr1.status, 200); - const xhr2 = new _runtime.XMLHttpRequest(); + const xhr2 = new window.XMLHttpRequest(); xhr2.open('GET', uri + '/2'); xhr2.onload = () => { assert.strictEqual(xhr2.status, 200); @@ -67,35 +63,37 @@ describe('cookies', () => { }); it('sends multiple cookies', done => { + nodeCometD.adapt(); const cookie1 = 'a=b'; const cookie2 = 'c=d'; const cookies = cookie1 + '; ' + cookie2; _server = http.createServer((request, response) => { - if (/\/1$/.test(request.url)) { + const uri: string = request.url || '/'; + if (/\/1$/.test(uri)) { response.setHeader('Set-Cookie', cookie1); response.end(); - } else if (/\/2$/.test(request.url)) { + } else if (/\/2$/.test(uri)) { response.setHeader('Set-Cookie', cookie2); response.end(); - } else if (/\/3$/.test(request.url)) { + } else if (/\/3$/.test(uri)) { assert.strictEqual(request.headers['cookie'], cookies); response.end(); } }); _server.listen(0, 'localhost', () => { - const port = _server.address().port; + const port = (_server.address() as AddressInfo).port console.log('listening on localhost:' + port); const uri = 'http://localhost:' + port; - const xhr1 = new _runtime.XMLHttpRequest(); + const xhr1 = new window.XMLHttpRequest(); xhr1.open('GET', uri + '/1'); xhr1.onload = () => { assert.strictEqual(xhr1.status, 200); - const xhr2 = new _runtime.XMLHttpRequest(); + const xhr2 = new window.XMLHttpRequest(); xhr2.open('GET', uri + '/2'); xhr2.onload = () => { assert.strictEqual(xhr2.status, 200); - const xhr3 = new _runtime.XMLHttpRequest(); + const xhr3 = new window.XMLHttpRequest(); xhr3.open('GET', uri + '/3'); xhr3.onload = () => { assert.strictEqual(xhr3.status, 200); @@ -110,45 +108,47 @@ describe('cookies', () => { }); it('handles cookies from different hosts', done => { + nodeCometD.adapt(); const cookieA = 'a=b'; const cookieB = 'b=c'; _server = http.createServer((request, response) => { - if (/\/hostA\//.test(request.url)) { - if (/\/1$/.test(request.url)) { + const uri: string = request.url || '/'; + if (/\/hostA\//.test(uri)) { + if (/\/1$/.test(uri)) { response.setHeader('Set-Cookie', cookieA); response.end(); - } else if (/\/2$/.test(request.url)) { + } else if (/\/2$/.test(uri)) { assert.strictEqual(request.headers['cookie'], cookieA); response.end(); } - } else if (/\/hostB\//.test(request.url)) { - if (/\/1$/.test(request.url)) { + } else if (/\/hostB\//.test(uri)) { + if (/\/1$/.test(uri)) { response.setHeader('Set-Cookie', cookieB); response.end(); - } else if (/\/2$/.test(request.url)) { + } else if (/\/2$/.test(uri)) { assert.strictEqual(request.headers['cookie'], cookieB); response.end(); } } }); _server.listen(0, 'localhost', () => { - const port = _server.address().port; + const port = (_server.address() as AddressInfo).port console.log('listening on localhost:' + port); - const xhrA1 = new _runtime.XMLHttpRequest(); + const xhrA1 = new window.XMLHttpRequest(); xhrA1.open('GET', 'http://localhost:' + port + '/hostA/1'); xhrA1.onload = () => { assert.strictEqual(xhrA1.status, 200); - const xhrA2 = new _runtime.XMLHttpRequest(); + const xhrA2 = new window.XMLHttpRequest(); xhrA2.open('GET', 'http://localhost:' + port + '/hostA/2'); xhrA2.onload = () => { assert.strictEqual(xhrA2.status, 200); - const xhrB1 = new _runtime.XMLHttpRequest(); + const xhrB1 = new window.XMLHttpRequest(); xhrB1.open('GET', 'http://127.0.0.1:' + port + '/hostB/1'); xhrB1.onload = () => { assert.strictEqual(xhrB1.status, 200); - const xhrB2 = new _runtime.XMLHttpRequest(); + const xhrB2 = new window.XMLHttpRequest(); xhrB2.open('GET', 'http://127.0.0.1:' + port + '/hostB/2'); xhrB2.onload = () => { assert.strictEqual(xhrB2.status, 200); @@ -165,11 +165,13 @@ describe('cookies', () => { }); it('handles cookie sent multiple times', done => { + nodeCometD.adapt(); const cookieName = 'a'; const cookieValue = 'b'; const cookie = cookieName + '=' + cookieValue; _server = http.createServer((request, response) => { - if (/\/verify$/.test(request.url)) { + const uri: string = request.url || '/'; + if (/\/verify$/.test(uri)) { assert.strictEqual(request.headers['cookie'], cookie); response.end(); } else { @@ -178,18 +180,18 @@ describe('cookies', () => { } }); _server.listen(0, 'localhost', () => { - const port = _server.address().port; + const port = (_server.address() as AddressInfo).port console.log('listening on localhost:' + port); - const xhr1 = new _runtime.XMLHttpRequest(); + const xhr1 = new window.XMLHttpRequest(); xhr1.open('GET', 'http://localhost:' + port + '/1'); xhr1.onload = () => { assert.strictEqual(xhr1.status, 200); - const xhr2 = new _runtime.XMLHttpRequest(); + const xhr2 = new window.XMLHttpRequest(); xhr2.open('GET', 'http://localhost:' + port + '/2'); xhr2.onload = () => { assert.strictEqual(xhr2.status, 200); - const xhr3 = new _runtime.XMLHttpRequest(); + const xhr3 = new window.XMLHttpRequest(); xhr3.open('GET', 'http://localhost:' + port + '/verify'); xhr3.onload = () => { assert.strictEqual(xhr1.status, 200); @@ -204,17 +206,19 @@ describe('cookies', () => { }); it('handles cookies as request headers', done => { + nodeCometD.adapt(); _server = http.createServer((request, response) => { - const cookies = request.headers['cookie']; - if (/\/1$/.test(request.url)) { + const cookies = request.headers['cookie'] || ''; + const uri: string = request.url || '/'; + if (/\/1$/.test(uri)) { response.setHeader('Set-Cookie', 'a=b'); response.end(); - } else if (/\/2$/.test(request.url)) { + } else if (/\/2$/.test(uri)) { assert.ok(cookies.indexOf('a=b') >= 0); assert.ok(cookies.indexOf('c=d') >= 0); assert.ok(cookies.indexOf('e=f') >= 0); response.end(); - } else if (/\/3$/.test(request.url)) { + } else if (/\/3$/.test(uri)) { assert.ok(cookies.indexOf('a=b') >= 0); assert.ok(cookies.indexOf('c=d') < 0); assert.ok(cookies.indexOf('e=f') < 0); @@ -222,19 +226,19 @@ describe('cookies', () => { } }); _server.listen(0, 'localhost', () => { - const port = _server.address().port; + const port = (_server.address() as AddressInfo).port console.log('listening on localhost:' + port); - const xhr1 = new _runtime.XMLHttpRequest(); + const xhr1 = new window.XMLHttpRequest(); xhr1.open('GET', 'http://localhost:' + port + '/1'); xhr1.onload = () => { assert.strictEqual(xhr1.status, 200); - const xhr2 = new _runtime.XMLHttpRequest(); + const xhr2 = new window.XMLHttpRequest(); xhr2.open('GET', 'http://localhost:' + port + '/2'); xhr2.setRequestHeader('cookie', 'c=d; e=f'); xhr2.onload = () => { assert.strictEqual(xhr2.status, 200); - const xhr3 = new _runtime.XMLHttpRequest(); + const xhr3 = new window.XMLHttpRequest(); xhr3.open('GET', 'http://localhost:' + port + '/3'); xhr3.onload = () => { assert.strictEqual(xhr3.status, 200); @@ -247,4 +251,66 @@ describe('cookies', () => { xhr1.send(); }); }); + + it('allows custom cookie handling', done => { + const cookieJar = new tough.CookieJar(); + nodeCometD.adapt({ + cookies: { + storeCookie(uri: any, header: string, callback: (failure: Error | null, cookie: any) => void) { + const cookie = tough.Cookie.parse(header); + if (cookie) { + cookieJar.setCookie(cookie, uri, + // Test both sync and async callbacks. + header.startsWith('a=1') ? callback : () => setTimeout(callback, 0)); + } else { + callback(null, cookie); + } + }, + retrieveCookies(context: any, uri: any, callback: (failure: Error | null, cookies: string[]) => void) { + cookieJar.getCookies(uri, (x: any, r: tough.Cookie[]) => { + const result: string[] = x ? [] : r.map(c => c.cookieString()); + callback(x, result); + }); + } + } + }); + + _server = http.createServer((request, response) => { + const cookies = request.headers['cookie'] || ''; + const uri: string = request.url || '/'; + if (/\/set$/.test(uri)) { + response.setHeader('Set-Cookie', ['a=1; Path=/', 'b=2; Path=/b']); + } else if (/\/b$/.test(uri)) { + assert.ok(cookies.indexOf('a=1') >= 0); + assert.ok(cookies.indexOf('b=2') >= 0); + } else { + assert.ok(cookies.indexOf('a=1') >= 0); + } + response.end(); + }); + _server.listen(0, 'localhost', () => { + const port = (_server.address() as AddressInfo).port + console.log('listening on localhost:' + port); + + const xhr1 = new window.XMLHttpRequest(); + xhr1.open('GET', 'http://localhost:' + port + '/set'); + xhr1.onload = () => { + assert.strictEqual(xhr1.status, 200); + const xhr2 = new window.XMLHttpRequest(); + xhr2.open('GET', 'http://localhost:' + port + '/a'); + xhr2.onload = () => { + assert.strictEqual(xhr2.status, 200); + const xhr3 = new window.XMLHttpRequest(); + xhr3.open('GET', 'http://localhost:' + port + '/b'); + xhr3.onload = () => { + assert.strictEqual(xhr3.status, 200); + done(); + }; + xhr3.send(); + }; + xhr2.send(); + }; + xhr1.send(); + }); + }); }); diff --git a/test/ts_client.ts b/test/ts_client.ts index bb749ba..09b3999 100644 --- a/test/ts_client.ts +++ b/test/ts_client.ts @@ -13,17 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import nodeCometD = require('..'); -import http = require('http'); +import * as nodeCometD from '..'; +import * as jsCometD from 'cometd'; +import * as http from 'http'; import {AddressInfo} from 'net'; describe('typescript client', () => { - let _cometd: any; let _server: http.Server; beforeEach(() => { nodeCometD.adapt(); - _cometd = require('cometd'); }); afterEach(() => { @@ -57,13 +56,13 @@ describe('typescript client', () => { const port = (_server.address() as AddressInfo).port console.log('listening on localhost:' + port); - const cometd = new _cometd.CometD(); + const cometd: jsCometD.CometD = new jsCometD.CometD(); cometd.websocketEnabled = false; cometd.configure({ url: 'http://localhost:' + port + '/cometd', logLevel: 'info' }); - cometd.handshake((r: any) => { + cometd.handshake((r: jsCometD.Message) => { if (r.successful) { done(); }