import http from 'http' import https from 'https' import fs from 'fs' import url from 'url' function resolveRelative(from, to) { const resolvedUrl = new URL(to, new URL(from, 'resolve://')); if (resolvedUrl.protocol === 'resolve:') { // `from` is a relative URL. const { pathname, search, hash } = resolvedUrl; return pathname + search + hash; } return resolvedUrl.toString(); } export function request(config, path, filePath = null, redirects, fastRaw = false) { if (!config || typeof(config) !== 'object' || Array.isArray(config)) { return Promise.reject(new Error('Request must be called with config in first parameter')) } let newRedirects = (redirects || 0) + 1 if (!path || typeof(path) !== 'string' || !path.startsWith('http')) { return Promise.reject(new Error('URL was empty or invalid')) } let parsed try { parsed = new url.URL(path) } catch { return Promise.reject(new Error('URL was empty or invalid')) } let h = http if (parsed.protocol === 'https:') { h = https } let req = null return new Promise(function(resolve, reject) { if (!path) { return reject(new Error('Request path was empty')) } let headers = { 'User-Agent': 'TheThing/service-core', Accept: 'application/vnd.github.v3+json' } if (config.githubAuthToken && path.indexOf('api.github.com') >= 0) { headers['Authorization'] = `token ${config.githubAuthToken}` } let timeout = fastRaw ? 5000 : config.timeout || 10000 let timedout = false let timer = setTimeout(function() { timedout = true if (req) { req.destroy() } reject(new Error(`Request ${path} timed out out after ${timeout}`)) }, timeout) req = h.request({ path: parsed.pathname + parsed.search, port: parsed.port, method: 'GET', headers: headers, timeout: timeout, hostname: parsed.hostname }, function(res) { if (timedout) { return } clearTimeout(timer) let output = '' if (filePath) { let file = fs.createWriteStream(filePath) res.pipe(file) } else { res.on('data', function(chunk) { output += chunk }) } res.on('end', function() { if (res.statusCode >= 300 && res.statusCode < 400) { if (newRedirects > 5) { return reject(new Error(`Too many redirects (last one was ${res.headers.location})`)) } if (!res.headers.location) { return reject(new Error('Redirect returned no path in location header')) } if (res.headers.location.startsWith('http')) { return resolve(request(config, res.headers.location, filePath, newRedirects, fastRaw)) } else { return resolve(request(config, resolveRelative(path, res.headers.location), filePath, newRedirects, fastRaw)) } } else if (res.statusCode >= 400) { return reject(new Error(`HTTP Error ${res.statusCode}: ${output}`)) } resolve({ statusCode: res.statusCode, status: res.statusCode, statusMessage: res.statusMessage, headers: res.headers, body: output }) }) }) req.on('error', function(err) { if (timedout) return let wrapped = new Error(`Error during request ${path}: ${err.message}`) wrapped.code = err.code reject(wrapped) }) req.on('timeout', function(err) { if (timedout) return reject(err) }) req.end() }).then(function(res) { if (req) { req.destroy() } if (!filePath && !fastRaw) { if (typeof(res.body) === 'string') { try { res.body = JSON.parse(res.body) } catch(e) { throw new Error(`Error parsing body ${res.body}: ${e.message}`) } } } return res }) }