Comparing version 0.3.5 to 0.3.6
@@ -1,5 +0,22 @@ | ||
v0.3.2 - Dec 11th 2011 | ||
v0.3.6 - Dec 15th 2011 | ||
====================== | ||
* Compile fix for Linux | ||
* Added option to let WebSocket.Server use an already existing http server [mmalecki] | ||
* Migrating various option structures to use options.js module [einaros] | ||
* Added a few more tests, options and handshake verifications to ensure that faulty connections are dealt with [einaros] | ||
* Code cleanups in Sender and Receiver, to ensure even faster parsing [einaros] | ||
v0.3.5 - Dec 13th 2011 | ||
====================== | ||
* Optimized Sender.js, Receiver.js and bufferutil.cc: | ||
* Apply loop-unrolling-like small block copies rather than use node.js Buffer#copy() (which is slow). | ||
* Mask blocks of data using combination of 32bit xor and loop-unrolling, instead of single bytes. | ||
* Keep pre-made send buffer for small transfers. | ||
* Leak fixes and code cleanups. | ||
v0.3.3 - Dec 12th 2011 | ||
====================== | ||
* Compile fix for Linux. | ||
* Rewrote parts of WebSocket.js, to avoid try/catch and thus avoid optimizer bailouts. | ||
@@ -6,0 +23,0 @@ |
@@ -18,21 +18,4 @@ /*! | ||
*/ | ||
var isNodeV4 = /^v0\.4/.test(process.version); | ||
var readUInt16BE = !isNodeV4 | ||
? Buffer.prototype.readUInt16BE | ||
: function(start) { | ||
var n = 0; | ||
for (var i = start; i < start + 2; ++i) { | ||
n = (i == 0) ? this[i] : (n * 256) + this[i]; | ||
} | ||
return n; | ||
}; | ||
var readUInt32BE = !isNodeV4 | ||
? Buffer.prototype.readUInt32BE | ||
: function(start) { | ||
var n = 0; | ||
for (var i = start; i < start + 4; ++i) { | ||
n = (i == 0) ? this[i] : (n * 256) + this[i]; | ||
} | ||
return n; | ||
}; | ||
@@ -50,3 +33,3 @@ /** | ||
}; | ||
this.overflow = null; | ||
this.overflow = []; | ||
this.bufferPool = new BufferPool(1024, function(db, length) { | ||
@@ -74,3 +57,3 @@ return db.used + length; | ||
self.expect(2, function(data) { | ||
opcodes['1'].getData(readUInt16BE.call(data, 0, true)); | ||
opcodes['1'].getData(readUInt16BE.call(data, 0)); | ||
}); | ||
@@ -80,7 +63,7 @@ } | ||
self.expect(8, function(data) { | ||
if (readUInt32BE.call(data, 0, true) != 0) { | ||
if (readUInt32BE.call(data, 0) != 0) { | ||
self.error('packets with length spanning more than 32 bit is currently not supported', 1008); | ||
return; | ||
} | ||
opcodes['1'].getData(readUInt32BE.call(data, 4, true)); | ||
opcodes['1'].getData(readUInt32BE.call(data, 4)); | ||
}); | ||
@@ -129,3 +112,3 @@ } | ||
self.expect(2, function(data) { | ||
opcodes['2'].getData(readUInt16BE.call(data, 0, true)); | ||
opcodes['2'].getData(readUInt16BE.call(data, 0)); | ||
}); | ||
@@ -135,3 +118,3 @@ } | ||
self.expect(8, function(data) { | ||
if (readUInt32BE.call(data, 0, true) != 0) { | ||
if (readUInt32BE.call(data, 0) != 0) { | ||
self.error('packets with length spanning more than 32 bit is currently not supported', 1008); | ||
@@ -208,3 +191,3 @@ return; | ||
} | ||
var code = data && data.length > 1 ? readUInt16BE.call(data, 0, true) : 1000; | ||
var code = data && data.length > 1 ? readUInt16BE.call(data, 0) : 1000; | ||
if (!ErrorCodes.isValidErrorCode(code)) { | ||
@@ -319,8 +302,6 @@ self.error('invalid error code', 1002); | ||
if (this.expectBuffer == null) { | ||
this.addToOverflow(data); | ||
this.overflow.push(data); | ||
return; | ||
} | ||
var toRead = Math.min(data.length, this.expectBuffer.length - this.expectOffset); | ||
// although ugly, this is a much faster approach for small buffers, | ||
// than calling copy() | ||
var dest = this.expectBuffer; | ||
@@ -330,2 +311,10 @@ var offset = this.expectOffset; | ||
default: data.copy(dest, offset, 0, toRead); break; | ||
case 16: dest[offset+15] = data[15]; | ||
case 15: dest[offset+14] = data[14]; | ||
case 14: dest[offset+13] = data[13]; | ||
case 13: dest[offset+12] = data[12]; | ||
case 12: dest[offset+11] = data[11]; | ||
case 11: dest[offset+10] = data[10]; | ||
case 10: dest[offset+9] = data[9]; | ||
case 9: dest[offset+8] = data[8]; | ||
case 8: dest[offset+7] = data[7]; | ||
@@ -342,3 +331,3 @@ case 7: dest[offset+6] = data[6]; | ||
if (toRead < data.length) { | ||
this.addToOverflow(data.slice(toRead, data.length)); | ||
this.overflow.push(data.slice(toRead, data.length)); | ||
} | ||
@@ -354,31 +343,2 @@ if (this.expectOffset == this.expectBuffer.length) { | ||
/** | ||
* Adds a piece of data to the overflow. | ||
* | ||
* @api private | ||
*/ | ||
Receiver.prototype.addToOverflow = function(data) { | ||
if (this.overflow == null) this.overflow = data; | ||
else { | ||
var prevOverflow = this.overflow; | ||
var dataLength = data.length; | ||
this.overflow = this.allocateFromPool(this.overflow.length + dataLength); | ||
prevOverflow.copy(this.overflow, 0); | ||
var dest = this.overflow; | ||
var offset = prevOverflow.length; | ||
switch (dataLength) { | ||
default: data.copy(dest, offset, 0, dataLength); break; | ||
case 8: dest[offset+7] = data[7]; | ||
case 7: dest[offset+6] = data[6]; | ||
case 6: dest[offset+5] = data[5]; | ||
case 5: dest[offset+4] = data[4]; | ||
case 4: dest[offset+3] = data[3]; | ||
case 3: dest[offset+2] = data[2]; | ||
case 2: dest[offset+1] = data[1]; | ||
case 1: dest[offset] = data[0]; | ||
} | ||
} | ||
} | ||
/** | ||
* Waits for a certain amount of bytes to be available, then fires a callback. | ||
@@ -397,6 +357,9 @@ * | ||
this.expectHandler = handler; | ||
if (this.overflow != null) { | ||
var toOverflow = this.overflow; | ||
this.overflow = null; | ||
this.add(toOverflow); | ||
var toRead = length; | ||
while (toRead > 0 && this.overflow.length > 0) { | ||
var buf = this.overflow.pop(); | ||
if (toRead < buf.length) this.overflow.push(buf.slice(toRead)) | ||
var read = Math.min(buf.length, toRead); | ||
this.add(buf.slice(0, read)); | ||
toRead -= read; | ||
} | ||
@@ -493,3 +456,3 @@ } | ||
this.expectHandler = null; | ||
this.overflow = null; | ||
this.overflow = []; | ||
this.currentMessage = []; | ||
@@ -536,2 +499,19 @@ } | ||
/** | ||
* Buffer utilities | ||
* | ||
*/ | ||
function readUInt16BE(start) { | ||
return (this[start]<<8) + | ||
this[start+1]; | ||
} | ||
function readUInt32BE(start) { | ||
return (this[start]<<24) + | ||
(this[start+1]<<16) + | ||
(this[start+2]<<8) + | ||
this[start+3]; | ||
} | ||
module.exports = Receiver; |
@@ -10,2 +10,3 @@ /*! | ||
, EventEmitter = events.EventEmitter | ||
, Options = require('options') | ||
, ErrorCodes = require('./ErrorCodes') | ||
@@ -15,27 +16,13 @@ , bufferUtil = new require('./BufferUtil').BufferUtil; | ||
/** | ||
* Node version 0.4 and 0.6 compatibility | ||
*/ | ||
var isNodeV4 = /^v0\.4/.test(process.version); | ||
var writeUInt16BE = !isNodeV4 | ||
? Buffer.prototype.writeUInt16BE | ||
: function(value, offset) { | ||
this[offset] = value >>> 8; | ||
this[offset + 1] = value % 256; | ||
}; | ||
var writeUInt32BE = !isNodeV4 | ||
? Buffer.prototype.writeUInt32BE | ||
: function(value, offset) { | ||
for (var i = offset + 3; i >= offset; --i) { | ||
this[i] = value & 0xff; | ||
value >>>= 8; | ||
} | ||
}; | ||
/** | ||
* HyBi Sender implementation | ||
*/ | ||
function Sender (socket, config) { | ||
this._sendCacheSize = (config && config.SendBufferCacheSize) ? config.SendBufferCacheSize : 65536; | ||
this._sendCache = new Buffer(this._sendCacheSize); | ||
function Sender (socket, options) { | ||
options = new Options({ | ||
sendBufferCacheSize: 65536 | ||
}).merge(options); | ||
if (options.value.sendBufferCacheSize > 0) { | ||
this._sendCacheSize = options.value.sendBufferCacheSize; | ||
this._sendCache = new Buffer(this._sendCacheSize); | ||
} | ||
this._socket = socket; | ||
@@ -64,3 +51,3 @@ this.firstFragment = true; | ||
var dataBuffer = new Buffer(2 + (data ? Buffer.byteLength(data) : 0)); | ||
writeUInt16BE.call(dataBuffer, code, 0, true); | ||
writeUInt16BE.call(dataBuffer, code, 0); | ||
if (dataBuffer.length > 2) dataBuffer.write(data, 2); | ||
@@ -162,7 +149,7 @@ var buf = this.frameData(0x8, dataBuffer, true, mask); | ||
case 126: | ||
writeUInt16BE.call(outputBuffer, dataLength, 2, true); | ||
writeUInt16BE.call(outputBuffer, dataLength, 2); | ||
break; | ||
case 127: | ||
writeUInt32BE.call(outputBuffer, 0, 2, true); | ||
writeUInt32BE.call(outputBuffer, dataLength, 6, true); | ||
writeUInt32BE.call(outputBuffer, 0, 2); | ||
writeUInt32BE.call(outputBuffer, dataLength, 6); | ||
} | ||
@@ -188,2 +175,14 @@ if (maskData) { | ||
function writeUInt16BE(value, offset) { | ||
this[offset] = (value & 0xff00)>>8; | ||
this[offset+1] = value & 0xff; | ||
} | ||
function writeUInt32BE(value, offset) { | ||
this[offset] = (value & 0xff000000)>>24; | ||
this[offset+1] = (value & 0xff0000)>>16; | ||
this[offset+2] = (value & 0xff00)>>8; | ||
this[offset+3] = value & 0xff; | ||
} | ||
function getArrayBuffer(array) { | ||
@@ -190,0 +189,0 @@ var l = array.byteLength |
@@ -13,2 +13,3 @@ /*! | ||
, fs = require('fs') | ||
, Options = require('options') | ||
, Sender = require('./Sender') | ||
@@ -34,2 +35,12 @@ , Receiver = require('./Receiver'); | ||
options = new Options({ | ||
protocolVersion: protocolVersion, | ||
protocol: null | ||
}).merge(options); | ||
Object.defineProperty(this, 'protocol', { | ||
value: options.value.protocol | ||
}); | ||
Object.defineProperty(this, 'protocolVersion', { | ||
value: options.value.protocolVersion | ||
}); | ||
this._state = 'connecting'; | ||
@@ -52,10 +63,12 @@ this._isServer = true; | ||
options = options || {}; | ||
options.origin = options.origin || null; | ||
options.protocolVersion = options.protocolVersion || protocolVersion; | ||
if (options.protocolVersion != 8 && options.protocolVersion != 13) { | ||
options = new Options({ | ||
origin: null, | ||
protocolVersion: protocolVersion, | ||
protocol: null | ||
}).merge(options); | ||
if (options.value.protocolVersion != 8 && options.value.protocolVersion != 13) { | ||
throw new Error('unsupported protocol version'); | ||
} | ||
var key = new Buffer(options.protocolVersion + '-' + Date.now()).toString('base64'); | ||
var key = new Buffer(options.value.protocolVersion + '-' + Date.now()).toString('base64'); | ||
var shasum = crypto.createHash('sha1'); | ||
@@ -82,6 +95,9 @@ shasum.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'); | ||
'Upgrade': 'websocket', | ||
'Sec-WebSocket-Version': options.protocolVersion, | ||
'Sec-WebSocket-Version': options.value.protocolVersion, | ||
'Sec-WebSocket-Key': key | ||
} | ||
}; | ||
if (options.value.protocol) { | ||
requestOptions.headers['Sec-WebSocket-Protocol'] = options.value.protocol; | ||
} | ||
if (isNodeV4) { | ||
@@ -92,5 +108,5 @@ requestOptions.path = (serverUrl.pathname || '/') + (serverUrl.search || ''); | ||
else requestOptions.path = serverUrl.path || '/'; | ||
if (options.origin) { | ||
if (options.protocolVersion < 13) requestOptions.headers['Sec-WebSocket-Origin'] = options.origin; | ||
else requestOptions.headers['Origin'] = options.origin; | ||
if (options.value.origin) { | ||
if (options.value.protocolVersion < 13) requestOptions.headers['Sec-WebSocket-Origin'] = options.value.origin; | ||
else requestOptions.headers['Origin'] = options.value.origin; | ||
} | ||
@@ -97,0 +113,0 @@ |
@@ -12,2 +12,3 @@ /*! | ||
, url = require('url') | ||
, Options = require('options') | ||
, WebSocket = require('./WebSocket'); | ||
@@ -20,9 +21,23 @@ | ||
function WebSocketServer(options, callback) { | ||
if (typeof options !== 'object' || typeof options.port == 'undefined') { | ||
throw new Error('port must be provided'); | ||
options = new Options({ | ||
host: '127.0.0.1', | ||
port: null, | ||
server: null, | ||
verifyOrigin: null | ||
}).merge(options); | ||
if (!options.value.port && !options.value.server) { | ||
throw new TypeError('`port` or a `server` must be provided'); | ||
} | ||
this._server = http.createServer(function (req, res) { | ||
res.writeHead(200, {'Content-Type': 'text/plain'}); | ||
res.end('okay'); | ||
}); | ||
if (!options.value.server) { | ||
this._server = http.createServer(function (req, res) { | ||
res.writeHead(200, {'Content-Type': 'text/plain'}); | ||
res.end('okay'); | ||
}); | ||
this._server.listen(options.value.port, options.value.host || '127.0.0.1', callback); | ||
} | ||
else { | ||
this._server = options.value.server; | ||
} | ||
this._clients = []; | ||
@@ -33,13 +48,35 @@ var self = this; | ||
}); | ||
this._server.on('upgrade', function(req, socket, upgradeHead) { | ||
if (typeof req.headers.upgrade === 'undefined' || req.headers.upgrade.toLowerCase() !== 'websocket') { | ||
socket.end(); | ||
self.emit('error', 'client connection with invalid headers'); | ||
abortConnection(socket, 400, 'Bad Request'); | ||
return; | ||
} | ||
// verify key presence | ||
if (!req.headers['sec-websocket-key']) { | ||
socket.end(); | ||
self.emit('error', 'websocket key is missing'); | ||
abortConnection(socket, 400, 'Bad Request'); | ||
return; | ||
} | ||
// verify version | ||
var version = parseInt(req.headers['sec-websocket-version']); | ||
if ([8, 13].indexOf(version) === -1) { | ||
abortConnection(socket, 400, 'Bad Request'); | ||
return; | ||
} | ||
// verify origin | ||
var origin = version < 13 ? | ||
req.headers['sec-websocket-origin'] : | ||
req.headers['origin']; | ||
if (typeof options.value.verifyOrigin == 'function') { | ||
if (!options.value.verifyOrigin(origin)) { | ||
abortConnection(socket, 401, 'Unauthorized'); | ||
return; | ||
} | ||
} | ||
var protocol = req.headers['sec-websocket-protocol']; | ||
// calc key | ||
@@ -57,3 +94,5 @@ var key = req.headers['sec-websocket-key']; | ||
]; | ||
if (typeof protocol != 'undefined') { | ||
headers['Sec-WebSocket-Protocol'] = protocol; | ||
} | ||
try { | ||
@@ -64,3 +103,2 @@ socket.write(headers.concat('', '').join('\r\n')); | ||
try { socket.end(); } catch (_) {} | ||
self.emit('error', 'socket error: ' + e); | ||
return; | ||
@@ -70,5 +108,6 @@ } | ||
socket.setNoDelay(true); | ||
self._socket = socket; | ||
var client = new WebSocket(Array.prototype.slice.call(arguments, 0)); | ||
var client = new WebSocket(Array.prototype.slice.call(arguments, 0), { | ||
protocolVersion: version, | ||
protocol: protocol | ||
}); | ||
self._clients.push(client); | ||
@@ -85,5 +124,2 @@ client.on('open', function() { | ||
}); | ||
this._server.listen(options.port, '127.0.0.1', function() { | ||
if (typeof callback == 'function') callback(); | ||
}); | ||
this.__defineGetter__('clients', function() { | ||
@@ -118,7 +154,5 @@ return self._clients; | ||
this._server.close(); | ||
this._socket.end(); | ||
} | ||
finally { | ||
this._server = null; | ||
this._socket = null; | ||
} | ||
@@ -129,1 +163,18 @@ if (error) throw error; | ||
module.exports = WebSocketServer; | ||
/** | ||
* Entirely private apis, | ||
* which may or may not be bound to a sepcific WebSocket instance. | ||
*/ | ||
function abortConnection(socket, code, name) { | ||
try { | ||
var response = [ | ||
'HTTP/1.1 ' + code + ' ' + name, | ||
'Content-type: text/html' | ||
]; | ||
socket.write(response.concat('', '').join('\r\n')); | ||
socket.end(); | ||
} | ||
catch (e) {} | ||
} |
@@ -5,3 +5,3 @@ { | ||
"description": "simple and very fast websocket protocol client for node.js", | ||
"version": "0.3.5", | ||
"version": "0.3.6", | ||
"repository": { | ||
@@ -11,2 +11,6 @@ "type": "git", | ||
}, | ||
"contributors": [ | ||
{ "name": "Einar Otto Stangvik", "email": "einaros@gmail.com" }, | ||
{ "name": "Maciej Małecki", "email": "maciej.malecki@notimplemented.org" } | ||
], | ||
"bin": { | ||
@@ -23,3 +27,4 @@ "wscat": "./bin/wscat" | ||
"dependencies": { | ||
"commander": "0.5.0" | ||
"commander": "0.5.0", | ||
"options": "latest" | ||
}, | ||
@@ -26,0 +31,0 @@ "devDependencies": { |
var assert = require('assert') | ||
, http = require('http') | ||
, WebSocket = require('../') | ||
@@ -52,2 +53,7 @@ , WebSocketServer = WebSocket.Server | ||
}) | ||
it('uses passed server object', function () { | ||
var srv = http.createServer() | ||
, wss = new WebSocketServer({server: srv}); | ||
wss._server.should.equal(srv); | ||
}); | ||
}) | ||
@@ -83,2 +89,135 @@ | ||
it('works with a http server', function (done) { | ||
var srv = http.createServer(); | ||
srv.listen(++port, function () { | ||
var wss = new WebSocketServer({server: srv}); | ||
var ws = new WebSocket('ws://localhost:' + port); | ||
wss.on('connection', function(client) { | ||
wss.close(); | ||
done(); | ||
}); | ||
}); | ||
}) | ||
it('does not accept connections with no sec-websocket-key', function(done) { | ||
var wss = new WebSocketServer({port: ++port}, function() { | ||
var options = { | ||
port: port, | ||
host: '127.0.0.1', | ||
headers: { | ||
'Connection': 'Upgrade', | ||
'Upgrade': 'websocket' | ||
} | ||
}; | ||
var req = http.request(options); | ||
req.end(); | ||
req.on('response', function(res) { | ||
res.statusCode.should.eql(400); | ||
wss.close(); | ||
done(); | ||
}); | ||
}); | ||
wss.on('error', function() {}); | ||
}) | ||
it('does not accept connections with no sec-websocket-version', function(done) { | ||
var wss = new WebSocketServer({port: ++port}, function() { | ||
var options = { | ||
port: port, | ||
host: '127.0.0.1', | ||
headers: { | ||
'Connection': 'Upgrade', | ||
'Upgrade': 'websocket', | ||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==' | ||
} | ||
}; | ||
var req = http.request(options); | ||
req.end(); | ||
req.on('response', function(res) { | ||
res.statusCode.should.eql(400); | ||
wss.close(); | ||
done(); | ||
}); | ||
}); | ||
wss.on('error', function() {}); | ||
}) | ||
it('does not accept connections with invalid sec-websocket-version', function(done) { | ||
var wss = new WebSocketServer({port: ++port}, function() { | ||
var options = { | ||
port: port, | ||
host: '127.0.0.1', | ||
headers: { | ||
'Connection': 'Upgrade', | ||
'Upgrade': 'websocket', | ||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', | ||
'Sec-WebSocket-Version': 12 | ||
} | ||
}; | ||
var req = http.request(options); | ||
req.end(); | ||
req.on('response', function(res) { | ||
res.statusCode.should.eql(400); | ||
wss.close(); | ||
done(); | ||
}); | ||
}); | ||
wss.on('error', function() {}); | ||
}) | ||
it('does not accept connections with invalid sec-websocket-origin (8)', function(done) { | ||
var wss = new WebSocketServer({port: ++port, verifyOrigin: function(o) { | ||
o.should.eql('http://foobar.com'); | ||
return false; | ||
}}, function() { | ||
var options = { | ||
port: port, | ||
host: '127.0.0.1', | ||
headers: { | ||
'Connection': 'Upgrade', | ||
'Upgrade': 'websocket', | ||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', | ||
'Sec-WebSocket-Version': 8, | ||
'Sec-WebSocket-Origin': 'http://foobar.com' | ||
} | ||
}; | ||
var req = http.request(options); | ||
req.end(); | ||
req.on('response', function(res) { | ||
res.statusCode.should.eql(401); | ||
wss.close(); | ||
done(); | ||
}); | ||
}); | ||
wss.on('error', function() {}); | ||
}) | ||
it('does not accept connections with invalid origin', function(done) { | ||
var wss = new WebSocketServer({port: ++port, verifyOrigin: function(o) { | ||
o.should.eql('http://foobar.com'); | ||
return false; | ||
}}, function() { | ||
var options = { | ||
port: port, | ||
host: '127.0.0.1', | ||
headers: { | ||
'Connection': 'Upgrade', | ||
'Upgrade': 'websocket', | ||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', | ||
'Sec-WebSocket-Version': 13, | ||
'Origin': 'http://foobar.com' | ||
} | ||
}; | ||
var req = http.request(options); | ||
req.end(); | ||
req.on('response', function(res) { | ||
res.statusCode.should.eql(401); | ||
wss.close(); | ||
done(); | ||
}); | ||
}); | ||
wss.on('error', function() {}); | ||
}) | ||
it('can send data', function(done) { | ||
@@ -98,2 +237,24 @@ var wss = new WebSocketServer({port: ++port}, function() { | ||
it('tracks the client protocol', function(done) { | ||
var wss = new WebSocketServer({port: ++port}, function() { | ||
var ws = new WebSocket('ws://localhost:' + port, {protocol: 'hi'}); | ||
}); | ||
wss.on('connection', function(client) { | ||
client.protocol.should.eql('hi'); | ||
wss.close(); | ||
done(); | ||
}); | ||
}) | ||
it('tracks the client protocolVersion', function(done) { | ||
var wss = new WebSocketServer({port: ++port}, function() { | ||
var ws = new WebSocket('ws://localhost:' + port, {protocolVersion: 8}); | ||
}); | ||
wss.on('connection', function(client) { | ||
client.protocolVersion.should.eql(8); | ||
wss.close(); | ||
done(); | ||
}); | ||
}) | ||
describe('#clients', function() { | ||
@@ -100,0 +261,0 @@ it('returns a list of connected clients', function(done) { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
130640
32
3349
2
4
+ Addedoptions@latest
+ Addedoptions@0.0.6(transitive)