Require X-Signal-Timestamp header on all storage/group server 403 responses
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
parent
ebd360c3a3
commit
0d9cb81130
5 changed files with 70 additions and 5 deletions
|
@ -796,6 +796,10 @@
|
||||||
"messageformat": "Failed to send message with endorsements",
|
"messageformat": "Failed to send message with endorsements",
|
||||||
"description": "An error popup when we attempted and failed to send a message using endorsements, only for internal users."
|
"description": "An error popup when we attempted and failed to send a message using endorsements, only for internal users."
|
||||||
},
|
},
|
||||||
|
"icu:Toast--InvalidStorageServiceHeaders": {
|
||||||
|
"messageformat": "Received invalid response from storage service. Please share your logs.",
|
||||||
|
"description": "[Only shown to internal/beta users] An error popup when we noticed an invalid response (i.e. a web request response) from one of our servers"
|
||||||
|
},
|
||||||
"icu:Toast--FailedToImportBackup": {
|
"icu:Toast--FailedToImportBackup": {
|
||||||
"messageformat": "Failed to process some frames during backup import. Please share your logs.",
|
"messageformat": "Failed to process some frames during backup import. Please share your logs.",
|
||||||
"description": "[Only shown to internal users] An error popup when we failed to process some parts of a backup import."
|
"description": "[Only shown to internal users] An error popup when we failed to process some parts of a backup import."
|
||||||
|
|
|
@ -121,6 +121,8 @@ function getToast(toastType: ToastType): AnyToast {
|
||||||
return { toastType: ToastType.GroupLinkCopied };
|
return { toastType: ToastType.GroupLinkCopied };
|
||||||
case ToastType.InvalidConversation:
|
case ToastType.InvalidConversation:
|
||||||
return { toastType: ToastType.InvalidConversation };
|
return { toastType: ToastType.InvalidConversation };
|
||||||
|
case ToastType.InvalidStorageServiceHeaders:
|
||||||
|
return { toastType: ToastType.InvalidStorageServiceHeaders };
|
||||||
case ToastType.LeftGroup:
|
case ToastType.LeftGroup:
|
||||||
return { toastType: ToastType.LeftGroup };
|
return { toastType: ToastType.LeftGroup };
|
||||||
case ToastType.LinkCopied:
|
case ToastType.LinkCopied:
|
||||||
|
|
|
@ -328,6 +328,20 @@ export function renderToast({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toastType === ToastType.InvalidStorageServiceHeaders) {
|
||||||
|
return (
|
||||||
|
<Toast
|
||||||
|
onClose={hideToast}
|
||||||
|
toastAction={{
|
||||||
|
label: i18n('icu:Toast__ActionLabel--SubmitLog'),
|
||||||
|
onClick: onShowDebugLog,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n('icu:Toast--InvalidStorageServiceHeaders')}
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (toastType === ToastType.FileSaved) {
|
if (toastType === ToastType.FileSaved) {
|
||||||
return (
|
return (
|
||||||
<Toast
|
<Toast
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import type { RequestInit, Response } from 'node-fetch';
|
import type { RequestInit, Response } from 'node-fetch';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import type { Agent } from 'https';
|
import type { Agent } from 'https';
|
||||||
import { escapeRegExp, isNumber, isString, isObject } from 'lodash';
|
import { escapeRegExp, isNumber, isString, isObject, throttle } from 'lodash';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import { v4 as getGuid } from 'uuid';
|
import { v4 as getGuid } from 'uuid';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
@ -81,6 +81,8 @@ import type {
|
||||||
import { isMockServer } from '../util/isMockServer';
|
import { isMockServer } from '../util/isMockServer';
|
||||||
import { getMockServerPort } from '../util/getMockServerPort';
|
import { getMockServerPort } from '../util/getMockServerPort';
|
||||||
import { pemToDer } from '../util/pemToDer';
|
import { pemToDer } from '../util/pemToDer';
|
||||||
|
import { ToastType } from '../types/Toast';
|
||||||
|
import { isProduction } from '../util/version';
|
||||||
|
|
||||||
// Note: this will break some code that expects to be able to use err.response when a
|
// Note: this will break some code that expects to be able to use err.response when a
|
||||||
// web request fails, because it will force it to text. But it is very useful for
|
// web request fails, because it will force it to text. But it is very useful for
|
||||||
|
@ -194,6 +196,7 @@ type PromiseAjaxOptionsType = {
|
||||||
socketManager?: SocketManager;
|
socketManager?: SocketManager;
|
||||||
basicAuth?: string;
|
basicAuth?: string;
|
||||||
certificateAuthority?: string;
|
certificateAuthority?: string;
|
||||||
|
chatServiceUrl?: string;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
data?: Uint8Array | (() => Readable) | string;
|
data?: Uint8Array | (() => Readable) | string;
|
||||||
disableRetries?: boolean;
|
disableRetries?: boolean;
|
||||||
|
@ -212,8 +215,8 @@ type PromiseAjaxOptionsType = {
|
||||||
| 'byteswithdetails'
|
| 'byteswithdetails'
|
||||||
| 'stream'
|
| 'stream'
|
||||||
| 'streamwithdetails';
|
| 'streamwithdetails';
|
||||||
serverUrl?: string;
|
|
||||||
stack?: string;
|
stack?: string;
|
||||||
|
storageUrl?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
type: HTTPCodeType;
|
type: HTTPCodeType;
|
||||||
user?: string;
|
user?: string;
|
||||||
|
@ -401,9 +404,35 @@ async function _promiseAjax(
|
||||||
throw makeHTTPError('promiseAjax catch', 0, {}, e.toString(), stack);
|
throw makeHTTPError('promiseAjax catch', 0, {}, e.toString(), stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const urlHostname = getHostname(url);
|
||||||
|
|
||||||
|
if (options.storageUrl && url.startsWith(options.storageUrl)) {
|
||||||
|
// The cloud infrastructure that sits in front of the Storage Service / Groups server
|
||||||
|
// has in the past terminated requests with a 403 before they make it to a Signal
|
||||||
|
// server. That's a problem, since we might take destructive action locally in
|
||||||
|
// response to a 403. Responses from a Signal server should always contain the
|
||||||
|
// `x-signal-timestamp` headers.
|
||||||
|
if (response.headers.get('x-signal-timestamp') == null) {
|
||||||
|
log.error(
|
||||||
|
logId,
|
||||||
|
response.status,
|
||||||
|
'Invalid header: missing required x-signal-timestamp header'
|
||||||
|
);
|
||||||
|
|
||||||
|
onIncorrectHeadersFromStorageService();
|
||||||
|
|
||||||
|
// TODO: DESKTOP-8300
|
||||||
|
if (response.status === 403) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId} ${response.status}: Dropping response, missing required x-signal-timestamp header`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
options.serverUrl &&
|
options.chatServiceUrl &&
|
||||||
getHostname(options.serverUrl) === getHostname(url)
|
getHostname(options.chatServiceUrl) === urlHostname
|
||||||
) {
|
) {
|
||||||
await handleStatusCode(response.status);
|
await handleStatusCode(response.status);
|
||||||
|
|
||||||
|
@ -2004,6 +2033,7 @@ export function initialize({
|
||||||
socketManager: useWebSocketForEndpoint ? socketManager : undefined,
|
socketManager: useWebSocketForEndpoint ? socketManager : undefined,
|
||||||
basicAuth: param.basicAuth,
|
basicAuth: param.basicAuth,
|
||||||
certificateAuthority,
|
certificateAuthority,
|
||||||
|
chatServiceUrl,
|
||||||
contentType: param.contentType || 'application/json; charset=utf-8',
|
contentType: param.contentType || 'application/json; charset=utf-8',
|
||||||
data:
|
data:
|
||||||
param.data ||
|
param.data ||
|
||||||
|
@ -2018,7 +2048,7 @@ export function initialize({
|
||||||
type: param.httpType,
|
type: param.httpType,
|
||||||
user: param.username ?? username,
|
user: param.username ?? username,
|
||||||
redactUrl: param.redactUrl,
|
redactUrl: param.redactUrl,
|
||||||
serverUrl: chatServiceUrl,
|
storageUrl,
|
||||||
validateResponse: param.validateResponse,
|
validateResponse: param.validateResponse,
|
||||||
version,
|
version,
|
||||||
unauthenticated: param.unauthenticated,
|
unauthenticated: param.unauthenticated,
|
||||||
|
@ -4666,3 +4696,16 @@ export function initialize({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: DESKTOP-8300
|
||||||
|
const onIncorrectHeadersFromStorageService = throttle(
|
||||||
|
() => {
|
||||||
|
if (!isProduction(window.getVersion())) {
|
||||||
|
window.reduxActions.toast.showToast({
|
||||||
|
toastType: ToastType.InvalidStorageServiceHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
5 * MINUTE,
|
||||||
|
{ trailing: false }
|
||||||
|
);
|
||||||
|
|
|
@ -40,6 +40,7 @@ export enum ToastType {
|
||||||
FileSize = 'FileSize',
|
FileSize = 'FileSize',
|
||||||
GroupLinkCopied = 'GroupLinkCopied',
|
GroupLinkCopied = 'GroupLinkCopied',
|
||||||
InvalidConversation = 'InvalidConversation',
|
InvalidConversation = 'InvalidConversation',
|
||||||
|
InvalidStorageServiceHeaders = 'InvalidStorageServiceHeaders',
|
||||||
LeftGroup = 'LeftGroup',
|
LeftGroup = 'LeftGroup',
|
||||||
LinkCopied = 'LinkCopied',
|
LinkCopied = 'LinkCopied',
|
||||||
LoadingFullLogs = 'LoadingFullLogs',
|
LoadingFullLogs = 'LoadingFullLogs',
|
||||||
|
@ -133,6 +134,7 @@ export type AnyToast =
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| { toastType: ToastType.InvalidConversation }
|
| { toastType: ToastType.InvalidConversation }
|
||||||
|
| { toastType: ToastType.InvalidStorageServiceHeaders }
|
||||||
| { toastType: ToastType.LeftGroup }
|
| { toastType: ToastType.LeftGroup }
|
||||||
| { toastType: ToastType.LinkCopied }
|
| { toastType: ToastType.LinkCopied }
|
||||||
| { toastType: ToastType.LoadingFullLogs }
|
| { toastType: ToastType.LoadingFullLogs }
|
||||||
|
|
Loading…
Add table
Reference in a new issue