Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidpelayo/e091bbaeaeaec0aa469aafbe817843c2 to your computer and use it in GitHub Desktop.
Save davidpelayo/e091bbaeaeaec0aa469aafbe817843c2 to your computer and use it in GitHub Desktop.
Zapier Node/JS step performing HTTP Digest Auth + Multipart/form-data example
// Kudos for this repo: https://github.com/simme/node-http-digest-client
// Original `node-http-digest-client` license: Copyright (c) 2012, Simon Ljungberg <hi@iamsim.me>
// Adapted by @ddpelayo / github.com/davidpelayo / davidpelayo.com - to Modern Javascript
const HTTPDigest = function () {
const crypto = require('crypto');
let http = require('http');
const HTTPDigest = function (username, password, https) {
this.nc = 0;
this.username = username;
this.password = password;
if (https === true) {
http = require('https');
}
};
HTTPDigest.prototype.request = function (options, callback) {
const firstRequest = http.request({
...options,
postData: undefined
}, (firstRequestResponse) => this._handleResponse(options, firstRequestResponse, callback));
for (let data of options.postData) {
firstRequest.write(data);
}
return firstRequest.end();
};
HTTPDigest.prototype._handleResponse = function (options, res, callback) {
const challenge = this._parseChallenge(res.headers['www-authenticate']);
const ha1 = crypto.createHash('md5');
ha1.update([this.username, challenge.realm, this.password].join(':'));
const ha2 = crypto.createHash('md5');
ha2.update([options.method, options.path].join(':'));
// Generate cnonce
let cnonce = false;
let nc = false;
if (typeof challenge.qop === 'string') {
var cnonceHash = crypto.createHash('md5');
cnonceHash.update(Math.random().toString(36));
cnonce = cnonceHash.digest('hex').substr(0, 8);
nc = this.updateNC();
}
// Generate response hash
const response = crypto.createHash('md5');
const responseParams = [
ha1.digest('hex'),
challenge.nonce
];
if (cnonce) {
responseParams.push(nc);
responseParams.push(cnonce);
}
responseParams.push(challenge.qop);
responseParams.push(ha2.digest('hex'));
response.update(responseParams.join(':'));
// Setup response parameters
const authParams = {
username: this.username,
realm: challenge.realm,
nonce: challenge.nonce,
uri: options.path,
qop: challenge.qop,
response: response.digest('hex'),
opaque: challenge.opaque
};
if (cnonce) {
authParams.nc = nc;
authParams.cnonce = cnonce;
}
const headers = options.headers || {};
headers.Authorization = this._compileParams(authParams);
options.headers = headers;
const realmedRequest = http.request({
...options,
postData: undefined
}, callback);
for (let data of options.postData) {
realmedRequest.write(data);
}
realmedRequest.end();
};
HTTPDigest.prototype._parseChallenge = function (digest) {
const prefix = "Digest ";
const challenge = digest.substr(digest.indexOf(prefix) + prefix.length);
const parts = challenge.split(',');
const length = parts.length;
const params = {};
for (let i = 0; i < length; i++) {
const part = parts[i].match(/^\s*?([a-zA-Z0-0]+)="(.*)"\s*?$/);
if (part && part.length > 2) {
params[part[1]] = part[2];
}
}
return params;
};
HTTPDigest.prototype._compileParams = function (params) {
const parts = [];
for (let i in params) {
parts.push(i + '="' + params[i] + '"');
}
return 'Digest ' + parts.join(',');
};
HTTPDigest.prototype.updateNC = function () {
const max = 99999999;
this.nc++;
if (this.nc > max) {
this.nc = 1;
}
const padding = new Array(8).join('0') + "";
const nc = this.nc + "";
return padding.substr(0, 8 - nc.length) + nc;
};
// Return response handler
return HTTPDigest;
}();
// Look first how HTTP Digest Auth works: https://en.wikipedia.org/wiki/Digest_access_authentication
// Or Google, to know what the above `HTTPDigest` module is doing
// Read how the Zapier scripts work: https://zapier.com/help/create/code-webhooks/javascript-code-examples-in-zaps#step-1
const csvAttachmentData = `Some;csv;file`;
const boundary = '__X_PAW_BOUNDARY__';
const postData = [`--${boundary}`, `\r\nContent-Disposition: form-data; name=\"file\"; filename=\"file.csv\"\r\n`, `Content-Type: text/csv \r\n\r\n`, csvAttachmentData, `\r\n--${boundary}\r\n`];
const postDataLength = postData.reduce((acc, curr) => acc += Buffer.byteLength(curr), 0)
const digestClient = new HTTPDigest('user', 'password', true);
const digestRequestPayload = {
host: 'httpbin.org',
path: 'digest-auth/auth/user/pass',
port: 443,
method: 'POST',
headers: {
'Cache-Control': 'no-cache',
'Content-Type': `multipart/form-data; charset=utf-8; boundary=${boundary}`,
'Content-Length': postDataLength,
'Accept': '*/*'
},
postData
};
const digestRequest = digestClient.request(digestRequestPayload, function (secondRequestResponse) {
secondRequestResponse.on('data', (data) => {
console.log('[Second request] data:', data.toString());
callback(null, [{
csv: csvAttachmentData
}])
});
secondRequestResponse.on('error', () => {
console.log('[Second request] Some error happened')
callback(err.message, [])
})
secondRequestResponse.on('end', () => console.log('[Second request] connection ended'));
});
var output = [{
id: digestRequest,
hello: "world",
}];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment