diff --git a/.travis.yml b/.travis.yml index f0472f7e3..7b9b3fa50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,13 +3,20 @@ language: node_js notifications: email: false node_js: - - 0.8 - 0.10 - 0.12 - 4.0 - iojs +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 before_install: - - npm install -g npm@~1.4.6 + - npm -g install npm@latest script: - npm run cover - npm run coveralls diff --git a/Readme.md b/Readme.md index 125debf7b..5c6cc6e4d 100644 --- a/Readme.md +++ b/Readme.md @@ -321,6 +321,16 @@ as default request options to the constructor: client.setSecurity(new soap.WSSecurity('username', 'password')) ``` +####WSSecurity with X509 Certificate + +``` javascript + var privateKey = fs.readFileSync(privateKeyPath); + var publicKey = fs.readFileSync(publicKeyPath); + var password = ''; // optional password + var wsSecurity = new soap.WSSecurityCert(privateKey, publicKey, password, 'utf8'); + client.setSecurity(wsSecurity); +``` + ####BearerSecurity ``` javascript diff --git a/lib/client.js b/lib/client.js index e7f52b9cd..6fef4cc37 100644 --- a/lib/client.js +++ b/lib/client.js @@ -226,7 +226,7 @@ Client.prototype._invoke = function(method, args, location, callback, options, e ( "" + (self.soapHeaders ? self.soapHeaders.join("\n") : "") + - (self.security ? self.security.toXML() : "") + + (self.security && !self.security.postProcess ? self.security.toXML() : "") + "" ) : @@ -234,11 +234,16 @@ Client.prototype._invoke = function(method, args, location, callback, options, e ) + "" + message + "" + ""; + if(self.security && self.security.postProcess){ + xml = self.security.postProcess(xml); + } + self.lastMessage = message; self.lastRequest = xml; self.lastEndpoint = location; diff --git a/lib/security/WSSecurityCert.js b/lib/security/WSSecurityCert.js new file mode 100644 index 000000000..11f8430ea --- /dev/null +++ b/lib/security/WSSecurityCert.js @@ -0,0 +1,78 @@ +"use strict"; + +var ursa = require('ursa'); +var fs = require('fs'); +var path = require('path'); +var ejs = require('ejs'); +var SignedXml = require('xml-crypto').SignedXml; +var uuid = require('node-uuid'); +var wsseSecurityHeaderTemplate = ejs.compile(fs.readFileSync(path.join(__dirname, 'templates', 'wsse-security-header.ejs')).toString()); +var wsseSecurityTokenTemplate = ejs.compile(fs.readFileSync(path.join(__dirname, 'templates', 'wsse-security-token.ejs')).toString()); + +function addMinutes(date, minutes) { + return new Date(date.getTime() + minutes * 60000); +} + +function dateStringForSOAP(date) { + return date.getUTCFullYear() + '-' + ('0' + (date.getUTCMonth() + 1)).slice(-2) + '-' + + ('0' + date.getUTCDate()).slice(-2) + 'T' + ('0' + date.getUTCHours()).slice(-2) + ":" + + ('0' + date.getUTCMinutes()).slice(-2) + ":" + ('0' + date.getUTCSeconds()).slice(-2) + "Z"; +} + +function generateCreated() { + return dateStringForSOAP(new Date()); +} + +function generateExpires() { + return dateStringForSOAP(addMinutes(new Date(), 10)); +} + +function insertStr(src, dst, pos) { + return [dst.slice(0, pos), src, dst.slice(pos)].join(''); +} + +function generateId() { + return uuid.v4().replace(/-/gm, ''); +} + +function WSSecurityCert(privatePEM, publicP12PEM, password, encoding) { + + this.privateKey = ursa.createPrivateKey(privatePEM, password, encoding); + this.publicP12PEM = publicP12PEM.toString().replace('-----BEGIN CERTIFICATE-----', '').replace('-----END CERTIFICATE-----', '').replace(/(\r\n|\n|\r)/gm, ''); + + this.signer = new SignedXml(); + this.signer.signingKey = this.privateKey.toPrivatePem(); + this.x509Id = "x509-" + generateId(); + + var references = ["http://www.w3.org/2000/09/xmldsig#enveloped-signature", + "http://www.w3.org/2001/10/xml-exc-c14n#"]; + + this.signer.addReference("//*[local-name(.)='Body']", references); + this.signer.addReference("//*[local-name(.)='Timestamp']", references); + + var _this = this; + this.signer.keyInfoProvider = {}; + this.signer.keyInfoProvider.getKeyInfo = function (key) { + return wsseSecurityTokenTemplate({ x509Id: _this.x509Id }); + }; +} + +WSSecurityCert.prototype.postProcess = function (xml) { + this.created = generateCreated(); + this.expires = generateExpires(); + + var secHeader = wsseSecurityHeaderTemplate({ + binaryToken: this.publicP12PEM, + created: this.created, + expires: this.expires, + id: this.x509Id + }); + + var xmlWithSec = insertStr(secHeader, xml, xml.indexOf('')); + + this.signer.computeSignature(xmlWithSec); + + return insertStr(this.signer.getSignatureXml(), xmlWithSec, xmlWithSec.indexOf('')); +}; + +module.exports = WSSecurityCert; diff --git a/lib/security/index.js b/lib/security/index.js index d0167d3f1..af9220096 100644 --- a/lib/security/index.js +++ b/lib/security/index.js @@ -6,4 +6,5 @@ module.exports = { , ClientSSLSecurityPFX: require('./ClientSSLSecurityPFX') , WSSecurity: require('./WSSecurity') , BearerSecurity: require('./BearerSecurity') +, WSSecurityCert: require('./WSSecurityCert') }; diff --git a/lib/security/templates/wsse-security-header.ejs b/lib/security/templates/wsse-security-header.ejs new file mode 100644 index 000000000..d137b4442 --- /dev/null +++ b/lib/security/templates/wsse-security-header.ejs @@ -0,0 +1,12 @@ + + <%-binaryToken%> + + <%-created%> + <%-expires%> + + diff --git a/lib/security/templates/wsse-security-token.ejs b/lib/security/templates/wsse-security-token.ejs new file mode 100644 index 000000000..59134cfb9 --- /dev/null +++ b/lib/security/templates/wsse-security-token.ejs @@ -0,0 +1,3 @@ + + + diff --git a/lib/soap.js b/lib/soap.js index 5b4a813e9..0f8b45315 100644 --- a/lib/soap.js +++ b/lib/soap.js @@ -69,6 +69,7 @@ function listen(server, pathOrOptions, services, xml) { exports.security = security; exports.BasicAuthSecurity = security.BasicAuthSecurity; exports.WSSecurity = security.WSSecurity; +exports.WSSecurityCert = security.WSSecurityCert; exports.ClientSSLSecurity = security.ClientSSLSecurity; exports.ClientSSLSecurityPFX = security.ClientSSLSecurityPFX; exports.BearerSecurity = security.BearerSecurity; diff --git a/package.json b/package.json index f41b91587..b749cfb52 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.13.0", "description": "A minimal node SOAP client", "engines": { - "node": ">=0.8.0" + "node": ">=0.10.0" }, "author": "Vinay Pulim ", "dependencies": { @@ -12,7 +12,11 @@ "request": ">=2.9.0", "sax": ">=0.6", "selectn": "^0.9.6", - "strip-bom": "~0.3.1" + "strip-bom": "~0.3.1", + "ursa": "0.8.5 || >=0.9.3", + "node-uuid": "~1.4.3", + "ejs": "~2.3.4", + "xml-crypto": "~0.8.0" }, "repository": { "type": "git", diff --git a/test/security/WSSecurityCert.js b/test/security/WSSecurityCert.js new file mode 100644 index 000000000..42be5d7c9 --- /dev/null +++ b/test/security/WSSecurityCert.js @@ -0,0 +1,72 @@ +'use strict'; + +var fs = require('fs'), + join = require('path').join; + +describe('WSSecurityCert', function() { + var WSSecurityCert = require('../../').WSSecurityCert; + var cert = fs.readFileSync(join(__dirname, '..', 'certs', 'agent2-cert.pem')); + var key = fs.readFileSync(join(__dirname, '..', 'certs', 'agent2-key.pem')); + + it('is a function', function() { + WSSecurityCert.should.be.type('function'); + }); + + it('should accept valid constructor variables', function() { + var instance = new WSSecurityCert(key, cert, '', 'utf8'); + instance.should.have.property('privateKey'); + instance.should.have.property('publicP12PEM'); + instance.should.have.property('signer'); + instance.should.have.property('x509Id'); + }); + + it('should not accept invalid constructor variables', function() { + var passed = true; + + try { + new WSSecurityCert('*****', cert, '', 'utf8'); + } catch(e) { + passed = false; + } + + if (passed) { + throw new Error('bad private key'); + } + + passed = true; + + try { + new WSSecurityCert(key, cert, '', 'bob'); + } catch(e) { + passed = false; + } + + if (passed) { + throw new Error('bad encoding'); + } + }); + + it('should insert a WSSecurity signing block when postProcess is called', function() { + var instance = new WSSecurityCert(key, cert, '', 'utf8'); + var xml = instance.postProcess(''); + + xml.should.containEql(''); + xml.should.containEql('' + instance.created); + xml.should.containEql('' + instance.expires); + xml.should.containEql(''); + xml.should.containEql(''); + xml.should.containEql(''); + xml.should.containEql(instance.publicP12PEM); + xml.should.containEql(instance.signer.getSignatureXml()); + }); +});