848 lines
32 KiB
JavaScript
848 lines
32 KiB
JavaScript
![]() |
"use strict";
|
||
|
|
||
|
var error = require("./error");
|
||
|
var fs = require("fs");
|
||
|
var mime = require("mime");
|
||
|
var Util = require("./util");
|
||
|
var Url = require("url");
|
||
|
|
||
|
/** section: github
|
||
|
* class Client
|
||
|
*
|
||
|
* Copyright 2012 Cloud9 IDE, Inc.
|
||
|
*
|
||
|
* This product includes software developed by
|
||
|
* Cloud9 IDE, Inc (http://c9.io).
|
||
|
*
|
||
|
* Author: Mike de Boer <mike@c9.io>
|
||
|
*
|
||
|
* [[Client]] can load any version of the [[github]] client API, with the
|
||
|
* requirement that a valid routes.json definition file is present in the
|
||
|
* `api/[VERSION]` directory and that the routes found in this file are
|
||
|
* implemented as well.
|
||
|
*
|
||
|
* Upon instantiation of the [[Client]] class, the routes.json file is loaded
|
||
|
* from the API version specified in the configuration and, parsed and from it
|
||
|
* the routes for HTTP requests are extracted. For each HTTP endpoint to the
|
||
|
* HTTP server, a method is generated which accepts a Javascript Object
|
||
|
* with parameters and an optional callback to be invoked when the API request
|
||
|
* returns from the server or when the parameters could not be validated.
|
||
|
*
|
||
|
* When an HTTP endpoint is processed and a method is generated as described
|
||
|
* above, [[Client]] also sets up parameter validation with the rules as
|
||
|
* defined in the routes.json. A full example that illustrates how this works
|
||
|
* is shown below:
|
||
|
*
|
||
|
* ##### Example
|
||
|
*
|
||
|
* First, we look at a listing of a sample routes.json routes definition file:
|
||
|
*
|
||
|
* {
|
||
|
* "defines": {
|
||
|
* "constants": {
|
||
|
* "name": "Github",
|
||
|
* "description": "A Node.JS module, which provides an object oriented wrapper for the GitHub v3 API.",
|
||
|
* "protocol": "https",
|
||
|
* "host": "api.github.com",
|
||
|
* "port": 443,
|
||
|
* "dateFormat": "YYYY-MM-DDTHH:MM:SSZ",
|
||
|
* "requestFormat": "json"
|
||
|
* },
|
||
|
* "response-headers": [
|
||
|
* "X-RateLimit-Limit",
|
||
|
* "X-RateLimit-Remaining",
|
||
|
* "Link"
|
||
|
* ],
|
||
|
* "params": {
|
||
|
* "files": {
|
||
|
* "type": "Json",
|
||
|
* "required": true,
|
||
|
* "validation": "",
|
||
|
* "invalidmsg": "",
|
||
|
* "description": "Files that make up this gist. The key of which should be a required string filename and the value another required hash with parameters: 'content'"
|
||
|
* },
|
||
|
* "user": {
|
||
|
* "type": "String",
|
||
|
* "required": true,
|
||
|
* "validation": "",
|
||
|
* "invalidmsg": "",
|
||
|
* "description": ""
|
||
|
* },
|
||
|
* "description": {
|
||
|
* "type": "String",
|
||
|
* "required": false,
|
||
|
* "validation": "",
|
||
|
* "invalidmsg": "",
|
||
|
* "description": ""
|
||
|
* },
|
||
|
* "page": {
|
||
|
* "type": "Number",
|
||
|
* "required": false,
|
||
|
* "validation": "^[0-9]+$",
|
||
|
* "invalidmsg": "",
|
||
|
* "description": "Page number of the results to fetch."
|
||
|
* },
|
||
|
* "per_page": {
|
||
|
* "type": "Number",
|
||
|
* "required": false,
|
||
|
* "validation": "^[0-9]+$",
|
||
|
* "invalidmsg": "",
|
||
|
* "description": "A custom page size up to 100. Default is 30."
|
||
|
* }
|
||
|
* }
|
||
|
* },
|
||
|
*
|
||
|
* "gists": {
|
||
|
* "get-from-user": {
|
||
|
* "url": ":user/gists",
|
||
|
* "method": "GET",
|
||
|
* "params": {
|
||
|
* "$user": null,
|
||
|
* "$page": null,
|
||
|
* "$per_page": null
|
||
|
* }
|
||
|
* },
|
||
|
*
|
||
|
* "create": {
|
||
|
* "url": "/gists",
|
||
|
* "method": "POST",
|
||
|
* "params": {
|
||
|
* "$description": null,
|
||
|
* "public": {
|
||
|
* "type": "Boolean",
|
||
|
* "required": true,
|
||
|
* "validation": "",
|
||
|
* "invalidmsg": "",
|
||
|
* "description": ""
|
||
|
* },
|
||
|
* "$files": null
|
||
|
* }
|
||
|
* }
|
||
|
* }
|
||
|
* }
|
||
|
*
|
||
|
* You probably noticed that the definition is quite verbose and the decision
|
||
|
* for its design was made to be verbose whilst still allowing for basic variable
|
||
|
* definitions and substitions for request parameters.
|
||
|
*
|
||
|
* There are two sections; 'defines' and 'gists' in this example.
|
||
|
*
|
||
|
* The `defines` section contains a list of `constants` that will be used by the
|
||
|
* [[Client]] to make requests to the right URL that hosts the API.
|
||
|
* The `gists` section defines the endpoints for calls to the API server, for
|
||
|
* gists specifically in this example, but the other API sections are defined in
|
||
|
* the exact same way.
|
||
|
* These definitions are parsed and methods are created that the client can call
|
||
|
* to make an HTTP request to the server.
|
||
|
* there is one endpoint defined: .
|
||
|
* In this example, the endpoint `gists/get-from-user` will be exposed as a member
|
||
|
* on the [[Client]] object and may be invoked with
|
||
|
*
|
||
|
* client.getFromUser({
|
||
|
* "user": "bob"
|
||
|
* }, function(err, ret) {
|
||
|
* // do something with the result here.
|
||
|
* });
|
||
|
*
|
||
|
* // or to fetch a specfic page:
|
||
|
* client.getFromUser({
|
||
|
* "user": "bob",
|
||
|
* "page": 2,
|
||
|
* "per_page": 100
|
||
|
* }, function(err, ret) {
|
||
|
* // do something with the result here.
|
||
|
* });
|
||
|
*
|
||
|
* All the parameters as specified in the Object that is passed to the function
|
||
|
* as first argument, will be validated according to the rules in the `params`
|
||
|
* block of the route definition.
|
||
|
* Thus, in the case of the `user` parameter, according to the definition in
|
||
|
* the `params` block, it's a variable that first needs to be looked up in the
|
||
|
* `params` block of the `defines` section (at the top of the JSON file). Params
|
||
|
* that start with a `$` sign will be substituted with the param with the same
|
||
|
* name from the `defines/params` section.
|
||
|
* There we see that it is a required parameter (needs to hold a value). In other
|
||
|
* words, if the validation requirements are not met, an HTTP error is passed as
|
||
|
* first argument of the callback.
|
||
|
*
|
||
|
* Implementation Notes: the `method` is NOT case sensitive, whereas `url` is.
|
||
|
* The `url` parameter also supports denoting parameters inside it as follows:
|
||
|
*
|
||
|
* "get-from-user": {
|
||
|
* "url": ":user/gists",
|
||
|
* "method": "GET"
|
||
|
* ...
|
||
|
* }
|
||
|
**/
|
||
|
var Client = module.exports = function(config) {
|
||
|
config.headers = config.headers || {};
|
||
|
this.config = config;
|
||
|
this.debug = Util.isTrue(config.debug);
|
||
|
|
||
|
this.version = config.version;
|
||
|
var cls = require("./api/v" + this.version);
|
||
|
this[this.version] = new cls(this);
|
||
|
|
||
|
var pathPrefix = "";
|
||
|
// Check if a prefix is passed in the config and strip any leading or trailing slashes from it.
|
||
|
if (typeof config.pathPrefix == "string") {
|
||
|
pathPrefix = "/" + config.pathPrefix.replace(/(^[\/]+|[\/]+$)/g, "");
|
||
|
this.config.pathPrefix = pathPrefix;
|
||
|
}
|
||
|
|
||
|
this.setupRoutes();
|
||
|
};
|
||
|
|
||
|
(function() {
|
||
|
/**
|
||
|
* Client#setupRoutes() -> null
|
||
|
*
|
||
|
* Configures the routes as defined in a routes.json file of an API version
|
||
|
*
|
||
|
* [[Client#setupRoutes]] is invoked by the constructor, takes the
|
||
|
* contents of the JSON document that contains the definitions of all the
|
||
|
* available API routes and iterates over them.
|
||
|
*
|
||
|
* It first recurses through each definition block until it reaches an API
|
||
|
* endpoint. It knows that an endpoint is found when the `url` and `param`
|
||
|
* definitions are found as a direct member of a definition block.
|
||
|
* Then the availability of an implementation by the API is checked; if it's
|
||
|
* not present, this means that a portion of the API as defined in the routes.json
|
||
|
* file is not implemented properly, thus an exception is thrown.
|
||
|
* After this check, a method is attached to the [[Client]] instance
|
||
|
* and becomes available for use. Inside this method, the parameter validation
|
||
|
* and typecasting is done, according to the definition of the parameters in
|
||
|
* the `params` block, upon invocation.
|
||
|
*
|
||
|
* This mechanism ensures that the handlers ALWAYS receive normalized data
|
||
|
* that is of the correct format and type. JSON parameters are parsed, Strings
|
||
|
* are trimmed, Numbers and Floats are casted and checked for NaN after that.
|
||
|
*
|
||
|
* Note: Query escaping for usage with SQL products is something that can be
|
||
|
* implemented additionally by adding an additional parameter type.
|
||
|
**/
|
||
|
this.setupRoutes = function() {
|
||
|
var self = this;
|
||
|
var api = this[this.version];
|
||
|
var routes = api.routes;
|
||
|
var defines = routes.defines;
|
||
|
this.constants = defines.constants;
|
||
|
this.requestHeaders = defines["request-headers"].map(function(header) {
|
||
|
return header.toLowerCase();
|
||
|
});
|
||
|
delete routes.defines;
|
||
|
|
||
|
function trim(s) {
|
||
|
if (typeof s != "string")
|
||
|
return s;
|
||
|
return s.replace(/^[\s\t\r\n]+/, "").replace(/[\s\t\r\n]+$/, "");
|
||
|
}
|
||
|
|
||
|
function parseParams(msg, paramsStruct) {
|
||
|
var params = Object.keys(paramsStruct);
|
||
|
var paramName, def, value, type;
|
||
|
for (var i = 0, l = params.length; i < l; ++i) {
|
||
|
paramName = params[i];
|
||
|
if (paramName.charAt(0) == "$") {
|
||
|
paramName = paramName.substr(1);
|
||
|
if (!defines.params[paramName]) {
|
||
|
throw new error.BadRequest("Invalid variable parameter name substitution; param '" +
|
||
|
paramName + "' not found in defines block", "fatal");
|
||
|
}
|
||
|
else {
|
||
|
def = paramsStruct[paramName] = defines.params[paramName];
|
||
|
delete paramsStruct["$" + paramName];
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
def = paramsStruct[paramName];
|
||
|
|
||
|
value = trim(msg[paramName]);
|
||
|
if (typeof value != "boolean" && !value) {
|
||
|
// we don't need to validation for undefined parameter values
|
||
|
// that are not required.
|
||
|
if (!def.required || (def["allow-empty"] && value === ""))
|
||
|
continue;
|
||
|
throw new error.BadRequest("Empty value for parameter '" +
|
||
|
paramName + "': " + value);
|
||
|
}
|
||
|
|
||
|
// validate the value and type of parameter:
|
||
|
if (def.validation) {
|
||
|
if (!new RegExp(def.validation).test(value)) {
|
||
|
throw new error.BadRequest("Invalid value for parameter '" +
|
||
|
paramName + "': " + value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (def.type) {
|
||
|
type = def.type.toLowerCase();
|
||
|
if (type == "number") {
|
||
|
value = parseInt(value, 10);
|
||
|
if (isNaN(value)) {
|
||
|
throw new error.BadRequest("Invalid value for parameter '" +
|
||
|
paramName + "': " + msg[paramName] + " is NaN");
|
||
|
}
|
||
|
}
|
||
|
else if (type == "float") {
|
||
|
value = parseFloat(value);
|
||
|
if (isNaN(value)) {
|
||
|
throw new error.BadRequest("Invalid value for parameter '" +
|
||
|
paramName + "': " + msg[paramName] + " is NaN");
|
||
|
}
|
||
|
}
|
||
|
else if (type == "json") {
|
||
|
if (typeof value == "string") {
|
||
|
try {
|
||
|
value = JSON.parse(value);
|
||
|
}
|
||
|
catch(ex) {
|
||
|
throw new error.BadRequest("JSON parse error of value for parameter '" +
|
||
|
paramName + "': " + value);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (type == "date") {
|
||
|
value = new Date(value);
|
||
|
}
|
||
|
}
|
||
|
msg[paramName] = value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function prepareApi(struct, baseType) {
|
||
|
if (!baseType)
|
||
|
baseType = "";
|
||
|
Object.keys(struct).forEach(function(routePart) {
|
||
|
var block = struct[routePart];
|
||
|
if (!block)
|
||
|
return;
|
||
|
var messageType = baseType + "/" + routePart;
|
||
|
if (block.url && block.params) {
|
||
|
// we ended up at an API definition part!
|
||
|
var endPoint = messageType.replace(/^[\/]+/g, "");
|
||
|
var parts = messageType.split("/");
|
||
|
var section = Util.toCamelCase(parts[1].toLowerCase());
|
||
|
parts.splice(0, 2);
|
||
|
var funcName = Util.toCamelCase(parts.join("-"));
|
||
|
|
||
|
if (!api[section]) {
|
||
|
throw new Error("Unsupported route section, not implemented in version " +
|
||
|
self.version + " for route '" + endPoint + "' and block: " +
|
||
|
JSON.stringify(block));
|
||
|
}
|
||
|
|
||
|
if (!api[section][funcName]) {
|
||
|
if (self.debug)
|
||
|
Util.log("Tried to call " + funcName);
|
||
|
throw new Error("Unsupported route, not implemented in version " +
|
||
|
self.version + " for route '" + endPoint + "' and block: " +
|
||
|
JSON.stringify(block));
|
||
|
}
|
||
|
|
||
|
if (!self[section]) {
|
||
|
self[section] = {};
|
||
|
// add a utility function 'getFooApi()', which returns the
|
||
|
// section to which functions are attached.
|
||
|
self[Util.toCamelCase("get-" + section + "-api")] = function() {
|
||
|
return self[section];
|
||
|
};
|
||
|
}
|
||
|
|
||
|
self[section][funcName] = function(msg, callback) {
|
||
|
try {
|
||
|
parseParams(msg, block.params);
|
||
|
}
|
||
|
catch (ex) {
|
||
|
// when the message was sent to the client, we can
|
||
|
// reply with the error directly.
|
||
|
api.sendError(ex, block, msg, callback);
|
||
|
if (self.debug)
|
||
|
Util.log(ex.message, "fatal");
|
||
|
// on error, there's no need to continue.
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
api[section][funcName].call(api, msg, block, callback);
|
||
|
};
|
||
|
}
|
||
|
else {
|
||
|
// recurse into this block next:
|
||
|
prepareApi(block, messageType);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
prepareApi(routes);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Client#authenticate(options) -> null
|
||
|
* - options (Object): Object containing the authentication type and credentials
|
||
|
* - type (String): One of the following: `basic` or `oauth`
|
||
|
* - username (String): Github username
|
||
|
* - password (String): Password to your account
|
||
|
* - token (String): OAuth2 token
|
||
|
*
|
||
|
* Set an authentication method to have access to protected resources.
|
||
|
*
|
||
|
* ##### Example
|
||
|
*
|
||
|
* // basic
|
||
|
* github.authenticate({
|
||
|
* type: "basic",
|
||
|
* username: "mikedeboertest",
|
||
|
* password: "test1324"
|
||
|
* });
|
||
|
*
|
||
|
* // or oauth
|
||
|
* github.authenticate({
|
||
|
* type: "oauth",
|
||
|
* token: "e5a4a27487c26e571892846366de023349321a73"
|
||
|
* });
|
||
|
*
|
||
|
* // or oauth key/ secret
|
||
|
* github.authenticate({
|
||
|
* type: "oauth",
|
||
|
* key: "clientID",
|
||
|
* secret: "clientSecret"
|
||
|
* });
|
||
|
*
|
||
|
* // or token
|
||
|
* github.authenticate({
|
||
|
* type: "token",
|
||
|
* token: "userToken",
|
||
|
* });
|
||
|
**/
|
||
|
this.authenticate = function(options) {
|
||
|
if (!options) {
|
||
|
this.auth = false;
|
||
|
return;
|
||
|
}
|
||
|
if (!options.type || "basic|oauth|client|token".indexOf(options.type) === -1)
|
||
|
throw new Error("Invalid authentication type, must be 'basic', 'oauth' or 'client'");
|
||
|
if (options.type == "basic" && (!options.username || !options.password))
|
||
|
throw new Error("Basic authentication requires both a username and password to be set");
|
||
|
if (options.type == "oauth") {
|
||
|
if (!options.token && !(options.key && options.secret))
|
||
|
throw new Error("OAuth2 authentication requires a token or key & secret to be set");
|
||
|
}
|
||
|
if (options.type == "token" && !options.token)
|
||
|
throw new Error("Token authentication requires a token to be set");
|
||
|
|
||
|
this.auth = options;
|
||
|
};
|
||
|
|
||
|
function getPageLinks(link) {
|
||
|
if (typeof link == "object" && (link.link || link.meta.link))
|
||
|
link = link.link || link.meta.link;
|
||
|
|
||
|
var links = {};
|
||
|
if (typeof link != "string")
|
||
|
return links;
|
||
|
|
||
|
// link format:
|
||
|
// '<https://api.github.com/users/aseemk/followers?page=2>; rel="next", <https://api.github.com/users/aseemk/followers?page=2>; rel="last"'
|
||
|
link.replace(/<([^>]*)>;\s*rel="([\w]*)\"/g, function(m, uri, type) {
|
||
|
links[type] = uri;
|
||
|
});
|
||
|
return links;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Client#hasNextPage(link) -> null
|
||
|
* - link (mixed): response of a request or the contents of the Link header
|
||
|
*
|
||
|
* Check if a request result contains a link to the next page
|
||
|
**/
|
||
|
this.hasNextPage = function(link) {
|
||
|
return getPageLinks(link).next;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Client#hasPreviousPage(link) -> null
|
||
|
* - link (mixed): response of a request or the contents of the Link header
|
||
|
*
|
||
|
* Check if a request result contains a link to the previous page
|
||
|
**/
|
||
|
this.hasPreviousPage = function(link) {
|
||
|
return getPageLinks(link).prev;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Client#hasLastPage(link) -> null
|
||
|
* - link (mixed): response of a request or the contents of the Link header
|
||
|
*
|
||
|
* Check if a request result contains a link to the last page
|
||
|
**/
|
||
|
this.hasLastPage = function(link) {
|
||
|
return getPageLinks(link).last;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Client#hasFirstPage(link) -> null
|
||
|
* - link (mixed): response of a request or the contents of the Link header
|
||
|
*
|
||
|
* Check if a request result contains a link to the first page
|
||
|
**/
|
||
|
this.hasFirstPage = function(link) {
|
||
|
return getPageLinks(link).first;
|
||
|
};
|
||
|
|
||
|
function getPage(link, which, callback) {
|
||
|
var url = getPageLinks(link)[which];
|
||
|
if (!url)
|
||
|
return callback(new error.NotFound("No " + which + " page found"));
|
||
|
|
||
|
var api = this[this.version];
|
||
|
var parsedUrl = Url.parse(url, true);
|
||
|
var block = {
|
||
|
url: parsedUrl.pathname,
|
||
|
method: "GET",
|
||
|
params: parsedUrl.query
|
||
|
};
|
||
|
this.httpSend(parsedUrl.query, block, function(err, res) {
|
||
|
if (err)
|
||
|
return api.sendError(err, null, parsedUrl.query, callback);
|
||
|
|
||
|
var ret;
|
||
|
try {
|
||
|
ret = res.data;
|
||
|
var contentType = res.headers["content-type"];
|
||
|
if (contentType && contentType.indexOf("application/json") !== -1)
|
||
|
ret = JSON.parse(ret);
|
||
|
}
|
||
|
catch (ex) {
|
||
|
if (callback)
|
||
|
callback(new error.InternalServerError(ex.message), res);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!ret)
|
||
|
ret = {};
|
||
|
if (typeof ret == "object") {
|
||
|
if (!ret.meta)
|
||
|
ret.meta = {};
|
||
|
["x-ratelimit-limit", "x-ratelimit-remaining", "link"].forEach(function(header) {
|
||
|
if (res.headers[header])
|
||
|
ret.meta[header] = res.headers[header];
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (callback)
|
||
|
callback(null, ret);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Client#getNextPage(link, callback) -> null
|
||
|
* - link (mixed): response of a request or the contents of the Link header
|
||
|
* - callback (Function): function to call when the request is finished with an error as first argument and result data as second argument.
|
||
|
*
|
||
|
* Get the next page, based on the contents of the `Link` header
|
||
|
**/
|
||
|
this.getNextPage = function(link, callback) {
|
||
|
getPage.call(this, link, "next", callback);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Client#getPreviousPage(link, callback) -> null
|
||
|
* - link (mixed): response of a request or the contents of the Link header
|
||
|
* - callback (Function): function to call when the request is finished with an error as first argument and result data as second argument.
|
||
|
*
|
||
|
* Get the previous page, based on the contents of the `Link` header
|
||
|
**/
|
||
|
this.getPreviousPage = function(link, callback) {
|
||
|
getPage.call(this, link, "prev", callback);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Client#getLastPage(link, callback) -> null
|
||
|
* - link (mixed): response of a request or the contents of the Link header
|
||
|
* - callback (Function): function to call when the request is finished with an error as first argument and result data as second argument.
|
||
|
*
|
||
|
* Get the last page, based on the contents of the `Link` header
|
||
|
**/
|
||
|
this.getLastPage = function(link, callback) {
|
||
|
getPage.call(this, link, "last", callback);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Client#getFirstPage(link, callback) -> null
|
||
|
* - link (mixed): response of a request or the contents of the Link header
|
||
|
* - callback (Function): function to call when the request is finished with an error as first argument and result data as second argument.
|
||
|
*
|
||
|
* Get the first page, based on the contents of the `Link` header
|
||
|
**/
|
||
|
this.getFirstPage = function(link, callback) {
|
||
|
getPage.call(this, link, "first", callback);
|
||
|
};
|
||
|
|
||
|
function getRequestFormat(hasBody, block) {
|
||
|
if (hasBody)
|
||
|
return block.requestFormat || this.constants.requestFormat;
|
||
|
|
||
|
return "query";
|
||
|
}
|
||
|
|
||
|
function getQueryAndUrl(msg, def, format, config) {
|
||
|
var url = def.url;
|
||
|
if (config.pathPrefix && url.indexOf(config.pathPrefix) !== 0) {
|
||
|
url = config.pathPrefix + def.url;
|
||
|
}
|
||
|
var ret = {
|
||
|
query: format == "json" ? {} : format == "raw" ? msg.data : []
|
||
|
};
|
||
|
if (!def || !def.params) {
|
||
|
ret.url = url;
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
Object.keys(def.params).forEach(function(paramName) {
|
||
|
paramName = paramName.replace(/^[$]+/, "");
|
||
|
if (!(paramName in msg))
|
||
|
return;
|
||
|
|
||
|
var isUrlParam = url.indexOf(":" + paramName) !== -1;
|
||
|
var valFormat = isUrlParam || format != "json" ? "query" : format;
|
||
|
var val;
|
||
|
if (valFormat != "json") {
|
||
|
if (typeof msg[paramName] == "object") {
|
||
|
try {
|
||
|
msg[paramName] = JSON.stringify(msg[paramName]);
|
||
|
val = encodeURIComponent(msg[paramName]);
|
||
|
}
|
||
|
catch (ex) {
|
||
|
return Util.log("httpSend: Error while converting object to JSON: "
|
||
|
+ (ex.message || ex), "error");
|
||
|
}
|
||
|
}
|
||
|
else if (def.params[paramName] && def.params[paramName].combined) {
|
||
|
// Check if this is a combined (search) string.
|
||
|
val = msg[paramName].split(/[\s\t\r\n]*\+[\s\t\r\n]*/)
|
||
|
.map(function(part) {
|
||
|
return encodeURIComponent(part);
|
||
|
})
|
||
|
.join("+");
|
||
|
}
|
||
|
else
|
||
|
val = encodeURIComponent(msg[paramName]);
|
||
|
}
|
||
|
else
|
||
|
val = msg[paramName];
|
||
|
|
||
|
if (isUrlParam) {
|
||
|
url = url.replace(":" + paramName, val);
|
||
|
}
|
||
|
else {
|
||
|
if (format == "json")
|
||
|
ret.query[paramName] = val;
|
||
|
else if (format != "raw")
|
||
|
ret.query.push(paramName + "=" + val);
|
||
|
}
|
||
|
});
|
||
|
ret.url = url;
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Client#httpSend(msg, block, callback) -> null
|
||
|
* - msg (Object): parameters to send as the request body
|
||
|
* - block (Object): parameter definition from the `routes.json` file that
|
||
|
* contains validation rules
|
||
|
* - callback (Function): function to be called when the request returns.
|
||
|
* If the the request returns with an error, the error is passed to
|
||
|
* the callback as its first argument (NodeJS-style).
|
||
|
*
|
||
|
* Send an HTTP request to the server and pass the result to a callback.
|
||
|
**/
|
||
|
this.httpSend = function(msg, block, callback) {
|
||
|
var self = this;
|
||
|
var method = block.method.toLowerCase();
|
||
|
var hasFileBody = block.hasFileBody;
|
||
|
var hasBody = !hasFileBody && ("head|get|delete".indexOf(method) === -1);
|
||
|
var format = getRequestFormat.call(this, hasBody, block);
|
||
|
var obj = getQueryAndUrl(msg, block, format, self.config);
|
||
|
var query = obj.query;
|
||
|
var url = this.config.url ? this.config.url + obj.url : obj.url;
|
||
|
|
||
|
var path = url;
|
||
|
var protocol = this.config.protocol || this.constants.protocol || "http";
|
||
|
var host = block.host || this.config.host || this.constants.host;
|
||
|
var port = this.config.port || this.constants.port || (protocol == "https" ? 443 : 80);
|
||
|
var proxyUrl;
|
||
|
if (this.config.proxy !== undefined) {
|
||
|
proxyUrl = this.config.proxy;
|
||
|
} else {
|
||
|
proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
|
||
|
}
|
||
|
if (proxyUrl) {
|
||
|
path = Url.format({
|
||
|
protocol: protocol,
|
||
|
hostname: host,
|
||
|
port: port,
|
||
|
pathname: path
|
||
|
});
|
||
|
|
||
|
if (!/^(http|https):\/\//.test(proxyUrl))
|
||
|
proxyUrl = "https://" + proxyUrl;
|
||
|
|
||
|
var parsedUrl = Url.parse(proxyUrl);
|
||
|
protocol = parsedUrl.protocol.replace(":", "");
|
||
|
host = parsedUrl.hostname;
|
||
|
port = parsedUrl.port || (protocol == "https" ? 443 : 80);
|
||
|
}
|
||
|
if (!hasBody && query.length)
|
||
|
path += "?" + query.join("&");
|
||
|
|
||
|
var headers = {
|
||
|
"host": host,
|
||
|
"content-length": "0"
|
||
|
};
|
||
|
if (hasBody) {
|
||
|
if (format == "json")
|
||
|
query = JSON.stringify(query);
|
||
|
else if (format != "raw")
|
||
|
query = query.join("&");
|
||
|
headers["content-length"] = Buffer.byteLength(query, "utf8");
|
||
|
headers["content-type"] = format == "json"
|
||
|
? "application/json; charset=utf-8"
|
||
|
: format == "raw"
|
||
|
? "text/plain; charset=utf-8"
|
||
|
: "application/x-www-form-urlencoded; charset=utf-8";
|
||
|
}
|
||
|
if (this.auth) {
|
||
|
var basic;
|
||
|
switch (this.auth.type) {
|
||
|
case "oauth":
|
||
|
if (this.auth.token) {
|
||
|
path += (path.indexOf("?") === -1 ? "?" : "&") +
|
||
|
"access_token=" + encodeURIComponent(this.auth.token);
|
||
|
} else {
|
||
|
path += (path.indexOf("?") === -1 ? "?" : "&") +
|
||
|
"client_id=" + encodeURIComponent(this.auth.key) +
|
||
|
"&client_secret=" + encodeURIComponent(this.auth.secret);
|
||
|
}
|
||
|
break;
|
||
|
case "token":
|
||
|
headers.authorization = "token " + this.auth.token;
|
||
|
break;
|
||
|
case "basic":
|
||
|
basic = new Buffer(this.auth.username + ":" + this.auth.password, "ascii").toString("base64");
|
||
|
headers.authorization = "Basic " + basic;
|
||
|
break;
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function callCallback(err, result) {
|
||
|
if (callback) {
|
||
|
var cb = callback;
|
||
|
callback = undefined;
|
||
|
cb(err, result);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function addCustomHeaders(customHeaders) {
|
||
|
Object.keys(customHeaders).forEach(function(header) {
|
||
|
var headerLC = header.toLowerCase();
|
||
|
if (self.requestHeaders.indexOf(headerLC) == -1)
|
||
|
return;
|
||
|
headers[headerLC] = customHeaders[header];
|
||
|
});
|
||
|
}
|
||
|
addCustomHeaders(Util.extend(msg.headers || {}, this.config.headers));
|
||
|
|
||
|
if (!headers["user-agent"])
|
||
|
headers["user-agent"] = "NodeJS HTTP Client";
|
||
|
|
||
|
if (!("accept" in headers))
|
||
|
headers.accept = this.config.requestMedia || this.constants.requestMedia;
|
||
|
|
||
|
var options = {
|
||
|
host: host,
|
||
|
port: port,
|
||
|
path: path,
|
||
|
method: method,
|
||
|
headers: headers
|
||
|
};
|
||
|
|
||
|
if (this.config.rejectUnauthorized !== undefined)
|
||
|
options.rejectUnauthorized = this.config.rejectUnauthorized;
|
||
|
|
||
|
if (this.debug)
|
||
|
console.log("REQUEST: ", options);
|
||
|
|
||
|
function httpSendRequest() {
|
||
|
var req = require(protocol).request(options, function(res) {
|
||
|
if (self.debug) {
|
||
|
console.log("STATUS: " + res.statusCode);
|
||
|
console.log("HEADERS: " + JSON.stringify(res.headers));
|
||
|
}
|
||
|
res.setEncoding("utf8");
|
||
|
var data = "";
|
||
|
res.on("data", function(chunk) {
|
||
|
data += chunk;
|
||
|
});
|
||
|
res.on("error", function(err) {
|
||
|
callCallback(err);
|
||
|
});
|
||
|
res.on("end", function() {
|
||
|
if (res.statusCode >= 400 && res.statusCode < 600 || res.statusCode < 10) {
|
||
|
callCallback(new error.HttpError(data, res.statusCode));
|
||
|
} else {
|
||
|
res.data = data;
|
||
|
callCallback(null, res);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
var timeout = (block.timeout !== undefined) ? block.timeout : self.config.timeout;
|
||
|
if (timeout) {
|
||
|
req.setTimeout(timeout);
|
||
|
}
|
||
|
|
||
|
req.on("error", function(e) {
|
||
|
if (self.debug)
|
||
|
console.log("problem with request: " + e.message);
|
||
|
callCallback(e.message);
|
||
|
});
|
||
|
|
||
|
req.on("timeout", function() {
|
||
|
if (self.debug)
|
||
|
console.log("problem with request: timed out");
|
||
|
callCallback(new error.GatewayTimeout());
|
||
|
});
|
||
|
|
||
|
// write data to request body
|
||
|
if (hasBody && query.length) {
|
||
|
if (self.debug)
|
||
|
console.log("REQUEST BODY: " + query + "\n");
|
||
|
req.write(query + "\n");
|
||
|
}
|
||
|
|
||
|
if (block.hasFileBody) {
|
||
|
var stream = fs.createReadStream(msg.filePath);
|
||
|
stream.pipe(req);
|
||
|
} else {
|
||
|
req.end();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
if (hasFileBody) {
|
||
|
fs.stat(msg.filePath, function(err, stat) {
|
||
|
if (err) {
|
||
|
callCallback(err);
|
||
|
} else {
|
||
|
headers["content-length"] = stat.size;
|
||
|
headers["content-type"] = mime.lookup(msg.name);
|
||
|
httpSendRequest();
|
||
|
}
|
||
|
});
|
||
|
} else {
|
||
|
httpSendRequest();
|
||
|
}
|
||
|
};
|
||
|
}).call(Client.prototype);
|