diff --git a/src/http.ts b/src/http.ts index 4e912f582..0b0fd5bf9 100644 --- a/src/http.ts +++ b/src/http.ts @@ -5,8 +5,10 @@ import * as debugBuilder from 'debug'; import * as httpNtlm from 'httpntlm'; +import * as _ from 'lodash'; import * as req from 'request'; import * as url from 'url'; +import * as uuid from 'uuid/v4'; import { IHeaders, IOptions } from './types'; const debug = debugBuilder('node-soap'); @@ -16,6 +18,13 @@ export interface IExOptions { [key: string]: any; } +export interface IAttachment { + name: string; + contentId: string; + mimetype: string; + body: ReadableStream; +} + export type Request = req.Request; /** @@ -41,7 +50,7 @@ export class HttpClient { * @param {Object} exoptions Extra options * @returns {Object} The http request object for the `request` module */ - public buildRequest(rurl: string, data: any, exheaders?: IHeaders, exoptions?: IExOptions): any { + public buildRequest(rurl: string, data: any, exheaders?: IHeaders, exoptions: IExOptions = {}): any { const curl = url.parse(rurl); const secure = curl.protocol === 'https:'; const host = curl.hostname; @@ -53,12 +62,13 @@ export class HttpClient { 'Accept': 'text/html,application/xhtml+xml,application/xml,text/xml;q=0.9,*/*;q=0.8', 'Accept-Encoding': 'none', 'Accept-Charset': 'utf-8', - 'Connection': exoptions && exoptions.forever ? 'keep-alive' : 'close', + 'Connection': exoptions.forever ? 'keep-alive' : 'close', 'Host': host + (isNaN(port) ? '' : ':' + port), }; const mergeOptions = ['headers']; + const attachments: IAttachment[] = exoptions.attachments || []; - if (typeof data === 'string') { + if (typeof data === 'string' && attachments.length === 0) { headers['Content-Length'] = Buffer.byteLength(data, 'utf8'); headers['Content-Type'] = 'application/x-www-form-urlencoded'; } @@ -75,10 +85,30 @@ export class HttpClient { followAllRedirects: true, }; - options.body = data; + if (attachments.length > 0) { + const start = uuid(); + headers['Content-Type'] = + 'multipart/related; type="application/xop+xml"; start="<' + start + '>"; start-info="text/xml"; boundary=' + uuid(); + const multipart: any[] = [{ + 'Content-Type': 'application/xop+xml; charset=UTF-8; type="text/xml"', + 'Content-ID': '<' + start + '>', + 'body': data, + }]; + attachments.forEach((attachment) => { + multipart.push({ + 'Content-Type': attachment.mimetype, + 'Content-Transfer-Encoding': 'binary', + 'Content-ID': '<' + attachment.contentId + '>', + 'Content-Disposition': 'attachment; filename="' + attachment.name + '"', + 'body': attachment.body, + }); + }); + options.multipart = multipart; + } else { + options.body = data; + } - exoptions = exoptions || {}; - for (const attr in exoptions) { + for (const attr in _.omit(exoptions, ['attachments'])) { if (mergeOptions.indexOf(attr) !== -1) { for (const header in exoptions[attr]) { options[attr][header] = exoptions[attr][header]; diff --git a/test/client-test.js b/test/client-test.js index 2e7926baf..c1c1bfda2 100644 --- a/test/client-test.js +++ b/test/client-test.js @@ -136,6 +136,84 @@ var fs = require('fs'), }); }); + describe('Binary attachments handling', function () { + var server = null; + var hostname = '127.0.0.1'; + var port = 15099; + var baseUrl = 'http://' + hostname + ':' + port; + var attachment = { + mimetype: 'image/png', + contentId: 'file_0', + name: 'nodejs.png', + body: fs.createReadStream(__dirname + '/static/nodejs.png') + }; + + function parsePartHeaders(part) { + const headersAndBody = part.split(/\r\n\r\n/); + const headersParts = headersAndBody[0].split(/\r\n/); + const headers = {}; + headersParts.forEach(header => { + let index; + if ((index = header.indexOf(':')) > -1) { + headers[header.substring(0, index)] = header.substring(index + 1).trim(); + } + }); + return headers; + } + + it('should send binary attachments using XOP + MTOM', function (done) { + server = http.createServer((req, res) => { + const bufs = []; + req.on('data', function (chunk) { + bufs.push(chunk); + }); + req.on('end', function () { + const body = Buffer.concat(bufs).toString().trim(); + const headers = req.headers; + const boundary = headers['content-type'].match(/boundary="?([^"]*"?)/)[1]; + const parts = body.split(new RegExp('--' + boundary + '-{0,2}')) + .filter(part => part) + .map(parsePartHeaders); + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ contentType: headers['content-type'], parts: parts }), 'utf8'); + }); + }).listen(port, hostname, function () { + + soap.createClient(__dirname + '/wsdl/attachments.wsdl', meta.options, function (initError, client) { + assert.ifError(initError); + + client.MyOperation({}, function (error, response, body) { + assert.ifError(error); + const contentType = {}; + body.contentType.split(/;\s?/).forEach(dir => { + const keyValue = dir.match(/(.*)="?([^"]*)?/); + if (keyValue && keyValue.length > 2) { + contentType[keyValue[1].trim()] = keyValue[2].trim(); + } else { + contentType.rootType = dir; + } + }); + assert.equal(contentType.rootType, 'multipart/related'); + assert.equal(body.parts.length, 2); + + const dataHeaders = body.parts[0]; + assert(dataHeaders['Content-Type'].indexOf('application/xop+xml') > -1); + assert.equal(dataHeaders['Content-ID'], contentType.start); + + const attachmentHeaders = body.parts[1]; + assert.equal(attachmentHeaders['Content-Type'], attachment.mimetype); + assert.equal(attachmentHeaders['Content-Transfer-Encoding'], 'binary'); + assert.equal(attachmentHeaders['Content-ID'], '<' + attachment.contentId + '>'); + assert(attachmentHeaders['Content-Disposition'].indexOf(attachment.name) > -1); + + server.close(); + done(); + }, { attachments: [attachment] }); + }, baseUrl); + }); + }); + }); + describe('Headers in request and last response', function () { var server = null; diff --git a/test/request-response-samples/attachments__should_send_binary_data/options.json b/test/request-response-samples/attachments__should_send_binary_data/options.json new file mode 100644 index 000000000..0fc59e166 --- /dev/null +++ b/test/request-response-samples/attachments__should_send_binary_data/options.json @@ -0,0 +1,8 @@ +{ + "attachments": [{ + "mimetype": "plain/txt", + "contentId": "file_0", + "name": "data.txt", + "body": "some data" + }] +} \ No newline at end of file diff --git a/test/request-response-samples/attachments__should_send_binary_data/request.json b/test/request-response-samples/attachments__should_send_binary_data/request.json new file mode 100644 index 000000000..cf3f06efa --- /dev/null +++ b/test/request-response-samples/attachments__should_send_binary_data/request.json @@ -0,0 +1,7 @@ +{ + "field1": "field 1 value", + "field2": "another value", + "attachment": { + "$xml": "" + } +} \ No newline at end of file diff --git a/test/request-response-samples/attachments__should_send_binary_data/response.json b/test/request-response-samples/attachments__should_send_binary_data/response.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/test/request-response-samples/attachments__should_send_binary_data/response.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/request-response-samples/attachments__should_send_binary_data/soap.wsdl b/test/request-response-samples/attachments__should_send_binary_data/soap.wsdl new file mode 100644 index 000000000..e94deee9b --- /dev/null +++ b/test/request-response-samples/attachments__should_send_binary_data/soap.wsdl @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/test/static/nodejs.png b/test/static/nodejs.png new file mode 100644 index 000000000..4c1c5b63b Binary files /dev/null and b/test/static/nodejs.png differ diff --git a/test/wsdl/attachments.wsdl b/test/wsdl/attachments.wsdl new file mode 100644 index 000000000..3b541272a --- /dev/null +++ b/test/wsdl/attachments.wsdl @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +