Better handling of failed message migration attempts

This commit is contained in:
trevor-signal 2024-11-18 16:35:47 -05:00 committed by GitHub
parent 512dcaad45
commit a068456139
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 84 additions and 74 deletions

View file

@ -270,15 +270,19 @@ describe('Message', () => {
upgrade: v3, upgrade: v3,
}); });
const context = getDefaultContext({ logger }); const actual = await Message.upgradeSchema(input, getDefaultContext(), {
const upgradeSchema = async (message: MessageAttributesType) => versions: [toVersion1, toVersion2, toVersion3],
toVersion3( });
await toVersion2(await toVersion1(message, context), context),
context
);
const actual = await upgradeSchema(input);
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
// if we try to upgrade it again, it will fail since it could not upgrade any
// versions
const upgradeAgainPromise = Message.upgradeSchema(
actual,
getDefaultContext(),
{ versions: [toVersion1, toVersion2, toVersion3] }
);
await assert.isRejected(upgradeAgainPromise);
}); });
it('should skip out-of-order upgrade steps', async () => { it('should skip out-of-order upgrade steps', async () => {
@ -391,12 +395,12 @@ describe('Message', () => {
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
it('should return original message if upgrade function throws', async () => { it('should throw if upgrade function throws', async () => {
const upgrade = async () => { const upgrade = async () => {
throw new Error('boom!'); throw new Error('boom!');
}; };
const upgradeWithVersion = Message._withSchemaVersion({ const upgradeWithVersion = Message._withSchemaVersion({
schemaVersion: 3, schemaVersion: 1,
upgrade, upgrade,
}); });
@ -404,15 +408,12 @@ describe('Message', () => {
id: 'guid-guid-guid-guid', id: 'guid-guid-guid-guid',
schemaVersion: 0, schemaVersion: 0,
}); });
const expected = getDefaultMessage({
id: 'guid-guid-guid-guid', const upgradePromise = upgradeWithVersion(
schemaVersion: 0,
});
const actual = await upgradeWithVersion(
input, input,
getDefaultContext({ logger }) getDefaultContext({ logger })
); );
assert.deepEqual(actual, expected); await assert.isRejected(upgradePromise);
}); });
it('should return original message if upgrade function returns null', async () => { it('should return original message if upgrade function returns null', async () => {
@ -866,31 +867,5 @@ describe('Message', () => {
assert.deepEqual({ ...message, schemaVersion: 14 }, result); assert.deepEqual({ ...message, schemaVersion: 14 }, result);
}); });
it('if file does exist, but migration errors, does not increment version', async () => {
const message = getDefaultMessage({
schemaVersion: 13,
schemaMigrationAttempts: 0,
attachments: [
{
size: 128,
contentType: MIME.IMAGE_BMP,
path: 'no/file/here.png',
iv: 'iv',
digest: 'digest',
key: 'key',
},
],
contact: [],
});
const result = await Message.upgradeSchema(message, {
...getDefaultContext(),
doesAttachmentExist: async () => true,
ensureAttachmentIsReencryptable: async () => {
throw new Error("Can't reencrypt!");
},
});
assert.deepEqual(message, result);
});
}); });
}); });

View file

@ -33,6 +33,7 @@ import { DAY } from '../util/durations';
import { getMessageQueueTime } from '../util/getMessageQueueTime'; import { getMessageQueueTime } from '../util/getMessageQueueTime';
import { getLocalAttachmentUrl } from '../util/getLocalAttachmentUrl'; import { getLocalAttachmentUrl } from '../util/getLocalAttachmentUrl';
import type { ReencryptionInfo } from '../AttachmentCrypto'; import type { ReencryptionInfo } from '../AttachmentCrypto';
import { redactGenericText } from '../util/privacy';
const MAX_WIDTH = 300; const MAX_WIDTH = 300;
const MAX_HEIGHT = MAX_WIDTH * 1.5; const MAX_HEIGHT = MAX_WIDTH * 1.5;
@ -1238,3 +1239,11 @@ export function isAttachmentLocallySaved(
): attachment is LocallySavedAttachment { ): attachment is LocallySavedAttachment {
return Boolean(attachment.path); return Boolean(attachment.path);
} }
export function getAttachmentIdForLogging(attachment: AttachmentType): string {
const { digest } = attachment;
if (typeof digest === 'string') {
return redactGenericText(digest);
}
return '[MissingDigest]';
}

View file

