updated-soap-server
Version:
Soap server, using pure javascript.
508 lines (414 loc) • 16 kB
JavaScript
'use strict';
var http = require('http'),
util = require('util'),
events = require('events'),
xml2js = require('xml2js'),
parseXml = xml2js.parseString,
Builder = xml2js.Builder;
function SoapServiceException(type, message) {
this.name = 'SoapServerException';
this.type = type;
this.message = message;
}
SoapServiceException.OPERATION_NOT_SPECIFIED = 'OPERATION_NOT_SPECIFIED';
SoapServiceException.MISSING_REQUIRED_INPUT = 'MISSING_REQUIRED_INPUT';
SoapServiceException.INVALID_INPUT_TYPE = 'INVALID_INPUT_TYPE';
function SoapServerException(type, message) {
this.name = 'SoapServerException';
this.type = type;
this.message = message;
}
SoapServerException.FILE_NOT_FOUND = 'FILE_NOT_FOUND';
SoapServerException.SERVICE_NOT_FOUND = 'SERVICE_NOT_FOUND';
SoapServerException.MISSING_REQUEST = 'MISSING_REQUEST';
function SoapOperation(name, service, operation){
this.name = name;
this.service = service;
this.operation = operation;
this.inputs = {};
this.output = {type: 'string'};
var regex = /^function\s+\(([^)]+)\)/;
var operationString = operation + '';
var matches = regex.exec(operationString);
var args = matches[1].split(',');
for(var i in args)
this.addInput(args[i].trim(), 'string');
}
SoapOperation.prototype.getType = function(options){
if(options === null)
options = 'string';
if('string' == typeof options)
options = {type: options};
if('function' == typeof options){
var membersObject = options.prototype;
options = {
type: 'object',
objectType: options.name,
constructor: options,
members: {}
};
for(var member in membersObject){
if('function' == typeof membersObject[member] && membersObject[member].name){
options.members[member] = this.getType(membersObject[member]);
}
else{
var type = 'string';
if(membersObject[member] !== null)
type = typeof membersObject[member];
options.members[member] = {type: type};
}
}
}
return options;
};
SoapOperation.prototype.setOutputType = function(options){
options = this.getType(options);
this.output = options;
};
SoapOperation.prototype.addInput = function(name, options){
if('string' == typeof options)
options = {type: options};
this.inputs[name] = options;
};
SoapOperation.prototype.setInputType = function(name, options){
options = this.getType(options);
this.inputs[name] = options;
};
SoapOperation.prototype.exec = function(){
return this.operation.apply(this.service, arguments);
};
SoapOperation.prototype.getObjects = function(){
var objects = {};
if(this.output.type == 'object'){
objects[this.output.objectType] = this.output.members;
}
for(var inputName in this.inputs){
var input = this.inputs[inputName];
if(input.type == 'object'){
objects[input.objectType] = input.members;
}
}
return objects;
};
function SoapService(server, name, service){
this.server = server;
this.name = name;
this.service = service;
this.operations = {};
for(var attribute in service){
if('function' == typeof service[attribute]){
this.addOperation(attribute, service[attribute]);
}
}
}
SoapService.prototype.addOperation = function(name, operation, scope){
this.operations[name] = new SoapOperation(name, scope ? scope : this.service, operation);
return this.operations[name];
};
SoapService.prototype.getOperation = function(name){
return this.operations[name];
};
SoapService.prototype.getObjects = function(){
var objects = {};
for(var operationName in this.operations){
var operationObjects = this.operations[operationName].getObjects();
for(var objectName in operationObjects){
objects[objectName] = operationObjects[objectName];
}
}
return objects;
};
function SoapServer(options, security){
if(!options){
options = {
tns: 'server.soap.com'
};
}
this.options = options;
this.services = {};
this.security = security;
}
SoapServer.prototype.securityCheck = function (headers) {
if (this.security === 'basicAuth') {
let credentials = Buffer.from(headers.authorization.replace('Basic ', ''), 'base64');
if (this.users[credentials]) {
return true;
}
}
return false;
};
util.inherits(SoapServer, events.EventEmitter);
exports.SoapServer = SoapServer;
exports.SoapServerException = SoapServerException;
SoapServer.prototype.addService = function (name, service){
this.services[name] = new SoapService(this.server, name, service);
return this.services[name];
};
SoapServer.prototype.castArgument = function (inputName, requestArgument, options){
switch(options.type){
case 'object':
if('object' != typeof requestArgument) {
throw new SoapServiceException(SoapServiceException.INVALID_INPUT_TYPE, 'Invalid input [' + inputName + '] type ' + typeof requestArgument + ', expected ' + options.type);
}
return (requestArgument && requestArgument[0] ? requestArgument[0] : requestArgument);
case 'number':
if (isNaN(requestArgument)) {
throw new SoapServiceException(SoapServiceException.INVALID_INPUT_TYPE, 'Invalid input [' + inputName + '] type ' + typeof requestArgument + ', expected ' + options.type);
}
return +requestArgument;
case 'boolean':
return Boolean(requestArgument);
default:
return String(requestArgument);
}
};
SoapServer.prototype.handleOperationRequest = function (service, soapRequest){
var operationRequest = soapRequest['soap:Envelope']['soap:Body'][0];
for(var operationRequestName in operationRequest){
var operationName = operationRequestName.substr(operationRequestName.indexOf(':') + 1);
var requestArguments = operationRequest[operationRequestName][0];
delete requestArguments.$;
var operation = service.getOperation(operationName);
if('undefined' == typeof operation)
throw new SoapServiceException(SoapServiceException.OPERATION_NOT_FOUND, 'Operation [' + operationName + '] not found');
var operationArguments = [];
for(var inputName in operation.inputs){
var input = operation.inputs[inputName];
inputName = 'tns:' + inputName;
if(!requestArguments[inputName]){
if(input.required)
throw new SoapServiceException(SoapServiceException.MISSING_REQUIRED_INPUT, 'Missing required input [' + inputName + ']');
operationArguments.push(null);
continue;
}
var requestArgument = this.castArgument(inputName, requestArguments[inputName], input);
operationArguments.push(requestArgument);
}
/*
console.log('Operation: ' + operationName + ':');
console.dir(operationArguments);
*/
var operationResponse = operation.exec.apply(operation, operationArguments);
var response = {};
var responseName = operationName + 'Response';
response[responseName] = {RETURN: operationResponse};
return response;
}
throw new SoapServerException(SoapServerException.OPERATION_NOT_SPECIFIED, 'Operation not specified');
};
SoapServer.prototype.js2xml = function(js/*, namespace, indentLevel*/){
var builder = new Builder();
return builder.buildObject(js);
};
SoapServer.prototype.listen = function(port, host, cp){
var self = this;
http.createServer(function (req, res) {
self.handleRequest(req, res);
}).listen(port, host, cp);
};
SoapServer.prototype.setUsers = function(users) {
this.users = users;
};
SoapServer.prototype.handleRequest = function(req, res){
if (req.url.indexOf('wsdl') === -1 && this.security && !this.securityCheck(req.headers)) {
res.writeHead(401, {'Content-Type': 'text/plain'});
res.end('UNAUTHORIZED');
return console.log('UNAUTHORIZED');
}
try{
this.handleSoapRequest(req, function(response){
res.writeHead(200, {'Content-Type': 'text/xml'});
res.end(response);
});
} catch(exception) {
if(exception.name == 'SoapServerException'){
if(exception.type == SoapServerException.SERVICE_NOT_FOUND) {
res.writeHead(404, {'Content-Type': 'text/plain'});
} else {
res.writeHead(500, {'Content-Type': 'text/plain'});
}
res.end(exception.message);
} else {
res.writeHead(500, {'Content-Type': 'text/plain'});
if('string' == typeof exception){
res.end(exception);
} else if('string' == typeof exception.message) {
res.end(exception.message);
} else {
res.end(exception + '');
}
}
}
};
SoapServer.prototype.handleSoapRequest = function(req, callback){
//console.log('Request url: ' + req.url);
var self = this;
var service = null;
var serviceName = null;
if (req.method == 'POST') {
serviceName = req.url.substr(1);
if(serviceName.indexOf('/') >= 0)
throw new SoapServerException(SoapServerException.FILE_NOT_FOUND, 'Path [' + req.url + '] not found');
service = this.services[serviceName];
if('undefined' == typeof service) {
throw new SoapServerException(SoapServerException.SERVICE_NOT_FOUND, 'Service [' + req.path + '] not found');
}
var body = '';
req.on('data', function (data) {
body += data;
});
req.on('end', function () {
//console.log('Request XML: ' + body);
parseXml(body, function (err, soapRequest) {
if(err){
console.dir('Request parse error: ' + err);
// throw err;
}
var responseXml = '<soap:Envelope\n';
responseXml += ' xmlns:soap="http://www.w3.org/2001/12/soap-envelope"\n';
responseXml += ' soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">\n';
responseXml += ' <soap:Body>\n';
try{
var soapResponse = self.handleOperationRequest(service, soapRequest);
//console.dir(soapResponse);
responseXml += self.js2xml(soapResponse, null, 2);
}
catch(exception){
console.log(exception);
responseXml += ' <soap:Fault>\n';
responseXml += ' <soap:faultcode>' + exception.type + '</soap:faultcode>\n';
responseXml += ' <soap:faultstring>' + exception.message + '</soap:faultstring>\n';
responseXml += ' </soap:Fault>\n';
}
responseXml += ' </soap:Body>\n';
responseXml += '</soap:Envelope>';
//console.log('Soap response: ' + responseXml);
callback.apply(null, [responseXml]);
});
});
return;
}
if(req.url.indexOf('?') < 0)
throw new SoapServerException(SoapServerException.MISSING_REQUEST, 'No query string supplied');
var urlParts = req.url.split('?');
serviceName = urlParts[0].substr(1);
if(serviceName.indexOf('/') >= 0)
throw new SoapServerException(SoapServerException.FILE_NOT_FOUND, 'Path [' + urlParts[0] + '] not found');
service = this.services[serviceName];
if('undefined' == typeof service)
throw new SoapServerException(SoapServerException.SERVICE_NOT_FOUND, 'Service [' + path + '] not found');
if(urlParts[1] == 'wsdl'){
var wsdl = this.getWsdl(req.headers.host, service);
callback.apply(null, [wsdl]);
}
};
SoapServer.prototype.getType = function(options){
if(options.type == 'object')
return options.objectType;
if(options.type == 'number')
return 'int';
return options.type;
};
SoapServer.prototype.getXsdElement = function(name, options, indentLevel){
var indent = '';
for(var i = 0; i < indentLevel; i++)
indent += ' ';
if(options.type != 'object')
return indent + '<xsd:element name="' + name + '" type="xsd:' + this.getType(options) + '" minOccurs="0" />\n';
var ret = indent + '<xsd:element name="' + name + '" minOccurs="0">\n';
ret += indent + ' <xsd:complexType>\n';
ret += indent + ' <xsd:sequence>\n';
for(var memberName in options.members){
ret += this.getXsdElement(memberName, options.members[memberName], indentLevel + 3);
}
ret += indent + ' </xsd:sequence>\n';
ret += indent + ' </xsd:complexType>\n';
ret += indent + '</xsd:element>\n';
return ret;
};
SoapServer.prototype.getWsdl = function(host, service){
if('object' != typeof service || service.constructor.name != 'SoapService')
throw 'getWsdl expect service or type SoapService';
var wsdl = '<?xml version="1.0" encoding="UTF-8"?><definitions\n';
wsdl += ' xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"\n';
wsdl += ' xmlns:wsp="http://www.w3.org/ns/ws-policy"\n';
wsdl += ' xmlns:wsp1_2="http://schemas.xmlsoap.org/ws/2004/09/policy"\n';
wsdl += ' xmlns:tns="http://' + this.options.tns + '/"\n';
wsdl += ' xmlns:xsd="http://www.w3.org/2001/XMLSchema"\n';
wsdl += ' xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata"\n';
wsdl += ' xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"\n';
wsdl += ' xmlns="http://schemas.xmlsoap.org/wsdl/"\n';
wsdl += ' targetNamespace="http://' + this.options.tns + '/"\n';
wsdl += ' name="' + service.name + 'Service">\n';
wsdl += ' <types>\n';
wsdl += ' <xsd:schema version="1.0" targetNamespace="http://' + this.options.tns + '/">\n';
var objects = service.getObjects();
for(var objectTypeName in objects){
var object = objects[objectTypeName];
wsdl += ' <xsd:element name="' + objectTypeName + '" type="tns:' + objectTypeName + '" />\n';
wsdl += ' <xsd:complexType name="' + objectTypeName + '">\n';
wsdl += ' <xsd:sequence>\n';
for(var memberName in object){
wsdl += this.getXsdElement(memberName, object[memberName], 5);
}
wsdl += ' </xsd:sequence>\n';
wsdl += ' </xsd:complexType>\n';
}
wsdl += ' </xsd:schema>\n';
wsdl += ' </types>\n';
var operationName;
for(operationName in service.operations){
var operation = service.getOperation(operationName);
wsdl += ' <message name="' + operationName + '">\n';
for(var inputName in operation.inputs){
var input = operation.inputs[inputName];
if(input.type == 'object'){
wsdl += ' <part name="' + inputName + '" element="tns:' + this.getType(input) + '" />\n';
}
else{
wsdl += ' <part name="' + inputName + '" element="xsd:' + this.getType(input) + '" />\n';
}
}
wsdl += ' </message>\n';
wsdl += ' <message name="' + operationName + 'Response">\n';
if(operation.output.type == 'object'){
wsdl += ' <part name="return" element="tns:' + this.getType(operation.output) + '" />\n';
}
else{
wsdl += ' <part name="return" element="xsd:' + this.getType(operation.output) + '" />\n';
}
wsdl += ' </message>\n';
}
wsdl += ' <portType name="' + service.name + '">\n';
for(operationName in service.operations){
var operation = service.getOperation(operationName);
wsdl += ' <operation name="' + operationName + '">\n';
wsdl += ' <input wsam:Action="' + operationName + '" message="tns:' + operationName + '" />\n';
wsdl += ' <output wsam:Action="http://' + this.options.tns + '/' + service.name + '/' + operationName + 'Response"\n';
wsdl += ' message="tns:' + operationName + 'Response" />\n';
wsdl += ' </operation>\n';
}
wsdl += ' </portType>\n';
wsdl += ' <binding name="' + service.name + 'PortBinding" type="tns:' + service.name + '">\n';
wsdl += ' <soap:binding transport="http://schemas.xmlsoap.org/soap/http"\n';
wsdl += ' style="rpc" />\n';
for(operationName in service.operations){
wsdl += ' <operation name="' + operationName + '">\n';
wsdl += ' <soap:operation soapAction="' + operationName + '" />\n';
wsdl += ' <input name="' + operationName + '">\n';
wsdl += ' <soap:body use="literal" />\n';
wsdl += ' </input>\n';
wsdl += ' <output name="' + operationName + 'Response">\n';
wsdl += ' <soap:body use="literal" />\n';
wsdl += ' </output>\n';
wsdl += ' </operation>\n';
}
wsdl += ' </binding>\n';
wsdl += ' <service name="' + service.name + 'Service">\n';
wsdl += ' <port name="' + service.name + 'Port" binding="tns:' + service.name + 'PortBinding">\n';
wsdl += ' <soap:address location="http://' + host + '/' + service.name + '" />\n';
wsdl += ' </port>\n';
wsdl += ' </service>\n';
wsdl += '</definitions>';
return wsdl;
};