Skip to content

Commit a068456

Browse files
Better handling of failed message migration attempts
1 parent 512dcaa commit a068456

File tree

5 files changed

+84
-74
lines changed

5 files changed

+84
-74
lines changed

ts/test-node/types/Message2_test.ts

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -270,15 +270,19 @@ describe('Message', () => {
270270
upgrade: v3,
271271
});
272272

273-
const context = getDefaultContext({ logger });
274-
const upgradeSchema = async (message: MessageAttributesType) =>
275-
toVersion3(
276-
await toVersion2(await toVersion1(message, context), context),
277-
context
278-
);
279-
280-
const actual = await upgradeSchema(input);
273+
const actual = await Message.upgradeSchema(input, getDefaultContext(), {
274+
versions: [toVersion1, toVersion2, toVersion3],
275+
});
281276
assert.deepEqual(actual, expected);
277+
278+
// if we try to upgrade it again, it will fail since it could not upgrade any
279+
// versions
280+
const upgradeAgainPromise = Message.upgradeSchema(
281+
actual,
282+
getDefaultContext(),
283+
{ versions: [toVersion1, toVersion2, toVersion3] }
284+
);
285+
await assert.isRejected(upgradeAgainPromise);
282286
});
283287

284288
it('should skip out-of-order upgrade steps', async () => {
@@ -391,28 +395,25 @@ describe('Message', () => {
391395
assert.deepEqual(actual, expected);
392396
});
393397

394-
it('should return original message if upgrade function throws', async () => {
398+
it('should throw if upgrade function throws', async () => {
395399
const upgrade = async () => {
396400
throw new Error('boom!');
397401
};
398402
const upgradeWithVersion = Message._withSchemaVersion({
399-
schemaVersion: 3,
403+
schemaVersion: 1,
400404
upgrade,
401405
});
402406

403407
const input = getDefaultMessage({
404408
id: 'guid-guid-guid-guid',
405409
schemaVersion: 0,
406410
});
407-
const expected = getDefaultMessage({
408-
id: 'guid-guid-guid-guid',
409-
schemaVersion: 0,
410-
});
411-
const actual = await upgradeWithVersion(
411+
412+
const upgradePromise = upgradeWithVersion(
412413
input,
413414
getDefaultContext({ logger })
414415
);
415-
assert.deepEqual(actual, expected);
416+
await assert.isRejected(upgradePromise);
416417
});
417418

418419
it('should return original message if upgrade function returns null', async () => {
@@ -866,31 +867,5 @@ describe('Message', () => {
866867

867868
assert.deepEqual({ ...message, schemaVersion: 14 }, result);
868869
});
869-
it('if file does exist, but migration errors, does not increment version', async () => {
870-
const message = getDefaultMessage({
871-
schemaVersion: 13,
872-
schemaMigrationAttempts: 0,
873-
attachments: [
874-
{
875-
size: 128,
876-
contentType: MIME.IMAGE_BMP,
877-
path: 'no/file/here.png',
878-
iv: 'iv',
879-
digest: 'digest',
880-
key: 'key',
881-
},
882-
],
883-
contact: [],
884-
});
885-
const result = await Message.upgradeSchema(message, {
886-
...getDefaultContext(),
887-
doesAttachmentExist: async () => true,
888-
ensureAttachmentIsReencryptable: async () => {
889-
throw new Error("Can't reencrypt!");
890-
},
891-
});
892-
893-
assert.deepEqual(message, result);
894-
});
895870
});
896871
});

ts/types/Attachment.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { DAY } from '../util/durations';
3333
import { getMessageQueueTime } from '../util/getMessageQueueTime';
3434
import { getLocalAttachmentUrl } from '../util/getLocalAttachmentUrl';
3535
import type { ReencryptionInfo } from '../AttachmentCrypto';
36+
import { redactGenericText } from '../util/privacy';
3637

3738
const MAX_WIDTH = 300;
3839
const MAX_HEIGHT = MAX_WIDTH * 1.5;
@@ -1238,3 +1239,11 @@ export function isAttachmentLocallySaved(
12381239
): attachment is LocallySavedAttachment {
12391240
return Boolean(attachment.path);
12401241
}
1242+
1243+
export function getAttachmentIdForLogging(attachment: AttachmentType): string {
1244+
const { digest } = attachment;
1245+
if (typeof digest === 'string') {
1246+
return redactGenericText(digest);
1247+
}
1248+
return '[MissingDigest]';
1249+
}

ts/types/Message2.ts

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
} from './Attachment';
1616
import {
1717
captureDimensionsAndScreenshot,
18+
getAttachmentIdForLogging,
1819
isAttachmentLocallySaved,
1920
removeSchemaVersion,
2021
replaceUnicodeOrderOverrides,
@@ -51,7 +52,6 @@ import { encryptLegacyAttachment } from '../util/encryptLegacyAttachment';
5152
import { deepClone } from '../util/deepClone';
5253
import { LONG_ATTACHMENT_LIMIT } from './Message';
5354
import * as Bytes from '../Bytes';
54-
import { redactGenericText } from '../util/privacy';
5555

5656
export const GROUP = 'group';
5757
export const PRIVATE = 'private';
@@ -249,10 +249,11 @@ export const _withSchemaVersion = ({
249249
upgradedMessage = await upgrade(message, context);
250250
} catch (error) {
251251
logger.error(
252-
`Message._withSchemaVersion: error updating message ${message.id}:`,
252+
`Message._withSchemaVersion: error updating message ${message.id},
253+
attempt ${message.schemaMigrationAttempts}:`,
253254
Errors.toLogFormat(error)
254255
);
255-
return message;
256+
throw error;
256257
}
257258

258259
if (!isValid(upgradedMessage)) {
@@ -646,16 +647,16 @@ const toVersion14 = _withSchemaVersion({
646647
attachment,
647648
{ logger, ensureAttachmentIsReencryptable, doesAttachmentExist }
648649
) => {
649-
const logId = `Message2.toVersion14(digest=${redactGenericText(attachment.digest ?? '')})`;
650-
651650
if (!isAttachmentLocallySaved(attachment)) {
652651
return attachment;
653652
}
654653

655654
if (!(await doesAttachmentExist(attachment.path))) {
656655
// Attachments may be missing, e.g. for quote thumbnails that reference messages
657656
// which have been deleted
658-
logger.info(`${logId}: File does not exist`);
657+
logger.info(
658+
`Message2.toVersion14(id=${getAttachmentIdForLogging(attachment)}: File does not exist`
659+
);
659660
return attachment;
660661
}
661662

@@ -711,8 +712,17 @@ export const upgradeSchema = async (
711712
deleteOnDisk,
712713
logger,
713714
maxVersion = CURRENT_SCHEMA_VERSION,
714-
}: ContextType
715+
}: ContextType,
716+
upgradeOptions: {
717+
versions: ReadonlyArray<
718+
(
719+
message: MessageAttributesType,
720+
context: ContextType
721+
) => Promise<MessageAttributesType>
722+
>;
723+
} = { versions: VERSIONS }
715724
): Promise<MessageAttributesType> => {
725+
const { versions } = upgradeOptions;
716726
if (!isFunction(readAttachmentData)) {
717727
throw new TypeError('context.readAttachmentData is required');
718728
}
@@ -748,30 +758,45 @@ export const upgradeSchema = async (
748758
}
749759

750760
let message = rawMessage;
751-
for (let index = 0, max = VERSIONS.length; index < max; index += 1) {
761+
const startingVersion = message.schemaVersion ?? 0;
762+
for (let index = 0, max = versions.length; index < max; index += 1) {
752763
if (maxVersion < index) {
753764
break;
754765
}
755766

756-
const currentVersion = VERSIONS[index];
757-
// We really do want this intra-loop await because this is a chained async action,
758-
// each step dependent on the previous
759-
// eslint-disable-next-line no-await-in-loop
760-
message = await currentVersion(message, {
761-
readAttachmentData,
762-
writeNewAttachmentData,
763-
makeObjectUrl,
764-
revokeObjectUrl,
765-
doesAttachmentExist,
766-
ensureAttachmentIsReencryptable,
767-
getImageDimensions,
768-
makeImageThumbnail,
769-
makeVideoScreenshot,
770-
logger,
771-
getRegionCode,
772-
writeNewStickerData,
773-
deleteOnDisk,
774-
});
767+
const currentVersion = versions[index];
768+
try {
769+
// We really do want this intra-loop await because this is a chained async action,
770+
// each step dependent on the previous
771+
// eslint-disable-next-line no-await-in-loop
772+
message = await currentVersion(message, {
773+
readAttachmentData,
774+
writeNewAttachmentData,
775+
makeObjectUrl,
776+
revokeObjectUrl,
777+
doesAttachmentExist,
778+
ensureAttachmentIsReencryptable,
779+
getImageDimensions,
780+
makeImageThumbnail,
781+
makeVideoScreenshot,
782+
logger,
783+
getRegionCode,
784+
writeNewStickerData,
785+
deleteOnDisk,
786+
});
787+
} catch (e) {
788+
// Throw the error if we were unable to upgrade the message at all
789+
if (message.schemaVersion === startingVersion) {
790+
throw e;
791+
} else {
792+
// Otherwise, return the message upgraded as far as it could be. On the next
793+
// migration attempt, it will fail.
794+
logger.error(
795+
`Upgraded message from ${startingVersion} -> ${message.schemaVersion}; failed on upgrade to ${index}`
796+
);
797+
break;
798+
}
799+
}
775800
}
776801

777802
return message;

ts/util/downloadAttachment.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import {
55
type AttachmentType,
66
mightBeOnBackupTier,
77
AttachmentVariant,
8+
getAttachmentIdForLogging,
89
} from '../types/Attachment';
910
import { downloadAttachment as doDownloadAttachment } from '../textsecure/downloadAttachment';
1011
import { MediaTier } from '../types/AttachmentDownload';
1112
import * as log from '../logging/log';
12-
import { redactGenericText } from './privacy';
1313
import { HTTPError } from '../textsecure/Errors';
1414
import { toLogFormat } from '../types/errors';
1515
import type { ReencryptedAttachmentV2 } from '../AttachmentCrypto';
@@ -25,10 +25,10 @@ export async function downloadAttachment({
2525
variant?: AttachmentVariant;
2626
dependencies?: { downloadAttachmentFromServer: typeof doDownloadAttachment };
2727
}): Promise<ReencryptedAttachmentV2> {
28-
const redactedDigest = redactGenericText(attachment.digest ?? '');
28+
const attachmentId = getAttachmentIdForLogging(attachment);
2929
const variantForLogging =
3030
variant !== AttachmentVariant.Default ? `[${variant}]` : '';
31-
const dataId = `${redactedDigest}${variantForLogging}`;
31+
const dataId = `${attachmentId}${variantForLogging}`;
3232
const logId = `downloadAttachmentUtil(${dataId})`;
3333

3434
const { server } = window.textsecure;

ts/util/ensureAttachmentIsReencryptable.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ import {
1717
hasAllOriginalEncryptionInfo,
1818
isReencryptableToSameDigest,
1919
isReencryptableWithNewEncryptionInfo,
20+
getAttachmentIdForLogging,
2021
} from '../types/Attachment';
2122
import { strictAssert } from './assert';
2223
import * as logging from '../logging/log';
2324
import { fromBase64, toBase64 } from '../Bytes';
24-
import { redactGenericText } from './privacy';
2525
import { toLogFormat } from '../types/errors';
2626

2727
/**
@@ -37,7 +37,6 @@ import { toLogFormat } from '../types/errors';
3737
export async function ensureAttachmentIsReencryptable(
3838
attachment: LocallySavedAttachment
3939
): Promise<ReencryptableAttachment> {
40-
const logId = `ensureAttachmentIsReencryptable(digest=${redactGenericText(attachment.digest ?? '')})`;
4140
if (isReencryptableToSameDigest(attachment)) {
4241
return attachment;
4342
}
@@ -54,6 +53,8 @@ export async function ensureAttachmentIsReencryptable(
5453
isReencryptableToSameDigest: true,
5554
};
5655
} catch (e) {
56+
const logId = `ensureAttachmentIsReencryptable(digest=${getAttachmentIdForLogging(attachment)})`;
57+
5758
if (e instanceof ReencryptedDigestMismatchError) {
5859
logging.info(
5960
`${logId}: Unable to reencrypt attachment to original digest; must have had non-zero padding`

0 commit comments

Comments
 (0)