@ -15,6 +15,7 @@ import type {
} from './Attachment'; } from './Attachment';
import { import {
captureDimensionsAndScreenshot, captureDimensionsAndScreenshot,
getAttachmentIdForLogging,
isAttachmentLocallySaved, isAttachmentLocallySaved,
removeSchemaVersion, removeSchemaVersion,
replaceUnicodeOrderOverrides, replaceUnicodeOrderOverrides,
@ -51,7 +52,6 @@ import { encryptLegacyAttachment } from '../util/encryptLegacyAttachment';
import { deepClone } from '../util/deepClone'; import { deepClone } from '../util/deepClone';
import { LONG_ATTACHMENT_LIMIT } from './Message'; import { LONG_ATTACHMENT_LIMIT } from './Message';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { redactGenericText } from '../util/privacy';
export const GROUP = 'group'; export const GROUP = 'group';
export const PRIVATE = 'private'; export const PRIVATE = 'private';
@ -249,10 +249,11 @@ export const _withSchemaVersion = ({
upgradedMessage = await upgrade(message, context); upgradedMessage = await upgrade(message, context);
} catch (error) { } catch (error) {
logger.error( logger.error(
`Message._withSchemaVersion: error updating message ${message.id}:`, `Message._withSchemaVersion: error updating message ${message.id},
attempt ${message.schemaMigrationAttempts}:`,
Errors.toLogFormat(error) Errors.toLogFormat(error)
); );
return message; throw error;
} }
if (!isValid(upgradedMessage)) { if (!isValid(upgradedMessage)) {
@ -646,8 +647,6 @@ const toVersion14 = _withSchemaVersion({
attachment, attachment,
{ logger, ensureAttachmentIsReencryptable, doesAttachmentExist } { logger, ensureAttachmentIsReencryptable, doesAttachmentExist }
) => { ) => {
const logId = `Message2.toVersion14(digest=${redactGenericText(attachment.digest ?? '')})`;
if (!isAttachmentLocallySaved(attachment)) { if (!isAttachmentLocallySaved(attachment)) {
return attachment; return attachment;
} }
@ -655,7 +654,9 @@ const toVersion14 = _withSchemaVersion({
if (!(await doesAttachmentExist(attachment.path))) { if (!(await doesAttachmentExist(attachment.path))) {
// Attachments may be missing, e.g. for quote thumbnails that reference messages // Attachments may be missing, e.g. for quote thumbnails that reference messages
// which have been deleted // which have been deleted
logger.info(`${logId}: File does not exist`); logger.info(
`Message2.toVersion14(id=${getAttachmentIdForLogging(attachment)}: File does not exist`
);
return attachment; return attachment;
} }
@ -711,8 +712,17 @@ export const upgradeSchema = async (
deleteOnDisk, deleteOnDisk,
logger, logger,
maxVersion = CURRENT_SCHEMA_VERSION, maxVersion = CURRENT_SCHEMA_VERSION,
}: ContextType }: ContextType,
upgradeOptions: {
versions: ReadonlyArray<
(
message: MessageAttributesType,
context: ContextType
) => Promise<MessageAttributesType>
>;
} = { versions: VERSIONS }
): Promise<MessageAttributesType> => { ): Promise<MessageAttributesType> => {
const { versions } = upgradeOptions;
if (!isFunction(readAttachmentData)) { if (!isFunction(readAttachmentData)) {
throw new TypeError('context.readAttachmentData is required'); throw new TypeError('context.readAttachmentData is required');
} }
@ -748,30 +758,45 @@ export const upgradeSchema = async (
} }
let message = rawMessage; let message = rawMessage;
for (let index = 0, max = VERSIONS.length; index < max; index += 1) { const startingVersion = message.schemaVersion ?? 0;
for (let index = 0, max = versions.length; index < max; index += 1) {
if (maxVersion < index) { if (maxVersion < index) {
break; break;
} }
const currentVersion = VERSIONS[index]; const currentVersion = versions[index];
// We really do want this intra-loop await because this is a chained async action, try {
// each step dependent on the previous // We really do want this intra-loop await because this is a chained async action,
// eslint-disable-next-line no-await-in-loop // each step dependent on the previous
message = await currentVersion(message, { // eslint-disable-next-line no-await-in-loop
readAttachmentData, message = await currentVersion(message, {
writeNewAttachmentData, readAttachmentData,
makeObjectUrl, writeNewAttachmentData,
revokeObjectUrl, makeObjectUrl,
doesAttachmentExist, revokeObjectUrl,
ensureAttachmentIsReencryptable, doesAttachmentExist,
getImageDimensions, ensureAttachmentIsReencryptable,
makeImageThumbnail, getImageDimensions,
makeVideoScreenshot, makeImageThumbnail,
logger, makeVideoScreenshot,
getRegionCode, logger,
writeNewStickerData, getRegionCode,
deleteOnDisk, writeNewStickerData,
}); deleteOnDisk,
});
} catch (e) {
// Throw the error if we were unable to upgrade the message at all
if (message.schemaVersion === startingVersion) {
throw e;
} else {
// Otherwise, return the message upgraded as far as it could be. On the next
// migration attempt, it will fail.
logger.error(
`Upgraded message from ${startingVersion} -> ${message.schemaVersion}; failed on upgrade to ${index}`
);
break;
}
}
} }
return message; return message;

View file

@ -5,11 +5,11 @@ import {
type AttachmentType, type AttachmentType,
mightBeOnBackupTier, mightBeOnBackupTier,
AttachmentVariant, AttachmentVariant,
getAttachmentIdForLogging,
} from '../types/Attachment'; } from '../types/Attachment';
import { downloadAttachment as doDownloadAttachment } from '../textsecure/downloadAttachment'; import { downloadAttachment as doDownloadAttachment } from '../textsecure/downloadAttachment';
import { MediaTier } from '../types/AttachmentDownload'; import { MediaTier } from '../types/AttachmentDownload';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { redactGenericText } from './privacy';
import { HTTPError } from '../textsecure/Errors'; import { HTTPError } from '../textsecure/Errors';
import { toLogFormat } from '../types/errors'; import { toLogFormat } from '../types/errors';
import type { ReencryptedAttachmentV2 } from '../AttachmentCrypto'; import type { ReencryptedAttachmentV2 } from '../AttachmentCrypto';
@ -25,10 +25,10 @@ export async function downloadAttachment({
variant?: AttachmentVariant; variant?: AttachmentVariant;
dependencies?: { downloadAttachmentFromServer: typeof doDownloadAttachment }; dependencies?: { downloadAttachmentFromServer: typeof doDownloadAttachment };
}): Promise<ReencryptedAttachmentV2> { }): Promise<ReencryptedAttachmentV2> {
const redactedDigest = redactGenericText(attachment.digest ?? ''); const attachmentId = getAttachmentIdForLogging(attachment);
const variantForLogging = const variantForLogging =
variant !== AttachmentVariant.Default ? `[${variant}]` : ''; variant !== AttachmentVariant.Default ? `[${variant}]` : '';
const dataId = `${redactedDigest}${variantForLogging}`; const dataId = `${attachmentId}${variantForLogging}`;
const logId = `downloadAttachmentUtil(${dataId})`; const logId = `downloadAttachmentUtil(${dataId})`;
const { server } = window.textsecure; const { server } = window.textsecure;

View file

@ -17,11 +17,11 @@ import {
hasAllOriginalEncryptionInfo, hasAllOriginalEncryptionInfo,
isReencryptableToSameDigest, isReencryptableToSameDigest,
isReencryptableWithNewEncryptionInfo, isReencryptableWithNewEncryptionInfo,
getAttachmentIdForLogging,
} from '../types/Attachment'; } from '../types/Attachment';
import { strictAssert } from './assert'; import { strictAssert } from './assert';
import * as logging from '../logging/log'; import * as logging from '../logging/log';
import { fromBase64, toBase64 } from '../Bytes'; import { fromBase64, toBase64 } from '../Bytes';
import { redactGenericText } from './privacy';
import { toLogFormat } from '../types/errors'; import { toLogFormat } from '../types/errors';
/** /**
@ -37,7 +37,6 @@ import { toLogFormat } from '../types/errors';
export async function ensureAttachmentIsReencryptable( export async function ensureAttachmentIsReencryptable(
attachment: LocallySavedAttachment attachment: LocallySavedAttachment
): Promise<ReencryptableAttachment> { ): Promise<ReencryptableAttachment> {
const logId = `ensureAttachmentIsReencryptable(digest=${redactGenericText(attachment.digest ?? '')})`;
if (isReencryptableToSameDigest(attachment)) { if (isReencryptableToSameDigest(attachment)) {
return attachment; return attachment;
} }
@ -54,6 +53,8 @@ export async function ensureAttachmentIsReencryptable(
isReencryptableToSameDigest: true, isReencryptableToSameDigest: true,
}; };
} catch (e) { } catch (e) {
const logId = `ensureAttachmentIsReencryptable(digest=${getAttachmentIdForLogging(attachment)})`;
if (e instanceof ReencryptedDigestMismatchError) { if (e instanceof ReencryptedDigestMismatchError) {
logging.info( logging.info(
`${logId}: Unable to reencrypt attachment to original digest; must have had non-zero padding` `${logId}: Unable to reencrypt attachment to original digest; must have had non-zero padding`