Added some authorisation, general fixes, logging option, option of incoming connection validation #24

Merged
merged 9 commits into from Apr 30, 2012
View
@@ -0,0 +1 @@
+node_modules
View
163 Readme.md
@@ -9,76 +9,135 @@ Current limitations:
Install with [npm](http://github.com/isaacs/npm):
- npm install soap
-
+```
+ npm install soap
+```
## Module
### soap.createClient(url, callback) - create a new SOAP client from a WSDL url
- var soap = require('soap');
- var url = 'http://example.com/wsdl?wsdl';
- var args = {name: 'value'};
- soap.createClient(url, function(err, client) {
- client.MyFunction(args, function(err, result) {
- console.log(result);
- });
- });
-
-### soap.listen(*server*, *path*, *services*, *wsdl*) - create a new SOAP server that listens on *path* and provides *services*. *wsdl* is an xml string that defines the service.
-
- var myService = {
- MyService: {
- MyPort: {
- MyFunction: function(args) {
- return {
- name: args.name
- };
- }
- }
- }
- }
-
- var xml = require('fs').readFileSync('myservice.wsdl', 'utf8'),
- server = http.createServer(function(request,response) {
- response.end("404: Not Found: "+request.url)
- });
-
- server.listen(8000);
- soap.listen(server, '/wsdl', myService, xml);
+``` javascript
+ var soap = require('soap');
+ var url = 'http://example.com/wsdl?wsdl';
+ var args = {name: 'value'};
+ soap.createClient(url, function(err, client) {
+ client.MyFunction(args, function(err, result) {
+ console.log(result);
+ });
+ });
+```
+
+### soap.listen(*server*, *path*, *services*, *wsdl*) - create a new SOAP server that listens on *path* and provides *services*.
+*wsdl* is an xml string that defines the service.
+
+``` javascript
+ var myService = {
+ MyService: {
+ MyPort: {
+ MyFunction: function(args) {
+ return {
+ name: args.name
+ };
+ }
+ }
+ }
+ }
+
+ var xml = require('fs').readFileSync('myservice.wsdl', 'utf8'),
+ server = http.createServer(function(request,response) {
+ response.end("404: Not Found: "+request.url)
+ });
+
+ server.listen(8000);
+ soap.listen(server, '/wsdl', myService, xml);
+```
+
+### server logging
+
+If the log method is defined it will be called with 'received' and 'replied'
+along with data.
+
+``` javascript
+ server = soap.listen(...)
+ server.log = function(type, data) {
+ // type is 'received' or 'replied'
+ };
+```
+
+### server security example using PasswordDigest
+
+If server.authenticate is not defined no authentation will take place.
+
+``` javascript
+ server = soap.listen(...)
+ server.authenticate = function(security) {
+ var created, nonce, password, user, token;
+ token = security.UsernameToken, user = token.Username,
+ password = token.Password, nonce = token.Nonce, created = token.Created;
+ return user === 'user' && password === soap.passwordDigest(nonce, created, 'password');
+ };
+```
+
+### server connection authorisation
+
+This is called prior to soap service method
+If the method is defined and returns false the incoming connection is
+terminated.
+
+``` javascript
+ server = soap.listen(...)
+ server.authoriseConnection = function(req) {
+ return true; // or false
+ };
+```
## Client
An instance of Client is passed to the soap.createClient callback. It is used to execute methods on the soap service.
### Client.describe() - description of services, ports and methods as a JavaScript object
- client.describe() =>
- { MyService: {
- MyPort: {
- MyFunction: {
- input: {
- name: 'string'
- }
- }}}}
+``` javascript
+ client.describe() // returns
+ {
+ MyService: {
+ MyPort: {
+ MyFunction: {
+ input: {
+ name: 'string'
+ }
+ }
+ }
+ }
+ }
+```
### Client.setSecurity(security) - use the specified security protocol (see WSSecurity below)
- client.setSecurity(new WSSecurity('username', 'password'))
-
-### Client.*method*(args, callback) - call *method* on the SOAP service.
+``` javascript
+ client.setSecurity(new WSSecurity('username', 'password'))
+```
+
+### Client.*method*(args, callback) - call *method* on the SOAP service.
- client.MyFunction({name: 'value'}, function(err, result) {
- // result is a javascript object
- })
-
+``` javascript
+ client.MyFunction({name: 'value'}, function(err, result) {
+ // result is a javascript object
+ })
+```
### Client.*service*.*port*.*method*(args, callback) - call a *method* using a specific *service* and *port*
- client.MyService.MyPort.MyFunction({name: 'value'}, function(err, result) {
- // result is a javascript object
- })
+``` javascript
+ client.MyService.MyPort.MyFunction({name: 'value'}, function(err, result) {
+ // result is a javascript object
+ })
+```
## WSSecurity
-WSSecurity implements WS-Security. Currently, only UsernameToken and PasswordText is supported. An instance of WSSecurity is passed to Client.setSecurity.
+WSSecurity implements WS-Security. UsernameToken and PasswordText/PasswordDigest is supported. An instance of WSSecurity is passed to Client.setSecurity.
- new WSSecurity(username, password)
+``` javascript
+ new WSSecurity(username, password, passwordType)
+ //'PasswordDigest' or 'PasswordText' default is PasswordText
+```
View
@@ -118,7 +118,7 @@ Client.prototype._invoke = function(method, arguments, location, callback) {
else {
try {
var obj = self.wsdl.xmlToObject(body);
- callback(null, obj[output.$name], body);
+ callback(null, obj.Body[output.$name], body);
}
catch (error) {
callback(error, null, body);
View
@@ -19,6 +19,12 @@ var Server = function(server, path, services, wsdl) {
wsdl.onReady(function(err) {
server.removeAllListeners('request');
server.addListener('request', function(req, res) {
+ if (typeof self.authoriseConnection === 'function') {
+ if (!self.authoriseConnection(req.connection.remoteAddress)) {
+ res.end();
+ return;
+ }
+ }
var reqPath = url.parse(req.url).pathname;
if (reqPath[reqPath.length-1] != '/') reqPath += '/';
if (path === reqPath) {
@@ -44,6 +50,7 @@ Server.prototype._requestListener = function(req, res) {
res.end();
}
else if (req.method === 'POST') {
+ res.setHeader('Content-Type', req.headers['content-type']);
var chunks = [], gunzip;
if (compress && req.headers["content-encoding"] == "gzip") {
gunzip = new compress.Gunzip;
@@ -67,6 +74,10 @@ Server.prototype._requestListener = function(req, res) {
}
res.write(result);
res.end();
+ if (typeof self.log === 'function') {
+ self.log("received", xml);
+ self.log("replied", result);
+ }
});
}
else {
@@ -77,10 +88,21 @@ Server.prototype._requestListener = function(req, res) {
Server.prototype._process = function(input) {
var self = this,
obj = this.wsdl.xmlToObject(input),
- messageName = Object.keys(obj)[0],
- args = obj[messageName],
+ body = obj.Body,
+ messageName = Object.keys(body)[0],
+ args = body[messageName],
portTypes = this.wsdl.definitions.portTypes;
+
+ if (typeof self.authenticate === 'function') {
+ if (obj.Header == null || obj.Header.Security == null) {
+ throw new Error('No security header');
+ }
+ if (!self.authenticate(obj.Header.Security)) {
+ throw new Error('Invalid username or password');
+ }
+ }
+
for (var portName in portTypes) {
var portType = portTypes[portName];
var methods = portType.methods;
@@ -121,4 +143,4 @@ Server.prototype._envelope = function(body) {
return xml;
}
-exports.Server = Server;
+exports.Server = Server;
View
@@ -6,7 +6,8 @@
var Client = require('./client').Client,
Server = require('./server').Server,
open_wsdl = require('./wsdl').open_wsdl,
- crypto = require('crypto');
+ crypto = require('crypto'),
+ WSDL = require('./wsdl').WSDL;
var _wsdlCache = {};
@@ -47,9 +48,19 @@ BasicAuthSecurity.prototype.toXML = function() {
return "";
}
-function WSSecurity(username, password) {
+function WSSecurity(username, password, passwordType) {
this._username = username;
this._password = password;
+ this._passwordType = passwordType || 'PasswordText';
+}
+
+var passwordDigest = function(nonce, created, password) {
+ // digest = base64 ( sha1 ( nonce + created + password ) )
+ var pwHash = crypto.createHash('sha1');
+ var rawNonce = new Buffer(nonce || '', 'base64').toString('binary');
+ pwHash.update(rawNonce + created + password);
+ var passwordDigest = pwHash.digest('base64');
+ return passwordDigest;
}
WSSecurity.prototype.toXML = function() {
@@ -72,22 +83,18 @@ WSSecurity.prototype.toXML = function() {
nHash.update(created + Math.random());
var nonce = nHash.digest('base64');
- // digest = base64 ( sha1 ( nonce + created + password ) )
- var pwHash = crypto.createHash('sha1');
- var rawNonce = new Buffer(nonce || '', 'base64').toString('utf8');
- pwHash.update( rawNonce + created + this._password );
- var passwordDigest = pwHash.digest('base64');
-
return "<wsse:Security xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\" xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">" +
"<wsu:Timestamp wsu:Id=\"Timestamp-"+created+"\">" +
"<wsu:Created>"+created+"</wsu:Created>" +
"<wsu:Expires>"+expires+"</wsu:Expires>" +
"</wsu:Timestamp>" +
"<wsse:UsernameToken xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\" wsu:Id=\"SecurityToken-"+created+"\">" +
"<wsse:Username>"+this._username+"</wsse:Username>" +
- //TODO:FIXME: reenable password hash
- //"<wsse:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"+passwordDigest+"</wsse:Password>" +
- "<wsse:Password>"+this._password+"</wsse:Password>" +
+ (this._passwordType === 'PasswordText' ?
+ "<wsse:Password>"+this._password+"</wsse:Password>"
+ :
+ "<wsse:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"+passwordDigest(nonce, created, this._password)+"</wsse:Password>"
+ ) +
"<wsse:Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">"+nonce+"</wsse:Nonce>" +
"<wsu:Created>"+created+"</wsu:Created>" +
"</wsse:UsernameToken>" +
@@ -97,5 +104,6 @@ WSSecurity.prototype.toXML = function() {
exports.BasicAuthSecurity = BasicAuthSecurity;
exports.WSSecurity = WSSecurity;
exports.createClient = createClient;
+exports.passwordDigest = passwordDigest;
exports.listen = listen;
View
@@ -612,7 +612,7 @@ WSDL.prototype.xmlToObject = function(xml) {
if (body.Fault) {
throw new Error(body.Fault.faultcode+': '+body.Fault.faultstring+(body.Fault.detail ? ': ' + body.Fault.detail : ''));
}
- return body;
+ return root.Envelope;
}
WSDL.prototype.objectToDocumentXML = function(name, params) {