|  |  |  | const fetch = require('node-fetch'); | 
					
						
							|  |  |  | const { Agent } = require('https'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* global Buffer, setTimeout, log, _ */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* eslint-disable more/no-then, no-bitwise, no-nested-ternary */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function _btoa(str) { | 
					
						
							|  |  |  |   let buffer; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (str instanceof Buffer) { | 
					
						
							|  |  |  |     buffer = str; | 
					
						
							|  |  |  |   } else { | 
					
						
							|  |  |  |     buffer = Buffer.from(str.toString(), 'binary'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return buffer.toString('base64'); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const _call = object => Object.prototype.toString.call(object); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const ArrayBufferToString = _call(new ArrayBuffer()); | 
					
						
							|  |  |  | const Uint8ArrayToString = _call(new Uint8Array()); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function _getString(thing) { | 
					
						
							|  |  |  |   if (typeof thing !== 'string') { | 
					
						
							|  |  |  |     if (_call(thing) === Uint8ArrayToString) { | 
					
						
							|  |  |  |       return String.fromCharCode.apply(null, thing); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (_call(thing) === ArrayBufferToString) { | 
					
						
							|  |  |  |       return _getString(new Uint8Array(thing)); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return thing; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function _validateResponse(response, schema) { | 
					
						
							|  |  |  |   try { | 
					
						
							|  |  |  |     // eslint-disable-next-line guard-for-in, no-restricted-syntax
 | 
					
						
							|  |  |  |     for (const i in schema) { | 
					
						
							|  |  |  |       switch (schema[i]) { | 
					
						
							|  |  |  |         case 'object': | 
					
						
							|  |  |  |         case 'string': | 
					
						
							|  |  |  |         case 'number': | 
					
						
							|  |  |  |           // eslint-disable-next-line valid-typeof
 | 
					
						
							|  |  |  |           if (typeof response[i] !== schema[i]) { | 
					
						
							|  |  |  |             return false; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           break; | 
					
						
							|  |  |  |         default: | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } catch (ex) { | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return true; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const FIVE_MINUTES = 1000 * 60 * 5; | 
					
						
							|  |  |  | const agents = { | 
					
						
							|  |  |  |   unauth: null, | 
					
						
							|  |  |  |   auth: null, | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function getContentType(response) { | 
					
						
							|  |  |  |   if (response.headers && response.headers.get) { | 
					
						
							|  |  |  |     return response.headers.get('content-type'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return null; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function _promiseAjax(providedUrl, options) { | 
					
						
							|  |  |  |   return new Promise((resolve, reject) => { | 
					
						
							|  |  |  |     const url = providedUrl || `${options.host}/${options.path}`; | 
					
						
							|  |  |  |     if (options.disableLogs) { | 
					
						
							|  |  |  |       log.info( | 
					
						
							|  |  |  |         `${options.type} [REDACTED_URL]${ | 
					
						
							|  |  |  |           options.unauthenticated ? ' (unauth)' : '' | 
					
						
							|  |  |  |         }`
 | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       log.info( | 
					
						
							|  |  |  |         `${options.type} ${url}${options.unauthenticated ? ' (unauth)' : ''}` | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const timeout = | 
					
						
							|  |  |  |       typeof options.timeout !== 'undefined' ? options.timeout : 10000; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const { proxyUrl } = options; | 
					
						
							|  |  |  |     const agentType = options.unauthenticated ? 'unauth' : 'auth'; | 
					
						
							|  |  |  |     const cacheKey = `${proxyUrl}-${agentType}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const { timestamp } = agents[cacheKey] || {}; | 
					
						
							|  |  |  |     if (!timestamp || timestamp + FIVE_MINUTES < Date.now()) { | 
					
						
							|  |  |  |       if (timestamp) { | 
					
						
							|  |  |  |         log.info(`Cycling agent for type ${cacheKey}`); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       agents[cacheKey] = { | 
					
						
							|  |  |  |         agent: new Agent({ keepAlive: true }), | 
					
						
							|  |  |  |         timestamp: Date.now(), | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const { agent } = agents[cacheKey]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const fetchOptions = { | 
					
						
							|  |  |  |       method: options.type, | 
					
						
							|  |  |  |       body: options.data || null, | 
					
						
							|  |  |  |       headers: { | 
					
						
							|  |  |  |         'User-Agent': 'Session', | 
					
						
							|  |  |  |         'X-Loki-Messenger-Agent': 'OWD', | 
					
						
							|  |  |  |         ...options.headers, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       redirect: options.redirect, | 
					
						
							|  |  |  |       agent, | 
					
						
							|  |  |  |       ca: options.certificateAuthority, | 
					
						
							|  |  |  |       timeout, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (fetchOptions.body instanceof ArrayBuffer) { | 
					
						
							|  |  |  |       // node-fetch doesn't support ArrayBuffer, only node Buffer
 | 
					
						
							|  |  |  |       const contentLength = fetchOptions.body.byteLength; | 
					
						
							|  |  |  |       fetchOptions.body = Buffer.from(fetchOptions.body); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // node-fetch doesn't set content-length like S3 requires
 | 
					
						
							|  |  |  |       fetchOptions.headers['Content-Length'] = contentLength; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const { accessKey, unauthenticated } = options; | 
					
						
							|  |  |  |     if (unauthenticated) { | 
					
						
							|  |  |  |       if (!accessKey) { | 
					
						
							|  |  |  |         throw new Error( | 
					
						
							|  |  |  |           '_promiseAjax: mode is aunathenticated, but accessKey was not provided' | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       // Access key is already a Base64 string
 | 
					
						
							|  |  |  |       fetchOptions.headers['Unidentified-Access-Key'] = accessKey; | 
					
						
							|  |  |  |     } else if (options.user && options.password) { | 
					
						
							|  |  |  |       const user = _getString(options.user); | 
					
						
							|  |  |  |       const password = _getString(options.password); | 
					
						
							|  |  |  |       const auth = _btoa(`${user}:${password}`); | 
					
						
							|  |  |  |       fetchOptions.headers.Authorization = `Basic ${auth}`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (options.contentType) { | 
					
						
							|  |  |  |       fetchOptions.headers['Content-Type'] = options.contentType; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     fetch(url, fetchOptions) | 
					
						
							|  |  |  |       .then(response => { | 
					
						
							|  |  |  |         let resultPromise; | 
					
						
							|  |  |  |         if ( | 
					
						
							|  |  |  |           options.responseType === 'json' && | 
					
						
							|  |  |  |           response.headers.get('Content-Type') === 'application/json' | 
					
						
							|  |  |  |         ) { | 
					
						
							|  |  |  |           resultPromise = response.json(); | 
					
						
							|  |  |  |         } else if ( | 
					
						
							|  |  |  |           options.responseType === 'arraybuffer' || | 
					
						
							|  |  |  |           options.responseType === 'arraybufferwithdetails' | 
					
						
							|  |  |  |         ) { | 
					
						
							|  |  |  |           resultPromise = response.buffer(); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           resultPromise = response.text(); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return resultPromise.then(result => { | 
					
						
							|  |  |  |           if ( | 
					
						
							|  |  |  |             options.responseType === 'arraybuffer' || | 
					
						
							|  |  |  |             options.responseType === 'arraybufferwithdetails' | 
					
						
							|  |  |  |           ) { | 
					
						
							|  |  |  |             // eslint-disable-next-line no-param-reassign
 | 
					
						
							|  |  |  |             result = result.buffer.slice( | 
					
						
							|  |  |  |               result.byteOffset, | 
					
						
							|  |  |  |               result.byteOffset + result.byteLength | 
					
						
							|  |  |  |             ); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           if (options.responseType === 'json') { | 
					
						
							|  |  |  |             if (options.validateResponse) { | 
					
						
							|  |  |  |               if (!_validateResponse(result, options.validateResponse)) { | 
					
						
							|  |  |  |                 if (options.disableLogs) { | 
					
						
							|  |  |  |                   log.info( | 
					
						
							|  |  |  |                     options.type, | 
					
						
							|  |  |  |                     '[REDACTED_URL]', | 
					
						
							|  |  |  |                     response.status, | 
					
						
							|  |  |  |                     'Error' | 
					
						
							|  |  |  |                   ); | 
					
						
							|  |  |  |                 } else { | 
					
						
							|  |  |  |                   log.error(options.type, url, response.status, 'Error'); | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 return reject( | 
					
						
							|  |  |  |                   HTTPError( | 
					
						
							|  |  |  |                     'promiseAjax: invalid response', | 
					
						
							|  |  |  |                     response.status, | 
					
						
							|  |  |  |                     result, | 
					
						
							|  |  |  |                     options.stack | 
					
						
							|  |  |  |                   ) | 
					
						
							|  |  |  |                 ); | 
					
						
							|  |  |  |               } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           if (response.status >= 0 && response.status < 400) { | 
					
						
							|  |  |  |             if (options.disableLogs) { | 
					
						
							|  |  |  |               log.info( | 
					
						
							|  |  |  |                 options.type, | 
					
						
							|  |  |  |                 '[REDACTED_URL]', | 
					
						
							|  |  |  |                 response.status, | 
					
						
							|  |  |  |                 'Success' | 
					
						
							|  |  |  |               ); | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |               log.info(options.type, url, response.status, 'Success'); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             if (options.responseType === 'arraybufferwithdetails') { | 
					
						
							|  |  |  |               return resolve({ | 
					
						
							|  |  |  |                 data: result, | 
					
						
							|  |  |  |                 contentType: getContentType(response), | 
					
						
							|  |  |  |                 response, | 
					
						
							|  |  |  |               }); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             return resolve(result, response.status); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           if (options.disableLogs) { | 
					
						
							|  |  |  |             log.info(options.type, '[REDACTED_URL]', response.status, 'Error'); | 
					
						
							|  |  |  |           } else { | 
					
						
							|  |  |  |             log.error(options.type, url, response.status, 'Error'); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           return reject( | 
					
						
							|  |  |  |             HTTPError( | 
					
						
							|  |  |  |               'promiseAjax: error response', | 
					
						
							|  |  |  |               response.status, | 
					
						
							|  |  |  |               result, | 
					
						
							|  |  |  |               options.stack | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       }) | 
					
						
							|  |  |  |       .catch(e => { | 
					
						
							|  |  |  |         if (options.disableLogs) { | 
					
						
							|  |  |  |           log.error(options.type, '[REDACTED_URL]', 0, 'Error'); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           log.error(options.type, url, 0, 'Error'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         const stack = `${e.stack}\nInitial stack:\n${options.stack}`; | 
					
						
							|  |  |  |         reject(HTTPError('promiseAjax catch', 0, e.toString(), stack)); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function _retryAjax(url, options, providedLimit, providedCount) { | 
					
						
							|  |  |  |   const count = (providedCount || 0) + 1; | 
					
						
							|  |  |  |   const limit = providedLimit || 3; | 
					
						
							|  |  |  |   return _promiseAjax(url, options).catch(e => { | 
					
						
							|  |  |  |     if (e.name === 'HTTPError' && e.code === -1 && count < limit) { | 
					
						
							|  |  |  |       return new Promise(resolve => { | 
					
						
							|  |  |  |         setTimeout(() => { | 
					
						
							|  |  |  |           resolve(_retryAjax(url, options, limit, count)); | 
					
						
							|  |  |  |         }, 1000); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     throw e; | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function _outerAjax(url, options) { | 
					
						
							|  |  |  |   // eslint-disable-next-line no-param-reassign
 | 
					
						
							|  |  |  |   options.stack = new Error().stack; // just in case, save stack here.
 | 
					
						
							|  |  |  |   return _retryAjax(url, options); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function HTTPError(message, providedCode, response, stack) { | 
					
						
							|  |  |  |   const code = providedCode > 999 || providedCode < 100 ? -1 : providedCode; | 
					
						
							|  |  |  |   const e = new Error(`${message}; code: ${code}`); | 
					
						
							|  |  |  |   e.name = 'HTTPError'; | 
					
						
							|  |  |  |   e.code = code; | 
					
						
							|  |  |  |   e.stack += `\nOriginal stack:\n${stack}`; | 
					
						
							|  |  |  |   if (response) { | 
					
						
							|  |  |  |     e.response = response; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return e; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports = { | 
					
						
							|  |  |  |   initialize, | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // We first set up the data that won't change during this session of the app
 | 
					
						
							|  |  |  | function initialize() { | 
					
						
							|  |  |  |   // Thanks to function-hoisting, we can put this return statement before all of the
 | 
					
						
							|  |  |  |   //   below function definitions.
 | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     connect, | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Then we connect to the server with user-specific information. This is the only API
 | 
					
						
							|  |  |  |   //   exposed to the browser context, ensuring that it can't connect to arbitrary
 | 
					
						
							|  |  |  |   //   locations.
 | 
					
						
							|  |  |  |   function connect() { | 
					
						
							|  |  |  |     // Thanks, function hoisting!
 | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       getAttachment, | 
					
						
							|  |  |  |       getProxiedSize, | 
					
						
							|  |  |  |       makeProxiedRequest, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function getAttachment(fileUrl) { | 
					
						
							|  |  |  |       return _outerAjax(fileUrl, { | 
					
						
							|  |  |  |         contentType: 'application/octet-stream', | 
					
						
							|  |  |  |         responseType: 'arraybuffer', | 
					
						
							|  |  |  |         timeout: 0, | 
					
						
							|  |  |  |         type: 'GET', | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // eslint-disable-next-line no-shadow
 | 
					
						
							|  |  |  |     async function getProxiedSize(url) { | 
					
						
							|  |  |  |       const result = await _outerAjax(url, { | 
					
						
							|  |  |  |         processData: false, | 
					
						
							|  |  |  |         responseType: 'arraybufferwithdetails', | 
					
						
							|  |  |  |         proxyUrl: '', | 
					
						
							|  |  |  |         type: 'HEAD', | 
					
						
							|  |  |  |         disableLogs: true, | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const { response } = result; | 
					
						
							|  |  |  |       if (!response.headers || !response.headers.get) { | 
					
						
							|  |  |  |         throw new Error('getProxiedSize: Problem retrieving header value'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const size = response.headers.get('content-length'); | 
					
						
							|  |  |  |       return parseInt(size, 10); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // eslint-disable-next-line no-shadow
 | 
					
						
							|  |  |  |     function makeProxiedRequest(url, options = {}) { | 
					
						
							|  |  |  |       const { returnArrayBuffer, start, end } = options; | 
					
						
							|  |  |  |       let headers; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (_.isNumber(start) && _.isNumber(end)) { | 
					
						
							|  |  |  |         headers = { | 
					
						
							|  |  |  |           Range: `bytes=${start}-${end}`, | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return _outerAjax(url, { | 
					
						
							|  |  |  |         processData: false, | 
					
						
							|  |  |  |         responseType: returnArrayBuffer ? 'arraybufferwithdetails' : null, | 
					
						
							|  |  |  |         proxyUrl: '', | 
					
						
							|  |  |  |         type: 'GET', | 
					
						
							|  |  |  |         redirect: 'follow', | 
					
						
							|  |  |  |         disableLogs: true, | 
					
						
							|  |  |  |         headers, | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |