You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

768 lines
28 KiB
JavaScript

// Generated by CoffeeScript 1.9.3
var Promise, S, _, addErrorData, bhttpAPI, bhttpErrors, concatStream, createCookieJar, debug, debugRequest, debugResponse, devNull, doPayloadRequest, doRedirect, errors, extend, formData, formFixArray, http, https, isStream, makeRequest, ofTypes, packageConfig, prepareCleanup, prepareDefaults, prepareOptions, preparePayload, prepareProtocol, prepareRequest, prepareSession, prepareUrl, processResponse, querystring, redirectGet, redirectUnchanged, sink, spy, stream, streamLength, toughCookie, urlUtil, util;
urlUtil = require("url");
querystring = require("querystring");
stream = require("stream");
http = require("http");
https = require("https");
util = require("util");
Promise = require("bluebird");
_ = require("lodash");
S = require("string");
formFixArray = require("form-fix-array");
errors = require("errors");
debug = require("debug");
debugRequest = debug("bhttp:request");
debugResponse = debug("bhttp:response");
extend = require("extend");
devNull = require("dev-null");
formData = require("form-data2");
concatStream = require("concat-stream");
toughCookie = require("tough-cookie");
streamLength = require("stream-length");
sink = require("through2-sink");
spy = require("through2-spy");
packageConfig = require("../package.json");
bhttpErrors = {};
errors.create({
name: "bhttpError",
scope: bhttpErrors
});
errors.create({
name: "ConflictingOptionsError",
parents: bhttpErrors.bhttpError,
scope: bhttpErrors
});
errors.create({
name: "UnsupportedProtocolError",
parents: bhttpErrors.bhttpError,
scope: bhttpErrors
});
errors.create({
name: "RedirectError",
parents: bhttpErrors.bhttpError,
scope: bhttpErrors
});
errors.create({
name: "MultipartError",
parents: bhttpErrors.bhttpError,
scope: bhttpErrors
});
errors.create({
name: "ConnectionTimeoutError",
parents: bhttpErrors.bhttpError,
scope: bhttpErrors
});
errors.create({
name: "ResponseTimeoutError",
parents: bhttpErrors.bhttpError,
scope: bhttpErrors
});
ofTypes = function(obj, types) {
var i, len, match, type;
match = false;
for (i = 0, len = types.length; i < len; i++) {
type = types[i];
match = match || obj instanceof type;
}
return match;
};
addErrorData = function(err, request, response, requestState) {
err.request = request;
err.response = response;
err.requestState = requestState;
return err;
};
isStream = function(obj) {
return (obj != null) && (ofTypes(obj, [stream.Readable, stream.Duplex, stream.Transform]) || obj.hasOwnProperty("_bhttpStreamWrapper"));
};
prepareSession = function(request, response, requestState) {
debugRequest("preparing session");
return Promise["try"](function() {
if (requestState.sessionOptions != null) {
request.options = _.merge(_.clone(requestState.sessionOptions), request.options);
}
if (request.options.headers != null) {
request.options.headers = _.clone(request.options.headers, true);
} else {
request.options.headers = {};
}
if (request.options.cookieJar != null) {
return Promise["try"](function() {
request.cookieJar = request.options.cookieJar;
delete request.options.cookieJar;
return request.cookieJar.get(request.url);
}).then(function(cookieString) {
debugRequest("sending cookie string: %s", cookieString);
request.options.headers["cookie"] = cookieString;
return Promise.resolve([request, response, requestState]);
});
} else {
return Promise.resolve([request, response, requestState]);
}
});
};
prepareDefaults = function(request, response, requestState) {
debugRequest("preparing defaults");
return Promise["try"](function() {
var base, base1, base2, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7;
request.responseOptions = {
discardResponse: (ref = request.options.discardResponse) != null ? ref : false,
keepRedirectResponses: (ref1 = request.options.keepRedirectResponses) != null ? ref1 : false,
followRedirects: (ref2 = request.options.followRedirects) != null ? ref2 : true,
noDecode: (ref3 = request.options.noDecode) != null ? ref3 : false,
decodeJSON: (ref4 = request.options.decodeJSON) != null ? ref4 : false,
stream: (ref5 = request.options.stream) != null ? ref5 : false,
justPrepare: (ref6 = request.options.justPrepare) != null ? ref6 : false,
redirectLimit: (ref7 = request.options.redirectLimit) != null ? ref7 : 10,
onDownloadProgress: request.options.onDownloadProgress,
responseTimeout: request.options.responseTimeout
};
if ((base = request.options).allowChunkedMultipart == null) {
base.allowChunkedMultipart = false;
}
if ((base1 = request.options).forceMultipart == null) {
base1.forceMultipart = false;
}
if ((base2 = request.options.headers)["user-agent"] == null) {
base2["user-agent"] = "bhttp/" + packageConfig.version;
}
request.options.method = request.options.method.toLowerCase();
return Promise.resolve([request, response, requestState]);
});
};
prepareUrl = function(request, response, requestState) {
debugRequest("preparing URL");
return Promise["try"](function() {
var ref, urlOptions;
urlOptions = urlUtil.parse(request.url, true);
_.extend(request.options, {
hostname: urlOptions.hostname,
port: urlOptions.port
});
request.options.path = urlUtil.format({
pathname: urlOptions.pathname,
query: (ref = request.options.query) != null ? ref : urlOptions.query
});
request.protocol = S(urlOptions.protocol).chompRight(":").toString();
return Promise.resolve([request, response, requestState]);
});
};
prepareProtocol = function(request, response, requestState) {
debugRequest("preparing protocol");
return Promise["try"](function() {
var base;
request.protocolModule = (function() {
switch (request.protocol) {
case "http":
return http;
case "https":
return https;
default:
return null;
}
})();
if (request.protocolModule == null) {
return Promise.reject()(new bhttpErrors.UnsupportedProtocolError("The protocol specified (" + protocol + ") is not currently supported by this module."));
}
if ((base = request.options).port == null) {
base.port = (function() {
switch (request.protocol) {
case "http":
return 80;
case "https":
return 443;
}
})();
}
return Promise.resolve([request, response, requestState]);
});
};
prepareOptions = function(request, response, requestState) {
debugRequest("preparing options");
return Promise["try"](function() {
var base;
if (((request.options.formFields != null) || (request.options.files != null)) && ((request.options.inputStream != null) || (request.options.inputBuffer != null))) {
return Promise.reject(addErrorData(new bhttpErrors.ConflictingOptionsError("You cannot define both formFields/files and a raw inputStream or inputBuffer."), request, response, requestState));
}
if (request.options.encodeJSON && ((request.options.inputStream != null) || (request.options.inputBuffer != null))) {
return Promise.reject(addErrorData(new bhttpErrors.ConflictingOptionsError("You cannot use both encodeJSON and a raw inputStream or inputBuffer.", void 0, "If you meant to JSON-encode the stream, you will currently have to do so manually."), request, response, requestState));
}
if (request.responseOptions.stream) {
if ((base = request.options).agent == null) {
base.agent = false;
}
}
return Promise.resolve([request, response, requestState]);
});
};
preparePayload = function(request, response, requestState) {
debugRequest("preparing payload");
return Promise["try"](function() {
var containsStreams, fieldName, fieldValue, formDataObject, i, len, multipart, ref, ref1, ref2, streamOptions, valueElement;
request.onUploadProgress = request.options.onUploadProgress;
multipart = request.options.forceMultipart || (request.options.files != null);
multipart = multipart || _.any(request.options.formFields, function(item) {
return item instanceof Buffer || isStream(item);
});
_.extend(request.options.formFields, request.options.files);
containsStreams = _.any(request.options.formFields, function(item) {
return isStream(item);
});
if (request.options.encodeJSON && containsStreams) {
return Promise.reject()(new bhttpErrors.ConflictingOptionsError("Sending a JSON-encoded payload containing data from a stream is not currently supported.", void 0, "Either don't use encodeJSON, or read your stream into a string or Buffer."));
}
if ((ref = request.options.method) !== "get" && ref !== "head" && ref !== "delete") {
if ((request.options.encodeJSON || (request.options.formFields != null)) && !multipart) {
debugRequest("got url-encodable form-data");
if (request.options.encodeJSON) {
debugRequest("... but encodeJSON was set, so we will send JSON instead");
request.options.headers["content-type"] = "application/json";
request.payload = JSON.stringify((ref1 = request.options.formFields) != null ? ref1 : null);
} else if (!_.isEmpty(request.options.formFields)) {
request.options.headers["content-type"] = "application/x-www-form-urlencoded";
request.payload = querystring.stringify(formFixArray(request.options.formFields));
} else {
request.payload = "";
}
request.options.headers["content-length"] = request.payload.length;
return Promise.resolve();
} else if ((request.options.formFields != null) && multipart) {
debugRequest("got multipart form-data");
formDataObject = new formData();
ref2 = formFixArray(request.options.formFields);
for (fieldName in ref2) {
fieldValue = ref2[fieldName];
if (!_.isArray(fieldValue)) {
fieldValue = [fieldValue];
}
for (i = 0, len = fieldValue.length; i < len; i++) {
valueElement = fieldValue[i];
if (valueElement._bhttpStreamWrapper != null) {
streamOptions = valueElement.options;
valueElement = valueElement.stream;
} else {
streamOptions = {};
}
formDataObject.append(fieldName, valueElement, streamOptions);
}
}
request.payloadStream = formDataObject;
return Promise["try"](function() {
return formDataObject.getHeaders();
}).then(function(headers) {
if (headers["content-transfer-encoding"] === "chunked" && !request.options.allowChunkedMultipart) {
return Promise.reject(addErrorData(new MultipartError("Most servers do not support chunked transfer encoding for multipart/form-data payloads, and we could not determine the length of all the input streams. See the documentation for more information."), request, response, requestState));
} else {
_.extend(request.options.headers, headers);
return Promise.resolve();
}
});
} else if (request.options.inputStream != null) {
debugRequest("got inputStream");
return Promise["try"](function() {
var ref3;
request.payloadStream = request.options.inputStream;
if ((request.payloadStream._bhttpStreamWrapper != null) && ((request.payloadStream.options.contentLength != null) || (request.payloadStream.options.knownLength != null))) {
return Promise.resolve((ref3 = request.payloadStream.options.contentLength) != null ? ref3 : request.payloadStream.options.knownLength);
} else {
return streamLength(request.options.inputStream);
}
}).then(function(length) {
debugRequest("length for inputStream is %s", length);
return request.options.headers["content-length"] = length;
})["catch"](function(err) {
debugRequest("unable to determine inputStream length, switching to chunked transfer encoding");
return request.options.headers["content-transfer-encoding"] = "chunked";
});
} else if (request.options.inputBuffer != null) {
debugRequest("got inputBuffer");
if (typeof request.options.inputBuffer === "string") {
request.payload = new Buffer(request.options.inputBuffer);
} else {
request.payload = request.options.inputBuffer;
}
debugRequest("length for inputBuffer is %s", request.payload.length);
request.options.headers["content-length"] = request.payload.length;
return Promise.resolve();
} else {
return Promise.resolve();
}
} else {
return Promise.resolve();
}
}).then(function() {
return Promise.resolve([request, response, requestState]);
});
};
prepareCleanup = function(request, response, requestState) {
debugRequest("preparing cleanup");
return Promise["try"](function() {
var fixedHeaders, i, key, len, ref, ref1, value;
ref = ["query", "formFields", "files", "encodeJSON", "inputStream", "inputBuffer", "discardResponse", "keepRedirectResponses", "followRedirects", "noDecode", "decodeJSON", "allowChunkedMultipart", "forceMultipart", "onUploadProgress", "onDownloadProgress"];
for (i = 0, len = ref.length; i < len; i++) {
key = ref[i];
delete request.options[key];
}
fixedHeaders = {};
ref1 = request.options.headers;
for (key in ref1) {
value = ref1[key];
fixedHeaders[key.toLowerCase()] = value;
}
request.options.headers = fixedHeaders;
return Promise.resolve([request, response, requestState]);
});
};
prepareRequest = function(request, response, requestState) {
debugRequest("preparing request");
return Promise["try"](function() {
var middlewareFunctions, promiseChain;
middlewareFunctions = [prepareSession, prepareDefaults, prepareUrl, prepareProtocol, prepareOptions, preparePayload, prepareCleanup];
promiseChain = Promise.resolve([request, response, requestState]);
middlewareFunctions.forEach(function(middleware) {
return promiseChain = promiseChain.spread(function(_request, _response, _requestState) {
return middleware(_request, _response, _requestState);
});
});
return promiseChain;
});
};
makeRequest = function(request, response, requestState) {
debugRequest("making %s request to %s", request.options.method.toUpperCase(), request.url);
return Promise["try"](function() {
var req, timeoutTimer;
req = request.protocolModule.request(request.options);
timeoutTimer = null;
return new Promise(function(resolve, reject) {
var completedBytes, progressStream, totalBytes;
if (request.responseOptions.responseTimeout != null) {
debugRequest("setting response timeout timer to " + request.responseOptions.responseTimeout + "ms...");
req.on("socket", function(socket) {
var timeoutHandler;
timeoutHandler = function() {
debugRequest("a response timeout occurred!");
req.abort();
return reject(addErrorData(new bhttpErrors.ResponseTimeoutError("The response timed out.")));
};
return timeoutTimer = setTimeout(timeoutHandler, request.responseOptions.responseTimeout);
});
}
totalBytes = request.options.headers["content-length"];
completedBytes = 0;
progressStream = spy(function(chunk) {
completedBytes += chunk.length;
return req.emit("progress", completedBytes, totalBytes);
});
if (request.onUploadProgress != null) {
req.on("progress", function(completedBytes, totalBytes) {
return request.onUploadProgress(completedBytes, totalBytes, req);
});
}
if (request.payload != null) {
debugRequest("sending payload");
req.emit("progress", request.payload.length, request.payload.length);
req.write(request.payload);
req.end();
} else if (request.payloadStream != null) {
debugRequest("piping payloadStream");
if (request.payloadStream._bhttpStreamWrapper != null) {
request.payloadStream.stream.pipe(progressStream).pipe(req);
} else {
request.payloadStream.pipe(progressStream).pipe(req);
}
} else {
debugRequest("closing request without payload");
req.end();
}
req.on("error", function(err) {
if (err.code === "ETIMEDOUT") {
debugRequest("a connection timeout occurred!");
return reject(addErrorData(new bhttpErrors.ConnectionTimeoutError("The connection timed out.")));
} else {
return reject(err);
}
});
return req.on("response", function(res) {
if (timeoutTimer != null) {
debugResponse("got response in time, clearing response timeout timer");
clearTimeout(timeoutTimer);
}
return resolve(res);
});
});
}).then(function(response) {
return Promise.resolve([request, response, requestState]);
});
};
processResponse = function(request, response, requestState) {
debugResponse("processing response, got status code %s", response.statusCode);
return Promise["try"](function() {
var cookieHeader, promises;
if ((request.cookieJar != null) && (response.headers["set-cookie"] != null)) {
promises = (function() {
var i, len, ref, results;
ref = response.headers["set-cookie"];
results = [];
for (i = 0, len = ref.length; i < len; i++) {
cookieHeader = ref[i];
debugResponse("storing cookie: %s", cookieHeader);
results.push(request.cookieJar.set(cookieHeader, request.url));
}
return results;
})();
return Promise.all(promises);
} else {
return Promise.resolve();
}
}).then(function() {
var completedBytes, progressStream, ref, ref1, totalBytes;
response.request = request;
response.requestState = requestState;
response.redirectHistory = requestState.redirectHistory;
if (((ref = response.statusCode) === 301 || ref === 302 || ref === 303 || ref === 307) && request.responseOptions.followRedirects) {
if (requestState.redirectHistory.length >= (request.responseOptions.redirectLimit - 1)) {
return Promise.reject(addErrorData(new bhttpErrors.RedirectError("The maximum amount of redirects ({request.responseOptions.redirectLimit}) was reached.")));
}
switch (response.statusCode) {
case 301:
switch (request.options.method) {
case "get":
case "head":
return redirectUnchanged(request, response, requestState);
case "post":
case "put":
case "patch":
case "delete":
return Promise.reject(addErrorData(new bhttpErrors.RedirectError("Encountered a 301 redirect for POST, PUT, PATCH or DELETE. RFC says we can't automatically continue."), request, response, requestState));
default:
return Promise.reject(addErrorData(new bhttpErrors.RedirectError("Encountered a 301 redirect, but not sure how to proceed for the " + (request.options.method.toUpperCase()) + " method.")));
}
break;
case 302:
case 303:
return redirectGet(request, response, requestState);
case 307:
if (request.containsStreams && ((ref1 = request.options.method) !== "get" && ref1 !== "head")) {
return Promise.reject(addErrorData(new bhttpErrors.RedirectError("Encountered a 307 redirect for POST, PUT or DELETE, but your payload contained (single-use) streams. We therefore can't automatically follow the redirect."), request, response, requestState));
} else {
return redirectUnchanged(request, response, requestState);
}
}
} else if (request.responseOptions.discardResponse) {
response.pipe(devNull());
return Promise.resolve(response);
} else {
totalBytes = response.headers["content-length"];
if (totalBytes != null) {
totalBytes = parseInt(totalBytes);
}
completedBytes = 0;
progressStream = sink(function(chunk) {
completedBytes += chunk.length;
return response.emit("progress", completedBytes, totalBytes);
});
if (request.responseOptions.onDownloadProgress != null) {
response.on("progress", function(completedBytes, totalBytes) {
return request.responseOptions.onDownloadProgress(completedBytes, totalBytes, response);
});
}
return new Promise(function(resolve, reject) {
var _on, _progressStreamAttached, _resume, attachProgressStream;
_resume = response.resume.bind(response);
_on = response.on.bind(response);
_progressStreamAttached = false;
attachProgressStream = function() {
if (!_progressStreamAttached) {
debugResponse("attaching progress stream");
_progressStreamAttached = true;
return response.pipe(progressStream);
}
};
response.on = function(eventName, handler) {
debugResponse("'on' called, " + eventName);
if (eventName === "data" || eventName === "readable") {
attachProgressStream();
}
return _on(eventName, handler);
};
response.resume = function() {
attachProgressStream();
return _resume();
};
if (request.responseOptions.stream) {
return resolve(response);
} else {
response.on("error", function(err) {
return reject(err);
});
return response.pipe(concatStream(function(body) {
var err, ref2;
if (request.responseOptions.decodeJSON || (((ref2 = response.headers["content-type"]) != null ? ref2 : "").split(";")[0] === "application/json" && !request.responseOptions.noDecode)) {
try {
response.body = JSON.parse(body);
} catch (_error) {
err = _error;
reject(err);
}
} else {
response.body = body;
}
return resolve(response);
}));
}
});
}
}).then(function(response) {
return Promise.resolve([request, response, requestState]);
});
};
doPayloadRequest = function(url, data, options, callback) {
if (isStream(data)) {
options.inputStream = data;
} else if (ofTypes(data, [Buffer]) || typeof data === "string") {
options.inputBuffer = data;
} else {
options.formFields = data;
}
return this.request(url, options, callback);
};
redirectGet = function(request, response, requestState) {
debugResponse("following forced-GET redirect to %s", response.headers["location"]);
return Promise["try"](function() {
var i, key, len, options, ref;
options = _.clone(requestState.originalOptions);
options.method = "get";
ref = ["inputBuffer", "inputStream", "files", "formFields"];
for (i = 0, len = ref.length; i < len; i++) {
key = ref[i];
delete options[key];
}
return doRedirect(request, response, requestState, options);
});
};
redirectUnchanged = function(request, response, requestState) {
debugResponse("following same-method redirect to %s", response.headers["location"]);
return Promise["try"](function() {
var options;
options = _.clone(requestState.originalOptions);
return doRedirect(request, response, requestState, options);
});
};
doRedirect = function(request, response, requestState, newOptions) {
return Promise["try"](function() {
if (!request.responseOptions.keepRedirectResponses) {
response.pipe(devNull());
}
requestState.redirectHistory.push(response);
return bhttpAPI._doRequest(urlUtil.resolve(request.url, response.headers["location"]), newOptions, requestState);
});
};
createCookieJar = function(jar) {
return {
set: function(cookie, url) {
return new Promise((function(_this) {
return function(resolve, reject) {
return _this.jar.setCookie(cookie, url, function(err, cookie) {
if (err) {
return reject(err);
} else {
return resolve(cookie);
}
});
};
})(this));
},
get: function(url) {
return new Promise((function(_this) {
return function(resolve, reject) {
return _this.jar.getCookieString(url, function(err, cookies) {
if (err) {
return reject(err);
} else {
return resolve(cookies);
}
});
};
})(this));
},
jar: jar
};
};
bhttpAPI = {
head: function(url, options, callback) {
if (options == null) {
options = {};
}
options.method = "head";
return this.request(url, options, callback);
},
get: function(url, options, callback) {
if (options == null) {
options = {};
}
options.method = "get";
return this.request(url, options, callback);
},
post: function(url, data, options, callback) {
if (options == null) {
options = {};
}
options.method = "post";
return doPayloadRequest.bind(this)(url, data, options, callback);
},
put: function(url, data, options, callback) {
if (options == null) {
options = {};
}
options.method = "put";
return doPayloadRequest.bind(this)(url, data, options, callback);
},
patch: function(url, data, options, callback) {
if (options == null) {
options = {};
}
options.method = "patch";
return doPayloadRequest.bind(this)(url, data, options, callback);
},
"delete": function(url, options, callback) {
if (options == null) {
options = {};
}
options.method = "delete";
return this.request(url, options, callback);
},
request: function(url, options, callback) {
if (options == null) {
options = {};
}
return this._doRequest(url, options).nodeify(callback);
},
_doRequest: function(url, options, requestState) {
return Promise["try"]((function(_this) {
return function() {
var ref, request, response;
request = {
url: url,
options: _.clone(options)
};
response = null;
if (requestState == null) {
requestState = {
originalOptions: _.clone(options),
redirectHistory: []
};
}
if (requestState.sessionOptions == null) {
requestState.sessionOptions = (ref = _this._sessionOptions) != null ? ref : {};
}
return prepareRequest(request, response, requestState);
};
})(this)).spread((function(_this) {
return function(request, response, requestState) {
if (request.responseOptions.justPrepare) {
return Promise.resolve([request, response, requestState]);
} else {
return Promise["try"](function() {
return bhttpAPI.executeRequest(request, response, requestState);
}).spread(function(request, response, requestState) {
return Promise.resolve(response);
});
}
};
})(this));
},
executeRequest: function(request, response, requestState) {
return Promise["try"](function() {
return makeRequest(request, response, requestState);
}).spread(function(request, response, requestState) {
return processResponse(request, response, requestState);
});
},
session: function(options) {
var key, session, value;
if (options == null) {
options = {};
}
options = _.clone(options);
session = {};
for (key in this) {
value = this[key];
if (value instanceof Function) {
value = value.bind(session);
}
session[key] = value;
}
if (options.cookieJar == null) {
options.cookieJar = createCookieJar(new toughCookie.CookieJar());
} else if (options.cookieJar === false) {
delete options.cookieJar;
} else {
options.cookieJar = createCookieJar(options.cookieJar);
}
session._sessionOptions = options;
return session;
},
wrapStream: function(stream, options) {
return {
_bhttpStreamWrapper: true,
stream: stream,
options: options
};
}
};
extend(bhttpAPI, bhttpErrors);
module.exports = bhttpAPI;