diff --git a/Changes.md b/Changes.md index d5a0c8ded..6f5614242 100644 --- a/Changes.md +++ b/Changes.md @@ -7,6 +7,7 @@ you spot any mistakes. ## HEAD * Add `connectTimeout` option to specify a timeout for establishing a connection #726 +* SSL support #481 ## v2.0.1 diff --git a/Readme.md b/Readme.md index 0a865a1b2..d4f6c6955 100644 --- a/Readme.md +++ b/Readme.md @@ -167,6 +167,9 @@ issue [#501](https://github.com/felixge/node-mysql/issues/501). (Default: `'fals with this, it exposes you to SQL injection attacks. (Default: `false`) * `flags`: List of connection flags to use other than the default ones. It is also possible to blacklist default ones. For more information, check [Connection Flags](#connection-flags). +* `ssl`: object with ssl parameters ( same format as [crypto.createCredentials](http://nodejs.org/api/crypto.html#crypto_crypto_createcredentials_details) argument ) + or a string containing name of ssl profile. Currently only 'Amazon RDS' profile is bundled, containing CA from https://rds.amazonaws.com/doc/rds-ssl-ca-cert.pem + In addition to passing these options as an object, you can also use a url string. For example: diff --git a/fixtures/ssl-profiles.json b/fixtures/ssl-profiles.json new file mode 100644 index 000000000..6dddc5b8a --- /dev/null +++ b/fixtures/ssl-profiles.json @@ -0,0 +1,4 @@ +{ + "Amazon RDS": {"ca":"-----BEGIN CERTIFICATE-----\nMIIDQzCCAqygAwIBAgIJAOd1tlfiGoEoMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRMw\nEQYDVQQKEwpBbWF6b24uY29tMQwwCgYDVQQLEwNSRFMxHDAaBgNVBAMTE2F3cy5h\nbWF6b24uY29tL3Jkcy8wHhcNMTAwNDA1MjI0NDMxWhcNMTUwNDA0MjI0NDMxWjB1\nMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2Vh\ndHRsZTETMBEGA1UEChMKQW1hem9uLmNvbTEMMAoGA1UECxMDUkRTMRwwGgYDVQQD\nExNhd3MuYW1hem9uLmNvbS9yZHMvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB\ngQDKhXGU7tizxUR5WaFoMTFcxNxa05PEjZaIOEN5ctkWrqYSRov0/nOMoZjqk8bC\nmed9vPFoQGD0OTakPs0jVe3wwmR735hyVwmKIPPsGlaBYj1O6llIpZeQVyupNx56\nUzqtiLaDzh1KcmfqP3qP2dInzBfJQKjiRudo1FWnpPt33QIDAQABo4HaMIHXMB0G\nA1UdDgQWBBT/H3x+cqSkR/ePSIinPtc4yWKe3DCBpwYDVR0jBIGfMIGcgBT/H3x+\ncqSkR/ePSIinPtc4yWKe3KF5pHcwdTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh\nc2hpbmd0b24xEDAOBgNVBAcTB1NlYXR0bGUxEzARBgNVBAoTCkFtYXpvbi5jb20x\nDDAKBgNVBAsTA1JEUzEcMBoGA1UEAxMTYXdzLmFtYXpvbi5jb20vcmRzL4IJAOd1\ntlfiGoEoMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAvguZy/BDT66x\nGfgnJlyQwnFSeVLQm9u/FIvz4huGjbq9dqnD6h/Gm56QPFdyMEyDiZWaqY6V08lY\nLTBNb4kcIc9/6pc0/ojKciP5QJRm6OiZ4vgG05nF4fYjhU7WClUx7cxq1fKjNc2J\nUCmmYqgiVkAGWRETVo+byOSDZ4swb10=\n-----END CERTIFICATE-----\n"} + +} diff --git a/lib/Connection.js b/lib/Connection.js index 8f75b6327..d04fbfff7 100644 --- a/lib/Connection.js +++ b/lib/Connection.js @@ -64,11 +64,19 @@ Connection.prototype.connect = function(cb) { ? Net.createConnection(this.config.socketPath) : Net.createConnection(this.config); - // Node v0.10+ Switch socket into "old mode" (Streams2) - this._socket.on("data",function() {}); - - this._socket.pipe(this._protocol); - this._protocol.pipe(this._socket); + var connection = this; + this._protocol.on('data', function(data) { + connection._socket.write(data); + }); + this._socket.on('data', function(data) { + connection._protocol.write(data); + }); + this._protocol.on('end', function() { + connection._socket.end() + }); + this._socket.on('end', function(err) { + connection._protocol.end(); + }); this._socket.on('error', this._handleNetworkError.bind(this)); this._socket.on('connect', this._handleProtocolConnect.bind(this)); @@ -200,6 +208,49 @@ Connection.prototype.format = function(sql, values) { return SqlString.format(sql, values, this.config.stringifyObjects, this.config.timezone); }; + +Connection.prototype._startTLS = function(onSecure) { + + var crypto = require('crypto'); + var tls = require('tls'); + var sslProfiles, sslProfileName; + if (typeof this.config.ssl == 'string') { + sslProfileName = this.config.ssl; + sslProfiles = require('../fixtures/ssl-profiles.json'); + this.config.ssl = sslProfiles[this.config.ssl]; + if (!this.config.ssl) + throw new Error('Unknown SSL profile for ' + sslProfileName); + } + + // before TLS: + // _socket <-> _protocol + // after: + // _socket <-> securePair.encrypted <-> securePair.cleartext <-> _protocol + + var credentials = crypto.createCredentials({ + key: this.config.ssl.key, + cert: this.config.ssl.cert, + passphrase: this.config.ssl.passphrase, + ca: this.config.ssl.ca + }); + + var securePair = tls.createSecurePair(credentials, false); + + securePair.encrypted.pipe(this._socket); + securePair.cleartext.pipe(this._protocol); + + // TODO: change to unpipe/pipe (does not work for some reason. Streams1/2 conflict?) + this._socket.removeAllListeners('data'); + this._protocol.removeAllListeners('data'); + this._socket.on('data', function(data) { + securePair.encrypted.write(data); + }); + this._protocol.on('data', function(data) { + securePair.cleartext.write(data); + }); + securePair.on('secure', onSecure); +}; + Connection.prototype._handleConnectTimeout = function() { if (this._socket) { this._socket.setTimeout(0); diff --git a/lib/ConnectionConfig.js b/lib/ConnectionConfig.js index d0fb4f6e6..20dfaa241 100644 --- a/lib/ConnectionConfig.js +++ b/lib/ConnectionConfig.js @@ -27,6 +27,7 @@ function ConnectionConfig(options) { this.flags = options.flags || ''; this.queryFormat = options.queryFormat; this.pool = options.pool || undefined; + this.ssl = options.ssl || undefined; this.multipleStatements = options.multipleStatements || false; this.typeCast = (options.typeCast === undefined) ? true diff --git a/lib/protocol/Protocol.js b/lib/protocol/Protocol.js index a087cc7d6..41defde4e 100644 --- a/lib/protocol/Protocol.js +++ b/lib/protocol/Protocol.js @@ -122,6 +122,11 @@ Protocol.prototype._enqueue = function(sequence) { }) .on('end', function() { self._dequeue(); + }) + .on('start-tls', function() { + self._connection._startTLS(function() { + sequence._tlsUpgradeCompleteHandler(); + }) }); if (this._queue.length === 1) { diff --git a/lib/protocol/packets/SSLRequestPacket.js b/lib/protocol/packets/SSLRequestPacket.js new file mode 100644 index 000000000..a57cfc1a1 --- /dev/null +++ b/lib/protocol/packets/SSLRequestPacket.js @@ -0,0 +1,27 @@ +// http://dev.mysql.com/doc/internals/en/ssl.html +// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::SSLRequest + +var ClientConstants = require('../constants/client'); + +module.exports = SSLRequestPacket; + +function SSLRequestPacket(options) { + options = options || {}; + this.clientFlags = options.clientFlags | ClientConstants.CLIENT_SSL; + this.maxPacketSize = options.maxPacketSize; + this.charsetNumber = options.charsetNumber; +} + +SSLRequestPacket.prototype.parse = function(parser) { + // TODO: check SSLRequest packet v41 vs pre v41 + this.clientFlags = parser.parseUnsignedNumber(4); + this.maxPacketSize = parser.parseUnsignedNumber(4); + this.charsetNumber = parser.parseUnsignedNumber(1); +}; + +SSLRequestPacket.prototype.write = function(writer) { + writer.writeUnsignedNumber(4, this.clientFlags); + writer.writeUnsignedNumber(4, this.maxPacketSize); + writer.writeUnsignedNumber(1, this.charsetNumber); + writer.writeFiller(23); +}; diff --git a/lib/protocol/sequences/Handshake.js b/lib/protocol/sequences/Handshake.js index 029e2275e..287213b1e 100644 --- a/lib/protocol/sequences/Handshake.js +++ b/lib/protocol/sequences/Handshake.js @@ -1,7 +1,8 @@ -var Sequence = require('./Sequence'); -var Util = require('util'); -var Packets = require('../packets'); -var Auth = require('../Auth'); +var Sequence = require('./Sequence'); +var Util = require('util'); +var Packets = require('../packets'); +var Auth = require('../Auth'); +var ClientConstants = require('../constants/client'); module.exports = Handshake; Util.inherits(Handshake, Sequence); @@ -31,6 +32,29 @@ Handshake.prototype['HandshakeInitializationPacket'] = function(packet) { this._config.protocol41 = packet.protocol41; + var serverSSLSupport = packet.serverCapabilities1 & ClientConstants.CLIENT_SSL; + + if (this._config.ssl) { + if (!serverSSLSupport) + throw new Error('Server does not support secure connnection'); + this._config.clientFlags |= ClientConstants.CLIENT_SSL; + this.emit('packet', new Packets.SSLRequestPacket({ + clientFlags : this._config.clientFlags, + maxPacketSize : this._config.maxPacketSize, + charsetNumber : this._config.charsetNumber + })); + this.emit('start-tls'); + } else { + this._sendCredentials(); + } +}; + +Handshake.prototype._tlsUpgradeCompleteHandler = function() { + this._sendCredentials(); +}; + +Handshake.prototype._sendCredentials = function(serverHello) { + var packet = this._handshakeInitializationPacket; this.emit('packet', new Packets.ClientAuthenticationPacket({ clientFlags : this._config.clientFlags, maxPacketSize : this._config.maxPacketSize,