import { HTTP } from 'meteor/http'; import { URL, URLSearchParams } from 'meteor/url'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { truncate } from '../../../lib/utils/stringUtils'; // Code extracted from https://github.com/meteor/meteor/blob/master/packages/deprecated/http // Modified to: // - Respect proxy envvars such as HTTP_PROXY and NO_PROXY // - Respect HTTP_DEFAULT_TIMEOUT envvar or use 20s when it is not set const envTimeout = parseInt(process.env.HTTP_DEFAULT_TIMEOUT || '', 10); const defaultTimeout = !isNaN(envTimeout) ? envTimeout : 20000; type HttpCallOptions = { content?: string | URLSearchParams; data?: Record; query?: string; params?: Record; auth?: string; headers?: Record; timeout?: number; followRedirects?: boolean; referrer?: string; integrity?: string; }; // eslint-disable-next-line @typescript-eslint/naming-convention interface HTTPResponse { statusCode?: number; headers?: { [id: string]: string }; content?: string; data?: any; ok?: boolean; redirected?: boolean; } type callbackFn = (error: Error | undefined, result?: HTTPResponse) => void; // Fill in `response.data` if the content-type is JSON. function populateData(response: Record): void { // Read Content-Type header, up to a ';' if there is one. // A typical header might be "application/json; charset=utf-8" // or just "application/json". const contentType = (response.headers['content-type'] || ';').split(';')[0]; // Only try to parse data as JSON if server sets correct content type. if (['application/json', 'text/javascript', 'application/javascript', 'application/x-javascript'].indexOf(contentType) >= 0) { try { response.data = JSON.parse(response.content); } catch (err) { response.data = null; } } else { response.data = null; } } function makeErrorByStatus(statusCode: number, content: string): Error { let message = `failed [${statusCode}]`; if (content) { message += `${truncate(content.replace(/\n/g, ' '), 500)}`; } return new Error(message); } function _call(httpMethod: string, url: string, options: HttpCallOptions, callback: callbackFn): void { const method = (httpMethod || '').toUpperCase(); if (!/^https?:\/\//.test(url)) { throw new Error('url must be absolute and start with http:// or https://'); } const headers: Record = {}; let { content } = options; if (!('timeout' in options)) { options.timeout = defaultTimeout; } if (options.data) { content = JSON.stringify(options.data); headers['Content-Type'] = 'application/json'; } let paramsForUrl; let paramsForBody; if (content || method === 'GET' || method === 'HEAD') { paramsForUrl = options.params; } else { paramsForBody = options.params; } const newUrl = URL._constructUrl(url, options.query, paramsForUrl); if (options.auth) { if (options.auth.indexOf(':') < 0) { throw new Error('auth option should be of the form "username:password"'); } const base64 = Buffer.from(options.auth, 'ascii').toString('base64'); headers.Authorization = `Basic ${base64}`; } if (paramsForBody) { const data = new URLSearchParams(); Object.entries(paramsForBody).forEach(([key, value]) => { data.append(key, value); }); content = data; headers['Content-Type'] = 'application/x-www-form-urlencoded'; } const { headers: receivedHeaders } = options; if (receivedHeaders) { Object.keys(receivedHeaders).forEach(function (key) { headers[key] = receivedHeaders[key]; }); } // wrap callback to add a 'response' property on an error, in case // we have both (http 4xx/5xx error, which has a response payload) const wrappedCallback = ((cb: callbackFn): { (error: Error | undefined, response?: HTTPResponse): void } => { let called = false; return (error: Error | undefined, response?: HTTPResponse): void => { if (!called) { called = true; if (error && response) { (error as any).response = response; } cb(error, response); } }; })(callback); // is false if false, otherwise always true const followRedirects = options.followRedirects === false ? 'manual' : 'follow'; const requestOptions = { method, jar: false, timeout: options.timeout, body: content, redirect: followRedirects, referrer: options.referrer, integrity: options.integrity, headers, } as const; fetch(newUrl, requestOptions) .then(async (res) => { const content = await res.text(); const response: HTTPResponse = {}; response.statusCode = res.status; response.content = `${content}`; // fetch headers don't allow simple read using bracket notation // so we iterate their entries and assign them to a new Object response.headers = {}; for (const entry of (res.headers as any).entries()) { const [key, val] = entry; response.headers[key] = val; } response.ok = res.ok; response.redirected = res.redirected; populateData(response); if (response.statusCode >= 400) { const error = makeErrorByStatus(response.statusCode, response.content); wrappedCallback(error, response); } else { wrappedCallback(undefined, response); } }) .catch((err) => wrappedCallback(err)); } function httpCallAsync(httpMethod: string, url: string, options: HttpCallOptions, callback: callbackFn): void; function httpCallAsync(httpMethod: string, url: string, callback: callbackFn): void; function httpCallAsync(httpMethod: string, url: string, optionsOrCallback: HttpCallOptions | callbackFn = {}, callback?: callbackFn): void { // If the options argument was omitted, adjust the arguments: if (!callback && typeof optionsOrCallback === 'function') { return _call(httpMethod, url, {}, optionsOrCallback as callbackFn); } return _call(httpMethod, url, optionsOrCallback as HttpCallOptions, callback as callbackFn); } export const httpCall = async (httpMethod: string, url: string, options: HttpCallOptions) => { return new Promise((resolve, reject) => { httpCallAsync.bind(HTTP)(httpMethod, url, options, (error, result) => { if (error) { return reject(error); } resolve(result); }); }); };