2018-06-15 20:33:07 +08:00
|
|
|
/*
|
|
|
|
Copyright 2018 New Vector Ltd
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2022-03-02 04:42:05 +08:00
|
|
|
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
2018-06-15 20:33:07 +08:00
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
import { DecryptionFailureTracker } from "../src/DecryptionFailureTracker";
|
2021-10-23 06:23:32 +08:00
|
|
|
|
2018-07-05 20:54:44 +08:00
|
|
|
class MockDecryptionError extends Error {
|
|
|
|
constructor(code) {
|
|
|
|
super();
|
|
|
|
|
2022-06-30 15:55:05 +08:00
|
|
|
this.code = code || "MOCK_DECRYPTION_ERROR";
|
2018-07-05 20:54:44 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-15 20:33:07 +08:00
|
|
|
function createFailedDecryptionEvent() {
|
|
|
|
const event = new MatrixEvent({
|
|
|
|
event_id: "event-id-" + Math.random().toString(16).slice(2),
|
2022-06-30 15:55:05 +08:00
|
|
|
content: {
|
|
|
|
algorithm: "m.megolm.v1.aes-sha2",
|
|
|
|
},
|
2018-06-15 20:33:07 +08:00
|
|
|
});
|
2021-06-17 22:23:29 +08:00
|
|
|
event.setClearData(event.badEncryptedMessage(":("));
|
2018-06-15 20:33:07 +08:00
|
|
|
return event;
|
|
|
|
}
|
|
|
|
|
2018-07-05 21:55:29 +08:00
|
|
|
describe("DecryptionFailureTracker", function () {
|
2022-01-20 03:31:43 +08:00
|
|
|
it("tracks a failed decryption for a visible event", function (done) {
|
2018-06-15 20:33:07 +08:00
|
|
|
const failedDecryptionEvent = createFailedDecryptionEvent();
|
2018-06-28 22:03:47 +08:00
|
|
|
|
|
|
|
let count = 0;
|
2021-12-06 18:43:42 +08:00
|
|
|
const tracker = new DecryptionFailureTracker(
|
|
|
|
(total) => (count += total),
|
|
|
|
() => "UnknownError",
|
|
|
|
);
|
2018-06-15 20:33:07 +08:00
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
tracker.addVisibleEvent(failedDecryptionEvent);
|
|
|
|
|
2018-07-05 20:54:44 +08:00
|
|
|
const err = new MockDecryptionError();
|
|
|
|
tracker.eventDecrypted(failedDecryptionEvent, err);
|
2018-06-15 20:33:07 +08:00
|
|
|
|
|
|
|
// Pretend "now" is Infinity
|
|
|
|
tracker.checkFailures(Infinity);
|
|
|
|
|
2018-07-05 20:54:44 +08:00
|
|
|
// Immediately track the newest failures
|
|
|
|
tracker.trackFailures();
|
2018-06-15 20:33:07 +08:00
|
|
|
|
2018-12-25 10:13:09 +08:00
|
|
|
expect(count).not.toBe(0, "should track a failure for an event that failed decryption");
|
2018-06-15 20:33:07 +08:00
|
|
|
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
|
2022-05-05 11:46:03 +08:00
|
|
|
it("tracks a failed decryption with expected raw error for a visible event", function (done) {
|
|
|
|
const failedDecryptionEvent = createFailedDecryptionEvent();
|
|
|
|
|
|
|
|
let count = 0;
|
|
|
|
let reportedRawCode = "";
|
|
|
|
const tracker = new DecryptionFailureTracker(
|
|
|
|
(total, errcode, rawCode) => {
|
|
|
|
count += total;
|
|
|
|
reportedRawCode = rawCode;
|
|
|
|
},
|
|
|
|
() => "UnknownError",
|
|
|
|
);
|
|
|
|
|
|
|
|
tracker.addVisibleEvent(failedDecryptionEvent);
|
|
|
|
|
|
|
|
const err = new MockDecryptionError("INBOUND_SESSION_MISMATCH_ROOM_ID");
|
|
|
|
tracker.eventDecrypted(failedDecryptionEvent, err);
|
|
|
|
|
|
|
|
// Pretend "now" is Infinity
|
|
|
|
tracker.checkFailures(Infinity);
|
|
|
|
|
|
|
|
// Immediately track the newest failures
|
|
|
|
tracker.trackFailures();
|
|
|
|
|
|
|
|
expect(count).not.toBe(0, "should track a failure for an event that failed decryption");
|
|
|
|
expect(reportedRawCode).toBe("INBOUND_SESSION_MISMATCH_ROOM_ID", "Should add the rawCode to the event context");
|
|
|
|
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
it("tracks a failed decryption for an event that becomes visible later", function (done) {
|
|
|
|
const failedDecryptionEvent = createFailedDecryptionEvent();
|
|
|
|
|
|
|
|
let count = 0;
|
|
|
|
const tracker = new DecryptionFailureTracker(
|
|
|
|
(total) => (count += total),
|
|
|
|
() => "UnknownError",
|
|
|
|
);
|
|
|
|
|
|
|
|
const err = new MockDecryptionError();
|
|
|
|
tracker.eventDecrypted(failedDecryptionEvent, err);
|
|
|
|
|
|
|
|
tracker.addVisibleEvent(failedDecryptionEvent);
|
|
|
|
|
|
|
|
// Pretend "now" is Infinity
|
|
|
|
tracker.checkFailures(Infinity);
|
|
|
|
|
|
|
|
// Immediately track the newest failures
|
|
|
|
tracker.trackFailures();
|
|
|
|
|
|
|
|
expect(count).not.toBe(0, "should track a failure for an event that failed decryption");
|
|
|
|
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
|
|
|
|
it("does not track a failed decryption for an event that never becomes visible", function (done) {
|
|
|
|
const failedDecryptionEvent = createFailedDecryptionEvent();
|
|
|
|
|
|
|
|
let count = 0;
|
|
|
|
const tracker = new DecryptionFailureTracker(
|
|
|
|
(total) => (count += total),
|
|
|
|
() => "UnknownError",
|
|
|
|
);
|
|
|
|
|
|
|
|
const err = new MockDecryptionError();
|
|
|
|
tracker.eventDecrypted(failedDecryptionEvent, err);
|
|
|
|
|
|
|
|
// Pretend "now" is Infinity
|
|
|
|
tracker.checkFailures(Infinity);
|
|
|
|
|
|
|
|
// Immediately track the newest failures
|
|
|
|
tracker.trackFailures();
|
|
|
|
|
|
|
|
expect(count).toBe(0, "should not track a failure for an event that never became visible");
|
|
|
|
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
|
2018-06-15 20:33:07 +08:00
|
|
|
it("does not track a failed decryption where the event is subsequently successfully decrypted", (done) => {
|
|
|
|
const decryptedEvent = createFailedDecryptionEvent();
|
2018-06-28 22:03:47 +08:00
|
|
|
const tracker = new DecryptionFailureTracker(
|
|
|
|
(total) => {
|
2018-06-15 20:33:07 +08:00
|
|
|
expect(true).toBe(false, "should not track an event that has since been decrypted correctly");
|
2021-12-06 18:43:42 +08:00
|
|
|
},
|
|
|
|
() => "UnknownError",
|
|
|
|
);
|
2018-06-15 20:33:07 +08:00
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
|
2018-07-05 20:54:44 +08:00
|
|
|
const err = new MockDecryptionError();
|
|
|
|
tracker.eventDecrypted(decryptedEvent, err);
|
2018-06-15 20:33:07 +08:00
|
|
|
|
|
|
|
// Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted
|
2021-06-17 22:23:29 +08:00
|
|
|
decryptedEvent.setClearData({});
|
2018-07-05 20:54:44 +08:00
|
|
|
tracker.eventDecrypted(decryptedEvent, null);
|
2018-06-15 20:33:07 +08:00
|
|
|
|
|
|
|
// Pretend "now" is Infinity
|
|
|
|
tracker.checkFailures(Infinity);
|
|
|
|
|
2018-07-05 20:54:44 +08:00
|
|
|
// Immediately track the newest failures
|
|
|
|
tracker.trackFailures();
|
2018-06-15 20:33:07 +08:00
|
|
|
done();
|
|
|
|
});
|
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
it(
|
|
|
|
"does not track a failed decryption where the event is subsequently successfully decrypted " +
|
|
|
|
"and later becomes visible",
|
|
|
|
(done) => {
|
|
|
|
const decryptedEvent = createFailedDecryptionEvent();
|
|
|
|
const tracker = new DecryptionFailureTracker(
|
|
|
|
(total) => {
|
|
|
|
expect(true).toBe(false, "should not track an event that has since been decrypted correctly");
|
|
|
|
},
|
|
|
|
() => "UnknownError",
|
|
|
|
);
|
2022-12-12 19:24:14 +08:00
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
const err = new MockDecryptionError();
|
|
|
|
tracker.eventDecrypted(decryptedEvent, err);
|
2022-12-12 19:24:14 +08:00
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
// Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted
|
|
|
|
decryptedEvent.setClearData({});
|
|
|
|
tracker.eventDecrypted(decryptedEvent, null);
|
2022-12-12 19:24:14 +08:00
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
2022-12-12 19:24:14 +08:00
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
// Pretend "now" is Infinity
|
|
|
|
tracker.checkFailures(Infinity);
|
2022-12-12 19:24:14 +08:00
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
// Immediately track the newest failures
|
|
|
|
tracker.trackFailures();
|
|
|
|
done();
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2018-06-15 20:33:07 +08:00
|
|
|
it("only tracks a single failure per event, despite multiple failed decryptions for multiple events", (done) => {
|
|
|
|
const decryptedEvent = createFailedDecryptionEvent();
|
|
|
|
const decryptedEvent2 = createFailedDecryptionEvent();
|
|
|
|
|
|
|
|
let count = 0;
|
2021-12-06 18:43:42 +08:00
|
|
|
const tracker = new DecryptionFailureTracker(
|
|
|
|
(total) => (count += total),
|
|
|
|
() => "UnknownError",
|
|
|
|
);
|
2018-06-15 20:33:07 +08:00
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
|
2018-06-15 20:33:07 +08:00
|
|
|
// Arbitrary number of failed decryptions for both events
|
2018-07-05 20:54:44 +08:00
|
|
|
const err = new MockDecryptionError();
|
|
|
|
tracker.eventDecrypted(decryptedEvent, err);
|
|
|
|
tracker.eventDecrypted(decryptedEvent, err);
|
|
|
|
tracker.eventDecrypted(decryptedEvent, err);
|
|
|
|
tracker.eventDecrypted(decryptedEvent, err);
|
|
|
|
tracker.eventDecrypted(decryptedEvent, err);
|
|
|
|
tracker.eventDecrypted(decryptedEvent2, err);
|
|
|
|
tracker.eventDecrypted(decryptedEvent2, err);
|
2022-01-20 03:31:43 +08:00
|
|
|
tracker.addVisibleEvent(decryptedEvent2);
|
2018-07-05 20:54:44 +08:00
|
|
|
tracker.eventDecrypted(decryptedEvent2, err);
|
2018-06-15 20:33:07 +08:00
|
|
|
|
|
|
|
// Pretend "now" is Infinity
|
|
|
|
tracker.checkFailures(Infinity);
|
|
|
|
|
2018-07-05 20:54:44 +08:00
|
|
|
// Simulated polling of `trackFailures`, an arbitrary number ( > 2 ) times
|
|
|
|
tracker.trackFailures();
|
|
|
|
tracker.trackFailures();
|
|
|
|
tracker.trackFailures();
|
|
|
|
tracker.trackFailures();
|
2018-06-15 20:33:07 +08:00
|
|
|
|
|
|
|
expect(count).toBe(2, count + " failures tracked, should only track a single failure per event");
|
|
|
|
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
|
2018-06-15 21:48:20 +08:00
|
|
|
it("should not track a failure for an event that was tracked previously", (done) => {
|
|
|
|
const decryptedEvent = createFailedDecryptionEvent();
|
|
|
|
|
2018-06-28 22:03:47 +08:00
|
|
|
let count = 0;
|
2021-12-06 18:43:42 +08:00
|
|
|
const tracker = new DecryptionFailureTracker(
|
|
|
|
(total) => (count += total),
|
|
|
|
() => "UnknownError",
|
|
|
|
);
|
2018-06-15 21:48:20 +08:00
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
|
2018-06-15 21:48:20 +08:00
|
|
|
// Indicate decryption
|
2018-07-05 20:54:44 +08:00
|
|
|
const err = new MockDecryptionError();
|
|
|
|
tracker.eventDecrypted(decryptedEvent, err);
|
2018-06-15 21:48:20 +08:00
|
|
|
|
|
|
|
// Pretend "now" is Infinity
|
|
|
|
tracker.checkFailures(Infinity);
|
|
|
|
|
2018-07-05 20:54:44 +08:00
|
|
|
tracker.trackFailures();
|
2018-06-15 21:48:20 +08:00
|
|
|
|
|
|
|
// Indicate a second decryption, after having tracked the failure
|
2018-07-05 20:54:44 +08:00
|
|
|
tracker.eventDecrypted(decryptedEvent, err);
|
2018-06-15 21:48:20 +08:00
|
|
|
|
2018-07-05 20:54:44 +08:00
|
|
|
tracker.trackFailures();
|
2018-06-15 21:48:20 +08:00
|
|
|
|
2018-06-28 22:03:47 +08:00
|
|
|
expect(count).toBe(1, "should only track a single failure per event");
|
2018-06-15 21:48:20 +08:00
|
|
|
|
|
|
|
done();
|
|
|
|
});
|
2018-06-15 22:26:53 +08:00
|
|
|
|
2018-06-16 00:08:11 +08:00
|
|
|
xit("should not track a failure for an event that was tracked in a previous session", (done) => {
|
2018-06-15 22:26:53 +08:00
|
|
|
// This test uses localStorage, clear it beforehand
|
|
|
|
localStorage.clear();
|
|
|
|
|
|
|
|
const decryptedEvent = createFailedDecryptionEvent();
|
|
|
|
|
2018-06-28 22:03:47 +08:00
|
|
|
let count = 0;
|
2021-12-06 18:43:42 +08:00
|
|
|
const tracker = new DecryptionFailureTracker(
|
|
|
|
(total) => (count += total),
|
|
|
|
() => "UnknownError",
|
|
|
|
);
|
2018-06-15 22:26:53 +08:00
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
|
2018-06-15 22:26:53 +08:00
|
|
|
// Indicate decryption
|
2018-07-05 20:54:44 +08:00
|
|
|
const err = new MockDecryptionError();
|
|
|
|
tracker.eventDecrypted(decryptedEvent, err);
|
2018-06-15 22:26:53 +08:00
|
|
|
|
|
|
|
// Pretend "now" is Infinity
|
|
|
|
// NB: This saves to localStorage specific to DFT
|
|
|
|
tracker.checkFailures(Infinity);
|
|
|
|
|
2018-07-05 20:54:44 +08:00
|
|
|
tracker.trackFailures();
|
2018-06-15 22:26:53 +08:00
|
|
|
|
|
|
|
// Simulate the browser refreshing by destroying tracker and creating a new tracker
|
2021-12-06 18:43:42 +08:00
|
|
|
const secondTracker = new DecryptionFailureTracker(
|
|
|
|
(total) => (count += total),
|
|
|
|
() => "UnknownError",
|
|
|
|
);
|
2018-06-16 00:08:11 +08:00
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
secondTracker.addVisibleEvent(decryptedEvent);
|
|
|
|
|
|
|
|
//secondTracker.loadTrackedEvents();
|
2018-06-16 00:08:11 +08:00
|
|
|
|
2018-07-05 20:54:44 +08:00
|
|
|
secondTracker.eventDecrypted(decryptedEvent, err);
|
2018-06-15 22:26:53 +08:00
|
|
|
secondTracker.checkFailures(Infinity);
|
2018-07-05 20:54:44 +08:00
|
|
|
secondTracker.trackFailures();
|
2018-06-15 22:26:53 +08:00
|
|
|
|
2018-06-28 22:03:47 +08:00
|
|
|
expect(count).toBe(1, count + " failures tracked, should only track a single failure per event");
|
2018-06-15 22:26:53 +08:00
|
|
|
|
|
|
|
done();
|
|
|
|
});
|
2018-07-05 20:54:44 +08:00
|
|
|
|
|
|
|
it("should count different error codes separately for multiple failures with different error codes", () => {
|
|
|
|
const counts = {};
|
|
|
|
const tracker = new DecryptionFailureTracker(
|
|
|
|
(total, errorCode) => (counts[errorCode] = (counts[errorCode] || 0) + total),
|
2021-12-06 18:43:42 +08:00
|
|
|
(error) => (error === "UnknownError" ? "UnknownError" : "OlmKeysNotSentError"),
|
2018-07-05 20:54:44 +08:00
|
|
|
);
|
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
const decryptedEvent1 = createFailedDecryptionEvent();
|
|
|
|
const decryptedEvent2 = createFailedDecryptionEvent();
|
|
|
|
const decryptedEvent3 = createFailedDecryptionEvent();
|
|
|
|
|
|
|
|
const error1 = new MockDecryptionError("UnknownError");
|
|
|
|
const error2 = new MockDecryptionError("OlmKeysNotSentError");
|
|
|
|
|
|
|
|
tracker.addVisibleEvent(decryptedEvent1);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent2);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent3);
|
|
|
|
|
2018-07-05 20:54:44 +08:00
|
|
|
// One failure of ERROR_CODE_1, and effectively two for ERROR_CODE_2
|
2022-01-20 03:31:43 +08:00
|
|
|
tracker.eventDecrypted(decryptedEvent1, error1);
|
|
|
|
tracker.eventDecrypted(decryptedEvent2, error2);
|
|
|
|
tracker.eventDecrypted(decryptedEvent2, error2);
|
|
|
|
tracker.eventDecrypted(decryptedEvent3, error2);
|
2018-07-05 20:54:44 +08:00
|
|
|
|
|
|
|
// Pretend "now" is Infinity
|
|
|
|
tracker.checkFailures(Infinity);
|
|
|
|
|
|
|
|
tracker.trackFailures();
|
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
//expect(counts['UnknownError']).toBe(1, 'should track one UnknownError');
|
2021-12-06 18:43:42 +08:00
|
|
|
expect(counts["OlmKeysNotSentError"]).toBe(2, "should track two OlmKeysNotSentError");
|
2018-07-05 20:54:44 +08:00
|
|
|
});
|
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
it("should aggregate error codes correctly", () => {
|
2018-07-05 20:54:44 +08:00
|
|
|
const counts = {};
|
|
|
|
const tracker = new DecryptionFailureTracker(
|
|
|
|
(total, errorCode) => (counts[errorCode] = (counts[errorCode] || 0) + total),
|
2021-12-06 18:43:42 +08:00
|
|
|
(errorCode) => "OlmUnspecifiedError",
|
2018-07-05 20:54:44 +08:00
|
|
|
);
|
|
|
|
|
2022-01-20 03:31:43 +08:00
|
|
|
const decryptedEvent1 = createFailedDecryptionEvent();
|
|
|
|
const decryptedEvent2 = createFailedDecryptionEvent();
|
|
|
|
const decryptedEvent3 = createFailedDecryptionEvent();
|
|
|
|
|
|
|
|
const error1 = new MockDecryptionError("ERROR_CODE_1");
|
|
|
|
const error2 = new MockDecryptionError("ERROR_CODE_2");
|
|
|
|
const error3 = new MockDecryptionError("ERROR_CODE_3");
|
|
|
|
|
|
|
|
tracker.addVisibleEvent(decryptedEvent1);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent2);
|
|
|
|
tracker.addVisibleEvent(decryptedEvent3);
|
|
|
|
|
|
|
|
tracker.eventDecrypted(decryptedEvent1, error1);
|
|
|
|
tracker.eventDecrypted(decryptedEvent2, error2);
|
|
|
|
tracker.eventDecrypted(decryptedEvent3, error3);
|
2018-07-05 20:54:44 +08:00
|
|
|
|
|
|
|
// Pretend "now" is Infinity
|
|
|
|
tracker.checkFailures(Infinity);
|
|
|
|
|
|
|
|
tracker.trackFailures();
|
|
|
|
|
2021-12-06 18:43:42 +08:00
|
|
|
expect(counts["OlmUnspecifiedError"]).toBe(
|
|
|
|
3,
|
|
|
|
"should track three OlmUnspecifiedError, got " + counts["OlmUnspecifiedError"],
|
|
|
|
);
|
2018-07-05 20:54:44 +08:00
|
|
|
});
|
2022-01-20 03:31:43 +08:00
|
|
|
|
|
|
|
it("should remap error codes correctly", () => {
|
|
|
|
const counts = {};
|
|
|
|
const tracker = new DecryptionFailureTracker(
|
|
|
|
(total, errorCode) => (counts[errorCode] = (counts[errorCode] || 0) + total),
|
|
|
|
(errorCode) => Array.from(errorCode).reverse().join(""),
|
|
|
|
);
|
|
|
|
|
|
|
|
const decryptedEvent = createFailedDecryptionEvent();
|
|
|
|
|
|
|
|
const error = new MockDecryptionError("ERROR_CODE_1");
|
|
|
|
|
|
|
|
tracker.addVisibleEvent(decryptedEvent);
|
|
|
|
|
|
|
|
tracker.eventDecrypted(decryptedEvent, error);
|
|
|
|
|
|
|
|
// Pretend "now" is Infinity
|
|
|
|
tracker.checkFailures(Infinity);
|
|
|
|
|
|
|
|
tracker.trackFailures();
|
|
|
|
|
|
|
|
expect(counts["1_EDOC_RORRE"]).toBe(1, "should track remapped error code");
|
|
|
|
});
|
2018-06-15 20:33:07 +08:00
|
|
|
});
|