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 | var err = Request.handleFailure(error.response && error.response.body, error.status); |
||
170 | |||
171 | self.deferred.reject(err); |
||
172 | return self.callback(err, error.response && error.response.body); |
||
173 | } |
||
174 | |||
175 | debug('response status code: %s content type: %s', res.status, res.headers['content-type']); |
||
176 | if (!error && (res.headers['content-type'].indexOf('application/json') >= 0)) { |
||
177 | try { |
||
178 | body = JSON.parse(res.text); |
||
179 | } catch (e) { |
||
180 | error = e; |
||
181 | } |
||
182 | } |
||
183 | |||
184 | if (!body) { |
||
185 | body = res.text; |
||
186 | } |
||
187 | |||
188 | if (!error && res.status !== 200) { |
||
189 | error = Request.handleFailure(res.text, res.statusCode); |
||
190 | } |
||
191 | |||
192 | if (error) { |
||
193 | self.deferred.reject(error); |
||
194 | } else { |
||
195 | self.deferred.resolve(body); |
||
196 | } |
||
197 | |||
198 | return self.callback(error, body); |
||
199 | }); |
||
200 | |||
201 | return self.deferred; |
||
202 | }; |
||
203 | |||
204 | Request.handleFailure = function(body, statusCode) { |
||
205 | var data, error; |
||
206 | if (typeof body === "object") { |
||
207 | data = body; |
||
208 | } else { |
||
209 | try { |
||
210 | data = JSON.parse(body); |
||
211 | } catch (e) {} |
||
0 ignored issues
–
show
Coding Style
Comprehensibility
Best Practice
introduced
by
![]() |
|||
212 | } |
||
213 | |||
214 | if (data) { |
||
215 | var msg = data.msg || ""; |
||
216 | if (!msg) { |
||
217 | if (statusCode === 429) { |
||
218 | msg = "Too Many Request"; |
||
219 | } |
||
220 | } |
||
221 | |||
222 | error = new Error(msg); |
||
223 | |||
224 | Object.keys(data).forEach(function(k) { |
||
225 | if (k !== "msg") { |
||
226 | error[k] = data[k]; |
||
227 | } |
||
228 | }); |
||
229 | } else if (body) { |
||
230 | error = new Error(body); |
||
231 | } else { |
||
232 | error = new Error('Unknown Server Error'); |
||
233 | } |
||
234 | |||
235 | if (statusCode) { |
||
236 | error.statusCode = statusCode; |
||
237 | } |
||
238 | |||
239 | return Request.convertError(error); |
||
240 | }; |
||
241 | |||
242 | Request.convertError = function(error) { |
||
243 | if (error.requires_2fa) { |
||
244 | return new blocktrail.WalletMissing2FAError(); |
||
245 | } else if (error.message.match(/Invalid two_factor_token/)) { |
||
246 | return new blocktrail.WalletInvalid2FAError(); |
||
247 | } |
||
248 | |||
249 | return error; |
||
250 | }; |
||
251 | |||
252 | |||
253 | |||
254 | module.exports = Request; |
||
255 |