Link Previews

This commit is contained in:
Scott Nonnenberg 2019-01-15 19:03:56 -08:00
parent 91ef39e482
commit 813924685e
36 changed files with 2298 additions and 134 deletions

View file

@ -5,9 +5,7 @@ const { Agent } = require('https');
const is = require('@sindresorhus/is');
/* global Buffer: false */
/* global setTimeout: false */
/* global log: false */
/* global Buffer, setTimeout, log, _ */
/* eslint-disable more/no-then, no-bitwise, no-nested-ternary */
@ -166,12 +164,28 @@ const agents = {
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}`;
log.info(
`${options.type} ${url}${options.unauthenticated ? ' (unauth)' : ''}`
);
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;
@ -195,7 +209,12 @@ function _promiseAjax(providedUrl, options) {
const fetchOptions = {
method: options.type,
body: options.data || null,
headers: { 'X-Signal-Agent': 'OWD' },
headers: {
'User-Agent': 'Signal Desktop (+https://signal.org/download)',
'X-Signal-Agent': 'OWD',
...options.headers,
},
redirect: options.redirect,
agent,
ca: options.certificateAuthority,
timeout,
@ -238,13 +257,20 @@ function _promiseAjax(providedUrl, options) {
response.headers.get('Content-Type') === 'application/json'
) {
resultPromise = response.json();
} else if (options.responseType === 'arraybuffer') {
} else if (
options.responseType === 'arraybuffer' ||
options.responseType === 'arraybufferwithdetails'
) {
resultPromise = response.buffer();
} else {
resultPromise = response.text();
}
return resultPromise.then(result => {
if (options.responseType === 'arraybuffer') {
if (
options.responseType === 'arraybuffer' ||
options.responseType === 'arraybufferwithdetails'
) {
// eslint-disable-next-line no-param-reassign
result = result.buffer.slice(
result.byteOffset,
@ -254,8 +280,17 @@ function _promiseAjax(providedUrl, options) {
if (options.responseType === 'json') {
if (options.validateResponse) {
if (!_validateResponse(result, options.validateResponse)) {
log.error(options.type, url, response.status, 'Error');
reject(
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,
@ -267,23 +302,47 @@ function _promiseAjax(providedUrl, options) {
}
}
if (response.status >= 0 && response.status < 400) {
log.info(options.type, url, response.status, 'Success');
resolve(result, response.status);
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');
reject(
HTTPError(
'promiseAjax: error response',
response.status,
result,
options.stack
)
);
}
return reject(
HTTPError(
'promiseAjax: error response',
response.status,
result,
options.stack
)
);
});
})
.catch(e => {
log.error(options.type, url, 0, 'Error');
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));
});
@ -342,7 +401,13 @@ module.exports = {
};
// We first set up the data that won't change during this session of the app
function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
function initialize({
url,
cdnUrl,
certificateAuthority,
contentProxyUrl,
proxyUrl,
}) {
if (!is.string(url)) {
throw new Error('WebAPI.initialize: Invalid server url');
}
@ -352,6 +417,9 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
if (!is.string(certificateAuthority)) {
throw new Error('WebAPI.initialize: Invalid certificateAuthority');
}
if (!is.string(contentProxyUrl)) {
throw new Error('WebAPI.initialize: Invalid contentProxyUrl');
}
// Thanks to function-hoisting, we can put this return statement before all of the
// below function definitions.
@ -372,8 +440,6 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
getAttachment,
getAvatar,
getDevices,
getSenderCertificate,
registerSupportForUnauthenticatedDelivery,
getKeysForNumber,
getKeysForNumberUnauth,
getMessageSocket,
@ -381,15 +447,19 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
getProfile,
getProfileUnauth,
getProvisioningSocket,
getProxiedSize,
getSenderCertificate,
makeProxiedRequest,
putAttachment,
registerKeys,
registerSupportForUnauthenticatedDelivery,
removeSignalingKey,
requestVerificationSMS,
requestVerificationVoice,
sendMessages,
sendMessagesUnauth,
setSignedPreKey,
updateDeviceName,
removeSignalingKey,
};
function _ajax(param) {
@ -799,6 +869,47 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
);
}
// eslint-disable-next-line no-shadow
async function getProxiedSize(url) {
const result = await _outerAjax(url, {
processData: false,
responseType: 'arraybufferwithdetails',
proxyUrl: contentProxyUrl,
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: contentProxyUrl,
type: 'GET',
redirect: 'follow',
disableLogs: true,
headers,
});
}
function getMessageSocket() {
log.info('opening message socket', url);
const fixedScheme = url