Skip to content

Commit

Permalink
correctly serialize MTOM into axios data
Browse files Browse the repository at this point in the history
  • Loading branch information
ischisan committed Nov 12, 2021
1 parent d284fea commit 788b1d8
Showing 1 changed file with 150 additions and 84 deletions.
234 changes: 150 additions & 84 deletions src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@
* MIT Licensed
*/

import * as req from 'axios';
import { NtlmClient } from 'axios-ntlm';
import * as contentTypeParser from 'content-type-parser';
import * as debugBuilder from 'debug';
import { ReadStream } from 'fs';
import * as url from 'url';
import * as req from "axios";
import { NtlmClient } from "axios-ntlm";
import * as contentTypeParser from "content-type-parser";
import * as debugBuilder from "debug";
import { ReadStream } from "fs";
import * as url from "url";

import { v4 as uuidv4 } from 'uuid';
import { IExOptions, IHeaders, IHttpClient, IMTOMAttachments, IOptions } from './types';
import { parseMTOMResp } from './utils';
import { v4 as uuidv4 } from "uuid";
import {
IExOptions,
IHeaders,
IHttpClient,
IMTOMAttachments,
IOptions,
} from "./types";
import { parseMTOMResp } from "./utils";

const debug = debugBuilder('node-soap');
const VERSION = require('../package.json').version;
const debug = debugBuilder("node-soap");
const VERSION = require("../package.json").version;

