Completed
Pull Request — master (#99)
by
unknown
03:07
created

Request.performRequest   D

Complexity

Conditions 9
Paths 25

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
c 0
b 0
f 0
nc 25
dl 0
loc 33
rs 4.909
nop 2
1
/* jshint -W100, -W071 */
2
var blocktrail = require('./blocktrail'),
3
    _ = require("lodash"),
4
    url = require('url'),
5
    qs = require('querystring'),
6
    q = require('q'),
7
    createHash = require('create-hash'),
8
    superagent = require('superagent'),
9
    superagentHttpSignature = require('superagent-http-signature/index-hmac-only');
10
11
var debug = require('debug')('blocktrail-sdk:request');
12
13
var isNodeJS = !process.browser;
14
15
var noop = function() {};
16
17
/**
18
 * Helper for doing HTTP requests
19
 *
20
 * @param options       object{
21
 *                          host: '',
22
 *                          endpoint: '', // base url for .request
23
 *                          auth: null || 'http-signature',
24
 *                          apiKey: 'API_KEY',
25
 *                          apiSecret: 'API_SECRET',
26
 *                          params: {}, // defaults
27
 *                          headers: {} // defaults
28
 *                      }
29
 * @constructor
30
 */
31
function Request(options) {
32
    var self = this;
33
34
    self.https = options.https;
35
    self.host = options.host;
36
    self.endpoint = options.endpoint;
37
    self.auth = options.auth;
38
    self.port = options.port;
39
    self.apiKey = options.apiKey;
40
    self.apiSecret = options.apiSecret;
41
    self.contentMd5 = typeof options.contentMd5 !== "undefined" ? options.contentMd5 : true;
42
43
    self.params = _.defaults({}, options.params);
44
    self.headers = _.defaults({}, options.headers);
45
}
46
47
/**
48
 * helper to make sure the query string is sorted in lexical order
49
 *
50
 * @param params
51
 * @returns {string}
52
 */
53
Request.qs = function(params) {
54
    var query = [];
55
    var qsKeys = Object.keys(params);
56
57
    qsKeys.sort();
58
    qsKeys.forEach(function(qsKey) {
59
        var qsChunk = {};
60
        qsChunk[qsKey] = params[qsKey];
61
        query.push(qs.stringify(qsChunk));
62
    });
63
64
    return query.join("&");
65
};
66
67
/**
68
 * execute request
69
 *
70
 * @param method        string      GET|POST|DELETE
71
 * @param resource      string      URL
72
 * @param params        object      are added to the querystring
73
 * @param data          object      is POSTed
74
 * @param fn
75
 * @returns q.Promise
76
 */
77
Request.prototype.request = function(method, resource, params, data, fn) {
78
    var self = this;
79
    self.deferred = q.defer();
80
81
    self.callback = fn || noop;
82
83
    var endpoint = url.parse(resource, true);
84
    var query = Request.qs(_.defaults({}, (params || {}), (endpoint.query || {}), (self.params || {})));
85
86
    self.path = ''.concat(self.endpoint, endpoint.pathname);
87
    if (query) {
88
        self.path = self.path.concat('?', query);
89
    }
90
91
    if (data) {
92
        self.payload = JSON.stringify(data);
93
        self.headers['Content-Type'] = 'application/json';
94
    } else {
95
        self.payload = "";
96
    }
97
98
    if (isNodeJS) {
99
        self.headers['Content-Length'] = self.payload ? self.payload.length : 0;
100
    }
101
102
    if (self.contentMd5 === true) {
103
        if (method === 'GET' || method === 'DELETE') {
104
            self.headers['Content-MD5'] = createHash('md5').update(self.path).digest().toString('hex');
105
        } else {
106
            self.headers['Content-MD5'] = createHash('md5').update(self.payload).digest().toString('hex');
107
        }
108
    }
109
110
    debug('%s %s %s', method, self.host, self.path);
111
112
    var opts = {
113
        hostname: self.host,
114
        path: self.path,
115
        port: self.port,
116
        method: method,
117
        headers: self.headers,
118
        auth: self.auth,
119
        agent: false,
120
        withCredentials: false
121
    };
122
123
    self.performRequest(opts);
124
125
    return self.deferred.promise;
126
};
127
128
Request.prototype.performRequest = function(options) {
129
    var self = this;
130
    var method = options.method;
131
    var signHMAC = false;
132
133
    if (options.auth === 'http-signature') {
134
        signHMAC = true;
135
        delete options.auth;
136
    }
137
138
    var uri = (self.https ? 'https://' : 'http://') + options.hostname + options.path;
139
140
    var request = superagent(method, uri);
141
142
    if (self.payload && (method === 'DELETE' || method === 'POST' || method === 'PUT' || method === 'PATCH')) {
143
        request.send(self.payload);
144
    }
145
146
    _.forEach(options.headers, function(value, header) {
147
        request.set(header, value);
148
    });
149
150
    if (signHMAC) {
151
        if (!self.apiSecret) {
152
            var error = new Error("Missing apiSecret! required to sign POST requests!");
153
            self.deferred.reject(error);
154
            return self.callback(error);
155
        }
156
157
        request.use(superagentHttpSignature({
158
            headers: ['(request-target)', 'content-md5'],
159
            algorithm: 'hmac-sha256',
160
            key: self.apiSecret,
161
            keyId: self.apiKey
162
        }));
163
    }
164
165
    request.end(function(error, res) {
166
        var body;
167
168
        if (error) {
169
            self.deferred.reject(error);
170
            return self.callback(error);
171
        }
172
173
        debug('response status code: %s content type: %s', res.status, res.headers['content-type']);
174
        if (!error && (res.headers['content-type'].indexOf('application/json') >= 0)) {
175
            try {
176
                body = JSON.parse(res.text);
177
            } catch (e) {
178
                error = e;
179
            }
180
        }
181
182
        if (!body) {
183
            body = res.text
184
        }
185
186
        if (!error && res.status !== 200) {
187
            error = Request.handleFailure(res.text, res.statusCode);
188
        }
189
190
        if (error) {
191
            self.deferred.reject(error);
192
        } else {
193
            self.deferred.resolve(body);
194
        }
195
196
        return self.callback(error, body);
197
    });
198
199
    return self.deferred;
200
};
201
202
Request.handleFailure = function(body, statusCode) {
203
    var data, error;
204
    if (typeof body === "object") {
205
        data = body;
206
    } else {
207
        try {
208
            data = JSON.parse(body);
209
        } catch (e) {}
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
210
    }
211
212
    if (data) {
213
        error = new Error(data.msg ? data.msg : null);
214
215
        Object.keys(data).forEach(function(k) {
216
            if (k !== "msg") {
217
                error[k] = data[k];
218
            }
219
        });
220
    } else if (body) {
221
        error = new Error(body);
222
    } else {
223
        error = new Error('Unknown Server Error');
224
    }
225
226
    if (statusCode) {
227
        error.statusCode = statusCode;
228
    }
229
230
    return Request.convertError(error);
231
};
232
233
Request.convertError = function(error) {
234
    if (error.requires_2fa) {
235
        return new blocktrail.WalletMissing2FAError();
236
    } else if (error.message.match(/Invalid two_factor_token/)) {
237
        return new blocktrail.WalletInvalid2FAError();
238
    }
239
240
    return error;
241
};
242
243
244
245
module.exports = Request;
246