Jonatan Nilsson
47344c5e7a
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
Fixed some minor bugs. Will now no longer travel through history but instead stop at last stable version.
166 lines
No EOL
4.9 KiB
JavaScript
166 lines
No EOL
4.9 KiB
JavaScript
import http from 'http'
|
|
import https from 'https'
|
|
import stream from 'stream/promises'
|
|
import fs from 'fs'
|
|
|
|
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' + (typeof(path) === 'string' ? ': ' + path : '')))
|
|
}
|
|
let parsed
|
|
try {
|
|
parsed = new URL(path)
|
|
} catch {
|
|
return Promise.reject(new Error('URL was empty or invalid: ' + path))
|
|
}
|
|
|
|
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.token) {
|
|
headers['Authorization'] = `token ${config.token}`
|
|
}
|
|
let timeout = config.timeout || 10000
|
|
|
|
let timedout = false
|
|
let timer = setTimeout(function() {
|
|
timedout = true
|
|
if (req) { req.destroy() }
|
|
reject(new Error(`Request ${path} timed out after ${timeout}ms`))
|
|
}, 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)
|
|
|
|
const ac = new AbortController()
|
|
|
|
let output = ''
|
|
if (filePath) {
|
|
stream.pipeline(res, fs.createWriteStream(filePath), { signal: ac.signal })
|
|
.then(function() {
|
|
resolve({
|
|
statusCode: res.statusCode,
|
|
status: res.statusCode,
|
|
statusMessage: res.statusMessage,
|
|
headers: res.headers,
|
|
body: null
|
|
})
|
|
}, function(err) {
|
|
if (err.code === 'ABORT_ERR') return
|
|
reject(err)
|
|
})
|
|
// let file = fs.createWriteStream(filePath)
|
|
// res.pipe(file)
|
|
} else {
|
|
res.on('data', function(chunk) {
|
|
output += chunk
|
|
})
|
|
}
|
|
res.on('end', function() {
|
|
let err = null
|
|
|
|
if (res.statusCode >= 300 && res.statusCode < 400) {
|
|
if (newRedirects > 5) {
|
|
err = new Error(`Too many redirects (last one was ${res.headers.location})`)
|
|
}
|
|
else if (!res.headers.location) {
|
|
err = new Error('Redirect returned no path in location header')
|
|
}
|
|
else if (res.headers.location.startsWith('http')) {
|
|
ac.abort()
|
|
return resolve(request(config, res.headers.location, filePath, newRedirects, fastRaw))
|
|
} else {
|
|
ac.abort()
|
|
return resolve(request(config, resolveRelative(path, res.headers.location), filePath, newRedirects, fastRaw))
|
|
}
|
|
} else if (res.statusCode >= 400) {
|
|
err = new Error(`HTTP Error ${res.statusCode}: ${output}`)
|
|
}
|
|
|
|
if (err) {
|
|
ac.abort()
|
|
if (!filePath) return reject(err)
|
|
|
|
// Do some cleanup in case we were in the middle of downloading file
|
|
return fs.rm(filePath, function() {
|
|
reject(err)
|
|
})
|
|
}
|
|
// Let the pipeline do the resolving so it can finish flusing before calling resolve
|
|
if (!filePath) {
|
|
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) {
|
|
if (res.body.indexOf('<!DOCTYPE') < 100 || res.body.indexOf('<html') < 100) {
|
|
return Promise.reject(new Error('Error parsing body, expected JSON but got HTML instead: ' + res.body))
|
|
}
|
|
return Promise.reject(new Error(`Error parsing body ${res.body}: ${e.message}`))
|
|
}
|
|
}
|
|
}
|
|
return res
|
|
})
|
|
} |