diff --git a/lib/bcoin/http.js b/lib/bcoin/http/http.js similarity index 100% rename from lib/bcoin/http.js rename to lib/bcoin/http/http.js diff --git a/lib/bcoin/http/request.js b/lib/bcoin/http/request.js new file mode 100644 index 00000000..cb464e54 --- /dev/null +++ b/lib/bcoin/http/request.js @@ -0,0 +1,279 @@ +var StringDecoder = require('string_decoder').StringDecoder; +var Stream = require('stream').Stream; +var qs = require('querystring'); +var url = require('url'); + +// Spoof by default +var USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)' + + ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36'; + +function request(options, callback, stream) { + var uri = options.uri; + var query = options.query; + var body = options.body; + var json = options.json; + var form = options.form; + var type = options.type; + var http, req, stream, opt; + + if (callback) + return request._buffer(options, callback); + + if (json && typeof json === 'object') { + body = json; + json = true; + } + + if (form && typeof form === 'object') { + body = qs.stringify(form); + form = true; + } + + if (typeof uri !== 'object') { + if (!/:\/\//.test(uri)) + uri = 'http://' + uri; + uri = url.parse(uri); + } + + if (uri.protocol === 'https:') + http = require('https'); + else + http = require('http'); + + if (uri.search) + query = qs.parse(uri.search); + + if (query && typeof query !== 'string') + query = qs.stringify(query); + + if (query) + query = '?' + query; + else + query = ''; + + opt = { + host: uri.hostname, + port: uri.port || (uri.protocol === 'https:' ? 443 : 80), + path: uri.pathname + query, + headers: { + 'User-Agent': options.agent || USER_AGENT + } + }; + + if (body) { + if (!type) { + if (form) + type = 'form'; + else if (json || (typeof body === 'object' && !Buffer.isBuffer(body))) + type = 'json'; + else if (typeof body === 'string') + type = 'text'; + else + type = 'binary'; + } + + if (type === 'form') + type = 'application/x-www-form-urlencoded; charset=utf-8'; + else if (type === 'json') + type = 'application/json; charset=utf-8'; + else if (type === 'text') + type = 'text/plain; charset=utf-8'; + else if (type === 'binary') + type = 'application/octet-stream'; + + if (type === 'json' && typeof body === 'object') + body = JSON.stringify(body); + else if (type === 'form' && typeof body === 'object') + body = qs.stringify(body); + + if (typeof body === 'string') + body = new Buffer(body, 'utf8'); + + assert(Buffer.isBuffer(body)); + + opt.headers['Content-Type'] = type; + opt.headers['Content-Length'] = body.length + ''; + + opt.method = options.method || 'POST'; + } else { + opt.method = options.method || 'GET'; + } + + opt.method = opt.method.toUpperCase(); + + req = http.request(opt); + + if (!stream) + stream = new ReqStream(options); + + stream.req = req; + + req.on('response', function(res) { + var called = false; + var type = res.headers['content-type']; + + if (res.headers['location']) { + if (++stream._redirects > stream.maxRedirects) + return done(new Error('Too many redirects.')); + options.uri = res.headers['location']; + return request(options, null, stream); + } + + if (/\/json/i.test(type)) + type = 'json'; + else if (/form-urlencoded/i.test(type)) + type = 'form'; + else if (/text\/plain/i.test(type)) + type = 'text'; + else if (/\/x?html/i.test(type)) + type = 'html'; + else + type = 'binary'; + + stream.res = res; + stream.headers = res.headers; + stream.type = type; + + if (options.expect && type !== options.expect) + return done(new Error('Wrong content-type for response.')); + + stream.emit('headers', res.headers); + stream.emit('type', type); + stream.emit('response', res); + + function done(err) { + if (called) + return; + + called = true; + + if (res.socket) + res.socket.removeListener('end', done); + + if (err) { + stream.destroy(); + stream.emit('error', err); + return; + } + + stream.emit('end'); + stream.emit('close'); + } + + res.on('data', function(data) { + stream.emit('data', data); + }); + + res.on('error', done); + + res.on('end', done); + + // An agent socket's `end` sometimes + // won't be emitted on the response. + if (res.socket) + res.socket.on('end', done); + }); + + if (body) + req.write(body); + + req.end(); + + return stream; +} + +request._buffer = function(options, callback) { + var stream = request(options); + var total = 0; + var called = false; + var decoder, body; + + function done(err) { + if (called) + return; + + called = true; + + if (err) + return callback(err); + + if (stream.type === 'binary') { + body = Buffer.concat(body); + } else if (stream.type === 'json') { + try { + body = JSON.parse(body); + } catch (e) { + return callback(e); + } + } else if (stream.type === 'form') { + try { + body = qs.parse(body); + } catch (e) { + return callback(e); + } + } + + callback(null, stream.res, body, stream.type); + } + + stream.on('type', function(type) { + if (type !== 'binary') { + decoder = new StringDecoder('utf8'); + body = ''; + } else { + body = []; + } + }); + + stream.on('data', function(data) { + total += data.length; + + if (options.limit && total > options.limit) { + stream.destroy(); + return done(); + } + + if (decoder) + body += decoder.write(data); + else + body.push(data); + }); + + stream.on('error', done); + + stream.on('end', done); + + return stream; +}; + +function ReqStream(options) { + Stream.call(this); + this.req = null; + this.res = null; + this.headers = null; + this.type = null; + this._redirects = 0; + this.maxRedirects = options.maxRedirects || 5; +} + +ReqStream.prototype.__proto__ = Stream.prototype; + +ReqStream.prototype.destroy = function destroy() { + try { + this.req.abort(); + } catch (e) { + ; + } + + try { + this.res.destroy(); + } catch (e) { + ; + } + + try { + this.res.socket.destroy(); + } catch (e) { + ; + } +};