Sunday, September 11, 2011

SOAP2REST Proxy with node.js

Given the rebirth of JavaScript in the past decade it shouldn't surprise us that some wise-guy asked why we don't use the same language on the server as we use on the client: JavaScript!

Actually Microsoft was doing this in the 90's with their <script runat="server"> where you could write either VBScript or JScript both client and server side. Then browsers like Firefox took off and VBScript was unsupported leading to a rebirth of the powerful JavaScript language.

Node.js (or node.exe) is basically an interpreter like php.exe except you can actually use it stand-alone as it's own web service. You write a simple server.js like this:
var http = require('http');
var srv = http.createServer(function (request, response) {
  response.writeHead(200, {'Content-Type': 'text/html'});
  response.write("<h1>Hello World!</h1>");
  response.end();
}).listen(8080);
Next you run your script from the command prompt:
C:\node.exe server.js
Finally, open up your browser to http://localhost:8080 and you should see Hello World! in large, friendly letters.

Because I'm planning a killer hip mobile/offline app which consumes SOAP web services I decided to write a node.js script which will allow me to make simple HTTP GET requests and receive JSON data in return. The soap2rest.js proxy I wrote converts the GET request to a SOAP request and should parse the SOAP response to return JSON:
myApp.html -> HTTP GET -> (soap2rest.js) -> SOAP Request -> SOAP Response -> JSON

The first thing I need is an HTTP client. It's important to remember that our soap2rest.js is going to proxy the SOAP request so I made a generic HTTP request function:

function doHTTP(options, success, error) {

  var req = http.request(options, function(res) {
    var data = '';
    res.setEncoding(options.encoding || 'utf8');
    res.on('data', function (chunk) { data += chunk; });
    res.on('end', function () { success({ data: data, response: res }); });
  });

  if (typeof options.body != 'undefined') req.write(options.body);
  req.end();
  
  req.on('error', function(e) { error('HTTP ERROR: ' + e.message); });
}
As you can see the function simply takes all it's parameters via the options object and has two callback functions for success and error. Now all we have to do is map the incoming GET to an HTTP SOAP request. In order to make this proxy as generic as possible I defined the following syntax for the URL: http://whatever/service/method?parameters=values So, for a simple weather service I would call: http://localhost:8080/weather/fetch?city=Zurich Before I can call my soap2rest.js proxy I need to define the service call. This is done in a configuration file called fetch.json corresponding to the fetch method of my weather service. The configuration file sits in a folder called weather with other potential methods: weather/fetch.json:
{
 "url": "http://www.webservicex.net/globalweather.asmx?op=GetWeather",
 "headers": {
  "SOAPAction": "http://www.webserviceX.NET/GetWeather",
  "Content-Type": "text/xml"
 }
}
This configuration file tells SOAP2REST.js almost everything it needs to know about calling the SOAP service globalweather.asmx. What it still needs is the request format. This can be discovered by parsing the WSDL of the service but this is seriously complicated and liable to error. And I'm too lazy. So, the developer "just" has to provide the sample request body in fetch.xml inside the weather/ folder:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetWeather xmlns="http://www.webserviceX.NET">
      <CityName>##CITY##</CityName>
      <CountryName></CountryName>
    </GetWeather>
  </soap:Body>
</soap:Envelope>
The ##CITY## is a placeholder which will be replaced with the city parameter of the incoming GET request. Here is where the magic happens. The SOAP2REST function does the following:
  1. Parse the URL and determine the service and method
  2. Read the service/method.json configuration file
  3. Read the service/method.xml request file
  4. Replace all placeholders from the query string (e.g. city)
  5. Call the SOAP service POSTing the method.xml data
  6. Return the response to the client
So here's the magic SOAP2REST:
function SOAP2REST(URL, success, error) {

  URL = url.parse(URL, true);

  // Load Service Definition
  try {
    var soapJSON = fs.readFileSync('./soap' + URL.pathname + '.json', 'UTF-8');
  } catch (e) {
    return die({msg: "Service description " + URL.pathname + ".json could not be found!", err: e}, 404);
  }
  
  // Load SOAP Request
  var soapXML = '';
  try {
    soapXML = fs.readFileSync('./soap' + URL.pathname + '.xml', 'UTF-8');
  } catch (e) {
    // No problem
  }
  
  // Parse SOAP Configuration
  try {
    var soapJSON = JSON.parse(soapJSON);
	var soapURL = url.parse(soapJSON.url, true);
  } catch (e) {
    return die({msg: "Service JSON could not be parsed!", name: e.name, message: e.message}, 500);
  }
  
  // Set Parameters
  for (p in URL.query) soapXML = soapXML.replace('##' + p.toUpperCase() + '##', URL.query[p]);

  // Prepare SOAP Request Headers
  soapJSON.headers = soapJSON.headers || {};
  soapJSON.headers["Content-Length"] = soapXML.length;
  soapJSON.headers["Connection"] = "close";
  
  // Do SOAP Call
  var httpOptions = {
    host:     soapURL.hostname,
    post:     soapURL.port || 80,
    method:   soapJSON.method || 'POST',
    path:     soapURL.pathname,
    headers:  soapJSON.headers,
  };
  
  httpOptions.body = soapXML;
  
  doHTTP(httpOptions,
    function(d) {
      success(d);
    },
    function(e){
      error(e, 500);
    }
  );

}
Technically, I guess, this ain't REST because I'm not using /weather/fetch/city/Zurich/ but I prefer /weather/fetch?city=Zurich because this is the standard HTTP way of passing parameters and it means AJAX forms will work automagically!

If I do say so myself, I like the way you can easily add a new service on the fly simply by adding a .json and .xml file.

Now to find an XML2JSON converter...

Note: This code is all for node.js v5.6 and the functions seem to change with every release so no doubt this won't work in future! Full Source Code

No comments:

Post a Comment