export interface IAttachment {
name: string;
Expand All @@ -32,7 +38,6 @@ export interface IAttachment {
* @constructor
*/
export class HttpClient implements IHttpClient {

private _request: req.AxiosInstance;
private options: IOptions;

Expand All @@ -50,30 +55,44 @@ export class HttpClient implements IHttpClient {
* @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 method = data ? 'POST' : 'GET';
const secure = curl.protocol === 'https:';
const path = [curl.pathname || '/', curl.search || '', curl.hash || ''].join('');
const method = data ? "POST" : "GET";
const secure = curl.protocol === "https:";
const path = [
curl.pathname || "/",
curl.search || "",
curl.hash || "",
].join("");

const host = curl.hostname;
const port = parseInt(curl.port, 10);
const headers: IHeaders = {
'User-Agent': 'node-soap/' + VERSION,
'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.forever ? 'keep-alive' : 'close',
'Host': host + (isNaN(port) ? '' : ':' + port),
"User-Agent": "node-soap/" + VERSION,
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.forever ? "keep-alive" : "close",
Host: host + (isNaN(port) ? "" : ":" + port),
};
const mergeOptions = ['headers'];
const mergeOptions = ["headers"];

const { attachments: _attachments, ...newExoptions } = exoptions;
const attachments: IAttachment[] = _attachments || [];

if (typeof data === 'string' && attachments.length === 0 && !exoptions.forceMTOM) {
headers['Content-Length'] = Buffer.byteLength(data, 'utf8');
headers['Content-Type'] = 'application/x-www-form-urlencoded';
if (
typeof data === "string" &&
attachments.length === 0 &&
!exoptions.forceMTOM
) {
headers["Content-Length"] = Buffer.byteLength(data, "utf8");
headers["Content-Type"] = "application/x-www-form-urlencoded";
}

exheaders = exheaders || {};
Expand All @@ -93,33 +112,56 @@ export class HttpClient implements IHttpClient {
if (exoptions.forceMTOM || attachments.length > 0) {
const start = uuidv4();
let action = null;
if (headers['Content-Type'].indexOf('action') > -1) {
for (const ct of headers['Content-Type'].split('; ')) {
if (ct.indexOf('action') > -1) {
if (headers["Content-Type"].indexOf("action") > -1) {
for (const ct of headers["Content-Type"].split("; ")) {
if (ct.indexOf("action") > -1) {
action = ct;
}
}
}
headers['Content-Type'] = 'multipart/related; type="application/xop+xml"; start="<' + start + '>"; start-info="text/xml"; boundary=' + uuidv4();
const boundary = uuidv4();
headers["Content-Type"] =
'multipart/related; type="application/xop+xml"; start="<' +
start +
'>"; type="text/xml"; boundary=' +
boundary;
if (action) {
headers['Content-Type'] = headers['Content-Type'] + '; ' + action;
headers["Content-Type"] = headers["Content-Type"] + "; " + action;
}
const multipart: any[] = [{
'Content-Type': 'application/xop+xml; charset=UTF-8; type="text/xml"',
'Content-ID': '<' + start + '>',
'body': data,
}];
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,
"Content-Type": attachment.mimetype,
"Content-Transfer-Encoding": "binary",
"Content-ID": "<" + attachment.contentId + ">",
"Content-Disposition":
'attachment; filename="' + attachment.name + '"',
body: attachment.body,
});
});

options.data = `--${boundary}\r\n`;

let multipartCount = 0;
multipart.forEach((part) => {
Object.keys(part).forEach((key) => {
if (key !== "body") {
options.data += `${key}: ${part[key]}\r\n`;
}
});
options.data += "\r\n";
options.data += `${part.body}\r\n--${boundary}${
multipartCount === multipart.length - 1 ? "--" : ""
}\r\n`;
multipartCount++;
});
// options.multipart = multipart;
} else {
options.data = data;
}
Expand All @@ -133,7 +175,7 @@ export class HttpClient implements IHttpClient {
options[attr] = exoptions[attr];
}
}
debug('Http request: %j', options);
debug("Http request: %j", options);
return options;
}

Expand All @@ -144,12 +186,17 @@ export class HttpClient implements IHttpClient {
* @param {Object} body The http body
* @param {Object} The parsed body
*/
public handleResponse(req: req.AxiosPromise, res: req.AxiosResponse, body: any) {
debug('Http response body: %j', body);
if (typeof body === 'string') {
public handleResponse(
req: req.AxiosPromise,
res: req.AxiosResponse,
body: any
) {
debug("Http response body: %j", body);
if (typeof body === "string") {
// Remove any extra characters that appear before or after the SOAP envelope.
const regex = /(?:<\?[^?]*\?>[\s]*)?<([^:]*):Envelope([\S\s]*)<\/\1:Envelope>/i;
const match = body.replace(/<!--[\s\S]*?-->/, '').match(regex);
const regex =
/(?:<\?[^?]*\?>[\s]*)?<([^:]*):Envelope([\S\s]*)<\/\1:Envelope>/i;
const match = body.replace(/<!--[\s\S]*?-->/, "").match(regex);
if (match) {
body = match[0];
}
Expand All @@ -163,65 +210,84 @@ export class HttpClient implements IHttpClient {
callback: (error: any, res?: any, body?: any) => any,
exheaders?: IHeaders,
exoptions?: IExOptions,
caller?,
caller?
) {
const options = this.buildRequest(rurl, data, exheaders, exoptions);
let req: req.AxiosPromise;
if (exoptions !== undefined && exoptions.ntlm) {
const ntlmReq = NtlmClient({
username: exoptions.username,
password: exoptions.password,
workstation: exoptions.workstation || '',
domain: exoptions.domain || '',
workstation: exoptions.workstation || "",
domain: exoptions.domain || "",
});
req = ntlmReq(options);
} else {
if (this.options.parseReponseAttachments) {
options.responseType = 'arraybuffer';
options.responseEncoding = 'binary';
options.responseType = "arraybuffer";
options.responseEncoding = "binary";
}
req = this._request(options);
}
const _this = this;
req.then((res) => {
let body;
if (_this.options.parseReponseAttachments) {
const isMultipartResp = res.headers['content-type'] && res.headers['content-type'].toLowerCase().indexOf('multipart/related') > -1;
if (isMultipartResp) {
let boundary;
const parsedContentType = contentTypeParser(res.headers['content-type']);
if (parsedContentType && parsedContentType.parameterList) {
boundary = ((parsedContentType.parameterList as any[]).find((item) => item.key === 'boundary') || {}).value;
}
if (!boundary) {
return callback(new Error('Missing boundary from content-type'));
}
const multipartResponse = parseMTOMResp(res.data, boundary);
req.then(
(res) => {
let body;
if (_this.options.parseReponseAttachments) {
const isMultipartResp =
res.headers["content-type"] &&
res.headers["content-type"]
.toLowerCase()
.indexOf("multipart/related") > -1;
if (isMultipartResp) {
let boundary;
const parsedContentType = contentTypeParser(
res.headers["content-type"]
);
if (parsedContentType && parsedContentType.parameterList) {
boundary = (
(parsedContentType.parameterList as any[]).find(
(item) => item.key === "boundary"
) || {}
).value;
}
if (!boundary) {
return callback(new Error("Missing boundary from content-type"));
}
const multipartResponse = parseMTOMResp(res.data, boundary);

// first part is the soap response
const firstPart = multipartResponse.parts.shift();
if (!firstPart || !firstPart.body) {
return callback(new Error('Cannot parse multipart response'));
// first part is the soap response
const firstPart = multipartResponse.parts.shift();
if (!firstPart || !firstPart.body) {
return callback(new Error("Cannot parse multipart response"));
}
body = firstPart.body.toString("utf8");
(res as any).mtomResponseAttachments = multipartResponse;
} else {
body = res.data.toString("utf8");
}
body = firstPart.body.toString('utf8');
(res as any).mtomResponseAttachments = multipartResponse;
} else {
body = res.data.toString('utf8');
}
}

res.data = this.handleResponse(req, res, body || res.data);
callback(null, res, res.data);
return res;
}, (err) => {
return callback(err);
});
res.data = this.handleResponse(req, res, body || res.data);
callback(null, res, res.data);
return res;
},
(err) => {
return callback(err);
}
);
return req;
}

public requestStream(rurl: string, data: any, exheaders?: IHeaders, exoptions?: IExOptions, caller?): req.AxiosPromise<ReadStream> {
public requestStream(
rurl: string,
data: any,
exheaders?: IHeaders,
exoptions?: IExOptions,
caller?
): req.AxiosPromise<ReadStream> {
const options = this.buildRequest(rurl, data, exheaders, exoptions);
options.responseType = 'stream';
options.responseType = "stream";
const req = this._request(options).then((res) => {
res.data = this.handleResponse(req, res, res.data);
return res;
Expand Down

0 comments on commit 788b1d8

Please sign in to comment.