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('');
+ 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());
+ });
+});