UNPKG

request-curl

Version:

Request.js syntax written with cURL bindings

275 lines (226 loc) 8.18 kB
const { Curl } = require('node-libcurl') const Caseless = require('caseless') const url = require('url') class Request { constructor(options) { return this.init(options) } async init(options) { // Support uri/url params if (options.url) { options.uri = options.url delete options.url } if (options.qs) { const parsed = url.parse(options.uri) // Append to query param options.uri += !parsed.search ? `?` : parsed.search == '?' ? '' : '&' const values = [] for (let key in options.qs) { values.push(`${key}=${options.qs[key]}`) } options.uri += values.join('&') } // await new Promise(resolve => setTimeout(resolve, '1000')) return new Promise(async (resolve, reject) => { const curl = new Curl() curl.setOpt('URL', options.uri) // Verbose option for debugging if (options.verbose) { curl.setOpt(Curl.option.VERBOSE, true) curl.setOpt(Curl.option.DEBUGFUNCTION, (infoType, content) => { if (infoType == 0) { console.log(Buffer.from(content).toString().trim()) } }) } if (options.strictSSL) { curl.setOpt('CAINFO', './cacert-2020-01-01.pem') } else { curl.setOpt('SSL_VERIFYPEER', false) curl.setOpt('SSL_VERIFYHOST', false) curl.setOpt('SSL_VERIFYSTATUS', false) } // Request method, defaults to GET curl.setOpt('CUSTOMREQUEST', options.method || 'GET') // Disable NPN/ALPN by default curl.setOpt('SSL_ENABLE_ALPN', false) curl.setOpt('SSL_ENABLE_NPN', false) // Default SSL off for https requests w/o cert // Handle gzip compression if (options.gzip) { curl.setOpt('ACCEPT_ENCODING', '') } else { curl.setOpt('HTTP_CONTENT_DECODING', '0') } // Timeout (ms) if (options.timeout) { curl.setOpt('TIMEOUT_MS') } // Forever (Handle connection reuse/not reuse) // Can be used for fingerprinting if (options.forever) { curl.setOpt('TCP_KEEPALIVE', 2) curl.setOpt('FORBID_REUSE', 0) curl.setOpt('FRESH_CONNECT', 0) } else { curl.setOpt('TCP_KEEPALIVE', 0) curl.setOpt('FORBID_REUSE', 2) curl.setOpt('FRESH_CONNECT', 1) } // Handle rebuilding URL // If false path will be rebuild `/../` can't be used as a directory name etc curl.setOpt('PATH_AS_IS', options.rebuild) // Post request form handling if (options.form) { const data = [] const keys = Object.keys(options.form) for (let i in keys) { const key = keys[i] data.push(`${key}=${options.form[key]}`) } const fields = data.join('&') curl.setOpt('POSTFIELDS', fields) const caseless = Caseless(options.headers) // Append content-length header if (options.headers) { if (!caseless.get('content-type')) { caseless.set('content-type', 'application/x-www-form-urlencoded') } } else { options.headers = { 'content-type': 'application/x-www-form-urlencoded', } } } else if (options.body) { const caseless = Caseless(options.headers) if (options.headers) { if (!caseless.get('content-type')) { caseless.set('content-type', 'application/json') } } else { options.headers = { 'content-type': 'application/json', } } curl.setOpt('POSTFIELDS', JSON.stringify(options.body)) } if (options.qs) { // Support qs parameter the same as request-promise const parsed = url.parse(options.uri) console.log(parsed) } // HTTP2 if (options.http2) { curl.setOpt('SSL_ENABLE_ALPN', true) curl.setOpt('HTTP_VERSION', 'CURL_HTTP_VERSION_2_0') } else { curl.setOpt('HTTP_VERSION', 'CURL_HTTP_VERSION_1_1') } const headers = [] let hasCookieHeader = false if (typeof options.headers === 'object') { for (const header in options.headers) { if (header.toLowerCase() == 'cookie') { if (options.jar) { const cookiesInJar = options.jar.getCookieStringSync(options.uri) if (!cookiesInJar) { // Cookie jar is empty, just use cookies inside header headers.push(`${header}: ${options.headers[header]}`) } else { // TODO: maybe make cookies set in the header overwrite that of the jar headers.push(`${header}: ${options.headers[header]}; ${cookiesInJar}`) } } else { // Has cookie header but no jar, just append headers.push(`${header}: ${options.headers[header]}`) } hasCookieHeader = true } else { headers.push(`${header}: ${options.headers[header]}`) } } } if (!hasCookieHeader && options.jar) { // If there is no cookie header and has a jar, only append jar cookies const cookiesInJar = options.jar.getCookieStringSync(options.uri) headers.push(`cookie: ${cookiesInJar}`) } // Set the headers curl.setOpt('HTTPHEADER', headers) // Proxy support if (options.proxy) { curl.setOpt('PROXY', options.proxy) } // Disable cURL redirects because we handle them manually for cookiejars curl.setOpt('FOLLOWLOCATION', false) // Cipher suites if (options.ciphers) { curl.setOpt('SSL_CIPHER_LIST', options.ciphers) } curl.on('end', async function (statusCode, data, headers) { // Remove results header and compress into single object // IDK why Curl returns headers as an array, that's a problem for future me! const respHeaders = headers[headers.length - 1] delete respHeaders.result if (options.jar) { // Get host to set cookies to const parsedUrl = url.parse(options.uri) const host = `${parsedUrl.protocol}//${parsedUrl.host}/` // Append set cookie headers to the cookie jar (if using) const caseless = Caseless(respHeaders) if (caseless.get('set-cookie')) { caseless.get('set-cookie').forEach(function (c) { const cookie = c.split(';')[0] options.jar.setCookieSync(cookie, host) }) } } if (options.followRedirects && curl.getInfo('REDIRECT_URL')) { options.redirectCount = options.redirectCount + 1 || 1 if (options.redirectCount != options.maxRedirects) { // Use the same socket when following the redirect. options.forever = true options.uri = curl.getInfo('REDIRECT_URL') options.method = options.followMethod || options.method || 'GET' if (options.method.toLowerCase() == 'get') { // Delete form if following request is GET delete options.form delete options.body } this.close() return resolve(new Request(options)) } } // Parse json if required if (options.json) { try { data = JSON.parse(data) } catch (err) {} } // Convert response into same format as request-promise let response = { body: data, headers: respHeaders, statusCode: statusCode, } // Close then resolve to prevent memory leaks this.close() if (options.runner) { // Runner, runs a function passed to it before resolving closing // Allows you to do logging etc options.runner(response) } resolve(response) }) curl.on('error', function (err) { curl.close.bind(curl) this.close() reject(err) }) // Execute curl.perform() }) } } module.exports = Request