Compare commits
61 Commits
v0.7.6-rc.2
...
v0.7.9
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f30d21fa2 | |||
| 4257c8c9f5 | |||
| 331859d383 | |||
| ef03b708a8 | |||
| 716d098361 | |||
| d887057660 | |||
| 7efbfebb4d | |||
| 4c7afe5af0 | |||
| 676515cf27 | |||
| 0eb5b0fdfa | |||
| 2feba4787f | |||
| 516dc1043e | |||
| b26c1c57dc | |||
| 0945ba9e90 | |||
| 69ed6f283d | |||
| 9eef850d0c | |||
| cf1574d690 | |||
| d6913e41a0 | |||
| 3c81c295c7 | |||
| 56dfa0c755 | |||
| 43989be768 | |||
| 822380ac38 | |||
| 8c37d9ac9a | |||
| e40b8461f7 | |||
| a3f45b466a | |||
| 672ad68c64 | |||
| 4ccec13739 | |||
| 09529a1aa8 | |||
| d182fd6bb7 | |||
| 36bf123e2b | |||
| 92cfbf655f | |||
| fbef701179 | |||
| 0415b9cf4c | |||
| cb9a9e8d50 | |||
| 6021c1c6b1 | |||
| 655be2fa2e | |||
| 98491a63a7 | |||
| acd7f15c83 | |||
| 5020d4e99f | |||
| 9693c30209 | |||
| 2b6f8adc64 | |||
| 822f5927e5 | |||
| 0f6e9d7b9d | |||
| 99f3e3f09e | |||
| aa81c96a98 | |||
| 9d532b6c72 | |||
| 4c63906b8f | |||
| dd2a870227 | |||
| 88948c3cfd | |||
| b33dcfe6ff | |||
| 2c15bdae04 | |||
| 2f45633312 | |||
| fdd42fbc6d | |||
| 54a6f5d425 | |||
| 68d9662fe5 | |||
| 4f0987da01 | |||
| 625697e097 | |||
| 92b14f20d2 | |||
| dd069647d1 | |||
| 4523ae7d29 | |||
| 19e5eda773 |
+3
-1
@@ -1,4 +1,6 @@
|
||||
.jsdoc
|
||||
/.jsdocbuild
|
||||
/.jsdoc
|
||||
|
||||
node_modules
|
||||
.lock-wscript
|
||||
build/Release
|
||||
|
||||
@@ -1,3 +1,74 @@
|
||||
Changes in [0.7.9](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.9) (2017-06-01)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.8...v0.7.9)
|
||||
|
||||
* Initial framework for indexeddb-backed crypto store
|
||||
[\#445](https://github.com/matrix-org/matrix-js-sdk/pull/445)
|
||||
* Factor out reEmit to a common module
|
||||
[\#444](https://github.com/matrix-org/matrix-js-sdk/pull/444)
|
||||
* crypto/algorithms/base.js: Convert to es6
|
||||
[\#443](https://github.com/matrix-org/matrix-js-sdk/pull/443)
|
||||
* maySendRedactionForEvent for userId
|
||||
[\#435](https://github.com/matrix-org/matrix-js-sdk/pull/435)
|
||||
* MatrixClient: add getUserId()
|
||||
[\#441](https://github.com/matrix-org/matrix-js-sdk/pull/441)
|
||||
* Run jsdoc on a custom babeling of the source
|
||||
[\#442](https://github.com/matrix-org/matrix-js-sdk/pull/442)
|
||||
* Add in a public api getStoredDevice allowing clients to get a specific
|
||||
device
|
||||
[\#439](https://github.com/matrix-org/matrix-js-sdk/pull/439)
|
||||
|
||||
Changes in [0.7.8](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.8) (2017-05-22)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.8-rc.1...v0.7.8)
|
||||
|
||||
* No changes
|
||||
|
||||
|
||||
Changes in [0.7.8-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.8-rc.1) (2017-05-19)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.7...v0.7.8-rc.1)
|
||||
|
||||
* Attempt to rework the release-tarball-signing stuff
|
||||
[\#438](https://github.com/matrix-org/matrix-js-sdk/pull/438)
|
||||
* ability to specify webrtc audio/video inputs for the lib to request
|
||||
[\#427](https://github.com/matrix-org/matrix-js-sdk/pull/427)
|
||||
* make screen sharing call FF friendly :D
|
||||
[\#434](https://github.com/matrix-org/matrix-js-sdk/pull/434)
|
||||
* Fix race in device list updates
|
||||
[\#431](https://github.com/matrix-org/matrix-js-sdk/pull/431)
|
||||
* WebRTC: Support recvonly for video for those without a webcam
|
||||
[\#424](https://github.com/matrix-org/matrix-js-sdk/pull/424)
|
||||
* Update istanbul to remove minimatch DoS Warning
|
||||
[\#422](https://github.com/matrix-org/matrix-js-sdk/pull/422)
|
||||
* webrtc/call: Make it much less likely that callIds collide locally
|
||||
[\#423](https://github.com/matrix-org/matrix-js-sdk/pull/423)
|
||||
* Automatically complete dummy auth
|
||||
[\#420](https://github.com/matrix-org/matrix-js-sdk/pull/420)
|
||||
* Don't leave the gh-pages branch checked out
|
||||
[\#418](https://github.com/matrix-org/matrix-js-sdk/pull/418)
|
||||
|
||||
Changes in [0.7.7](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.7) (2017-04-25)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.7-rc.1...v0.7.7)
|
||||
|
||||
* No changes
|
||||
|
||||
|
||||
Changes in [0.7.7-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.7-rc.1) (2017-04-21)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6...v0.7.7-rc.1)
|
||||
|
||||
* Automatically complete dummy auth
|
||||
[\#420](https://github.com/matrix-org/matrix-js-sdk/pull/420)
|
||||
|
||||
|
||||
Changes in [0.7.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6) (2017-04-12)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6-rc.2...v0.7.6)
|
||||
|
||||
* No changes
|
||||
|
||||
Changes in [0.7.6-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6-rc.2) (2017-04-10)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6-rc.1...v0.7.6-rc.2)
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
var matrixcs = require("./lib/matrix");
|
||||
matrixcs.request(require("browser-request"));
|
||||
|
||||
matrixcs.setCryptoStoreFactory(
|
||||
function() {
|
||||
return new matrixcs.IndexedDBCryptoStore(
|
||||
global.indexedDB, "matrix-js-sdk:crypto"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = matrixcs; // keep export for browserify package deps
|
||||
global.matrixcs = matrixcs;
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.7.6-rc.2",
|
||||
"version": "0.7.9",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -8,12 +8,12 @@
|
||||
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" _mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
|
||||
"test": "npm run test:build && npm run test:run",
|
||||
"check": "npm run test:build && _mocha --recursive specbuild --colors",
|
||||
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
|
||||
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
|
||||
"start": "babel -s -w -d lib src",
|
||||
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
|
||||
"dist": "npm run build",
|
||||
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
|
||||
"lint": "eslint --max-warnings 115 src spec",
|
||||
"lint": "eslint --max-warnings 113 src spec",
|
||||
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt"
|
||||
},
|
||||
"repository": {
|
||||
@@ -62,7 +62,7 @@
|
||||
"eslint-config-google": "^0.7.1",
|
||||
"exorcist": "^0.4.0",
|
||||
"expect": "^1.20.2",
|
||||
"istanbul": "^0.3.13",
|
||||
"istanbul": "^0.4.5",
|
||||
"jsdoc": "^3.4.0",
|
||||
"lolex": "^1.5.2",
|
||||
"mocha": "^3.2.0",
|
||||
@@ -70,7 +70,7 @@
|
||||
"rimraf": "^2.5.4",
|
||||
"source-map-support": "^0.4.11",
|
||||
"sourceify": "^0.1.0",
|
||||
"uglifyjs": "^2.4.10",
|
||||
"uglify-js": "^2.8.26",
|
||||
"watchify": "^3.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
+48
-13
@@ -178,27 +178,61 @@ if [ $dodist -eq 0 ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# push the release branch (github can't release from
|
||||
# a branch it doesn't have)
|
||||
git push origin "$rel_branch"
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signed tag
|
||||
# gnupg seems to fail to get the right tty device unless we set it here
|
||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||
|
||||
# also make a signature for the source tarball.
|
||||
project_name=`jq -r '.name' package.json`
|
||||
source_sigfile="${tag}-src.tar.gz.asc"
|
||||
git archive --format tgz --prefix="${project_name}-${release}/" "$tag" |
|
||||
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig -
|
||||
assets="$assets -a $source_sigfile"
|
||||
else
|
||||
git tag -a -F "${latest_changes}" "$tag"
|
||||
fi
|
||||
|
||||
# push the tag
|
||||
git push origin "$tag"
|
||||
# push the tag and the release branch
|
||||
git push origin "$rel_branch" "$tag"
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signature for the source tarball.
|
||||
#
|
||||
# github will make us a tarball from the tag - we want to create a
|
||||
# signature for it, which means that first of all we need to check that
|
||||
# it's correct.
|
||||
#
|
||||
# we can't deterministically build exactly the same tarball, due to
|
||||
# differences in gzip implementation - but we *can* build the same tar - so
|
||||
# the easiest way to check the validity of the tarball from git is to unzip
|
||||
# it and compare it with our own idea of what the tar should look like.
|
||||
|
||||
# the name of the sig file we want to create
|
||||
source_sigfile="${tag}-src.tar.gz.asc"
|
||||
|
||||
tarfile="$tag.tar.gz"
|
||||
gh_project_url=$(git remote get-url origin |
|
||||
sed -e 's#^git@github.com:#https://github.com/#' -e 's/\.git$//')
|
||||
project_name="${gh_project_url##*/}"
|
||||
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
|
||||
|
||||
# unzip it and compare it with the tar we would generate
|
||||
if ! cmp --silent <(gunzip -c $tarfile) \
|
||||
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
|
||||
|
||||
# we don't bail out here, because really it's more likely that our comparison
|
||||
# screwed up and it's super annoying to abort the script at this point.
|
||||
cat >&2 <<EOF
|
||||
!!!!!!!!!!!!!!!!!
|
||||
!!!! WARNING !!!!
|
||||
|
||||
Mismatch between our own tarfile and that generated by github: not signing
|
||||
source tarball.
|
||||
|
||||
To resolve, determine if $tarfile is correct, and if so sign it with gpg and
|
||||
attach it to the release as $source_sigfile.
|
||||
|
||||
!!!!!!!!!!!!!!!!!
|
||||
EOF
|
||||
else
|
||||
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig "$tarfile"
|
||||
assets="$assets -a $source_sigfile"
|
||||
fi
|
||||
fi
|
||||
|
||||
hubflags=''
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
@@ -237,6 +271,7 @@ fi
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
git checkout "$rel_branch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ TestClient.prototype.toString = function() {
|
||||
* @return {Promise}
|
||||
*/
|
||||
TestClient.prototype.start = function() {
|
||||
console.log(this + ': starting');
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
|
||||
@@ -403,29 +403,6 @@ describe("MatrixClient crypto", function() {
|
||||
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
|
||||
it('Ali knows the difference between a new user and one with no devices',
|
||||
function(done) {
|
||||
aliTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
'@bob:id': {},
|
||||
},
|
||||
});
|
||||
|
||||
const p1 = aliTestClient.client.downloadKeys(['@bob:id']);
|
||||
const p2 = aliTestClient.httpBackend.flush('/keys/query', 1);
|
||||
|
||||
q.all([p1, p2]).then(function() {
|
||||
const devices = aliTestClient.storage.getEndToEndDevicesForUser(
|
||||
'@bob:id',
|
||||
);
|
||||
expect(utils.keys(devices).length).toEqual(0);
|
||||
|
||||
// request again: should be no more requests
|
||||
return aliTestClient.client.downloadKeys(['@bob:id']);
|
||||
}).nodeify(done);
|
||||
},
|
||||
);
|
||||
|
||||
it("Bob uploads device keys", function() {
|
||||
return q()
|
||||
.then(bobUploadsDeviceKeys);
|
||||
@@ -673,6 +650,27 @@ describe("MatrixClient crypto", function() {
|
||||
return q()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
|
||||
// ali will only care about bob's new_device if she is tracking
|
||||
// bob's devices, which she will do if we enable encryption
|
||||
.then(aliEnablesEncryption)
|
||||
|
||||
.then(() => {
|
||||
aliTestClient.expectKeyQuery({
|
||||
device_keys: {
|
||||
[aliUserId]: {},
|
||||
[bobUserId]: {},
|
||||
},
|
||||
});
|
||||
return aliTestClient.httpBackend.flush('/keys/query', 1);
|
||||
})
|
||||
|
||||
// make sure that the initial key download has completed
|
||||
// (downloadKeys will wait until it does)
|
||||
.then(() => {
|
||||
return aliTestClient.client.downloadKeys([bobUserId]);
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
const syncData = {
|
||||
next_batch: '2',
|
||||
|
||||
+29
-48
@@ -1001,7 +1001,19 @@ describe("megolm", function() {
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);
|
||||
const bobStat = aliceTestClient.storage
|
||||
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
|
||||
const chrisStat = aliceTestClient.storage
|
||||
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
@@ -1020,7 +1032,15 @@ describe("megolm", function() {
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(2);
|
||||
const bobStat = aliceTestClient.storage
|
||||
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = aliceTestClient.storage
|
||||
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
@@ -1030,56 +1050,17 @@ describe("megolm", function() {
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
const bobStat = aliceTestClient.storage
|
||||
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
|
||||
const chrisStat = aliceTestClient.storage
|
||||
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("Device list downloads before /changes shouldn't affect sync token",
|
||||
() => {
|
||||
// https://github.com/vector-im/riot-web/issues/3126#issuecomment-279374939
|
||||
aliceTestClient.storage.storeEndToEndDeviceSyncToken(0);
|
||||
aliceTestClient.storage.storeEndToEndRoom(ROOM_ID, {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
});
|
||||
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse([aliceTestClient.userId, '@bob:xyz']));
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(() => {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {device_keys: {'@bob:xyz': {}}},
|
||||
);
|
||||
return q.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(() => {
|
||||
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(0);
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'GET', '/keys/changes',
|
||||
).check((req) => {
|
||||
expect(req.queryParams.from).toEqual(0);
|
||||
expect(req.queryParams.to).toEqual(1);
|
||||
}).respond(200, {changed: ['@bob:xyz']});
|
||||
|
||||
return aliceTestClient.httpBackend.flush('/keys/changes');
|
||||
}).then((flushed) => {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {device_keys: {'@bob:xyz': {}}},
|
||||
);
|
||||
return aliceTestClient.httpBackend.flush('/keys/query');
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// let the client finish processing the keys
|
||||
return q.delay(10);
|
||||
}).then(() => {
|
||||
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("Alice exports megolm keys and imports them to a new device", function(done) {
|
||||
let messageEncrypted;
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import DeviceList from '../../../lib/crypto/DeviceList';
|
||||
import MockStorageApi from '../../MockStorageApi';
|
||||
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
|
||||
import testUtils from '../../test-utils';
|
||||
import utils from '../../../lib/utils';
|
||||
|
||||
import expect from 'expect';
|
||||
import q from 'q';
|
||||
|
||||
const signedDeviceList = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@test1:sw1v.org": {
|
||||
"HGKAWHRVJQ": {
|
||||
"signatures": {
|
||||
"@test1:sw1v.org": {
|
||||
"ed25519:HGKAWHRVJQ":
|
||||
"8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" +
|
||||
"XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw",
|
||||
},
|
||||
},
|
||||
"user_id": "@test1:sw1v.org",
|
||||
"keys": {
|
||||
"ed25519:HGKAWHRVJQ":
|
||||
"0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
|
||||
"curve25519:HGKAWHRVJQ":
|
||||
"mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
|
||||
},
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "HGKAWHRVJQ",
|
||||
"unsigned": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let sessionStore;
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
|
||||
downloadSpy = expect.createSpy();
|
||||
const mockStorage = new MockStorageApi();
|
||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
});
|
||||
|
||||
function createTestDeviceList() {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
};
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
};
|
||||
return new DeviceList(baseApis, sessionStore, mockOlm);
|
||||
}
|
||||
|
||||
it("should successfully download and store device keys", function() {
|
||||
const dl = createTestDeviceList();
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = q.defer();
|
||||
downloadSpy.andReturn(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
return prom1.then(() => {
|
||||
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
});
|
||||
});
|
||||
|
||||
it("should have an outdated devicelist on an invalidation while an " +
|
||||
"update is in progress", function() {
|
||||
const dl = createTestDeviceList();
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = q.defer();
|
||||
downloadSpy.andReturn(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
downloadSpy.reset();
|
||||
|
||||
// outdated notif arrives while the request is in flight.
|
||||
const queryDefer2 = q.defer();
|
||||
downloadSpy.andReturn(queryDefer2.promise);
|
||||
|
||||
dl.invalidateUserDeviceList('@test1:sw1v.org');
|
||||
dl.refreshOutdatedDeviceLists();
|
||||
|
||||
// the first request completes
|
||||
queryDefer1.resolve({
|
||||
device_keys: {
|
||||
'@test1:sw1v.org': {},
|
||||
},
|
||||
});
|
||||
|
||||
return prom1.then(() => {
|
||||
// uh-oh; user restarts before second request completes. The new instance
|
||||
// should know we never got a complete device list.
|
||||
console.log("Creating new devicelist to simulate app reload");
|
||||
downloadSpy.reset();
|
||||
const dl2 = createTestDeviceList();
|
||||
const queryDefer3 = q.defer();
|
||||
downloadSpy.andReturn(queryDefer3.promise);
|
||||
|
||||
const prom3 = dl2.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
|
||||
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
// allow promise chain to complete
|
||||
return prom3;
|
||||
}).then(() => {
|
||||
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -120,6 +120,20 @@ MatrixBaseApis.prototype.makeTxnId = function() {
|
||||
// Registration/Login operations
|
||||
// =============================
|
||||
|
||||
/**
|
||||
* Check whether a username is available prior to registration. An error response
|
||||
* indicates an invalid/unavailable username.
|
||||
* @param {string} username The username to check the availability of.
|
||||
* @return {module:client.Promise} Resolves: to `true`.
|
||||
*/
|
||||
MatrixBaseApis.prototype.isUsernameAvailable = function(username) {
|
||||
return this._http.authedRequest(
|
||||
undefined, "GET", '/register/available', { username: username },
|
||||
).then((response) => {
|
||||
return response.available;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
@@ -470,6 +484,34 @@ MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a marker to indicate the point in a room before which the user has read every
|
||||
* event. This can be retrieved from room account data (the event type is `m.fully_read`)
|
||||
* and displayed as a horizontal line in the timeline that is visually distinct to the
|
||||
* position of the user's own read receipt.
|
||||
* @param {string} roomId ID of the room that has been read
|
||||
* @param {string} rmEventId ID of the event that has been read
|
||||
* @param {string} rrEventId ID of the event tracked by the read receipt. This is here
|
||||
* for convenience because the RR and the RM are commonly updated at the same time as
|
||||
* each other. Optional.
|
||||
* @return {module:client.Promise} Resolves: the empty object, {}.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
|
||||
function(roomId, rmEventId, rrEventId) {
|
||||
const path = utils.encodeUri("/rooms/$roomId/read_markers", {
|
||||
$roomId: roomId,
|
||||
});
|
||||
|
||||
const content = {
|
||||
"m.fully_read": rmEventId,
|
||||
"m.read": rrEventId,
|
||||
};
|
||||
|
||||
return this._http.authedRequest(
|
||||
undefined, "POST", path, undefined, content,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Room Directory operations
|
||||
// =========================
|
||||
|
||||
+80
-1
@@ -104,6 +104,9 @@ try {
|
||||
* disabled by default for compatibility with older clients - in particular to
|
||||
* maintain support for back-paginating the live timeline after a '/sync'
|
||||
* result with a gap.
|
||||
*
|
||||
* @param {module:crypto.store.base~CryptoStore} opts.cryptoStore
|
||||
* crypto store implementation.
|
||||
*/
|
||||
function MatrixClient(opts) {
|
||||
MatrixBaseApis.call(this, opts);
|
||||
@@ -153,13 +156,16 @@ function MatrixClient(opts) {
|
||||
this._notifTimelineSet = null;
|
||||
|
||||
this._crypto = null;
|
||||
this._cryptoStore = opts.cryptoStore;
|
||||
if (CRYPTO_ENABLED && Boolean(opts.sessionStore) &&
|
||||
Boolean(this._cryptoStore) &&
|
||||
userId !== null && this.deviceId !== null) {
|
||||
this._crypto = new Crypto(
|
||||
this, this,
|
||||
opts.sessionStore,
|
||||
userId, this.deviceId,
|
||||
this.store,
|
||||
opts.cryptoStore,
|
||||
);
|
||||
|
||||
this.olmVersion = Crypto.getOlmVersion();
|
||||
@@ -168,6 +174,37 @@ function MatrixClient(opts) {
|
||||
utils.inherits(MatrixClient, EventEmitter);
|
||||
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
|
||||
|
||||
/**
|
||||
* Clear any data out of the persistent stores used by the client.
|
||||
*
|
||||
* @returns {Promise} Promise which resolves when the stores have been cleared.
|
||||
*/
|
||||
MatrixClient.prototype.clearStores = function() {
|
||||
if (this._clientRunning) {
|
||||
throw new Error("Cannot clear stores while client is running");
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.store.deleteAllData());
|
||||
if (this._cryptoStore) {
|
||||
promises.push(this._cryptoStore.deleteAllData());
|
||||
}
|
||||
return q.all(promises);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the user-id of the logged-in user
|
||||
*
|
||||
* @return {?string} MXID for the logged-in user, or null if not logged in
|
||||
*/
|
||||
MatrixClient.prototype.getUserId = function() {
|
||||
if (this.credentials && this.credentials.userId) {
|
||||
return this.credentials.userId;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the domain for this client's MXID
|
||||
* @return {?string} Domain of this MXID
|
||||
@@ -359,6 +396,21 @@ MatrixClient.prototype.getStoredDevicesForUser = function(userId) {
|
||||
return this._crypto.getStoredDevicesForUser(userId) || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the stored device key for a user id and device id
|
||||
*
|
||||
* @param {string} userId the user to list keys for.
|
||||
* @param {string} deviceId unique identifier for the device
|
||||
*
|
||||
* @return {?module:crypto-deviceinfo} device or null
|
||||
*/
|
||||
MatrixClient.prototype.getStoredDevice = function(userId, deviceId) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this._crypto.getStoredDevice(userId, deviceId) || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark the given device as verified
|
||||
*
|
||||
@@ -552,7 +604,7 @@ function _decryptEvent(client, event) {
|
||||
console.warn(
|
||||
`Error decrypting event (id=${event.getId()}): ${e}`,
|
||||
);
|
||||
if (!(e instanceof Crypto.DecryptionError)) {
|
||||
if (e.name !== "DecryptionError") {
|
||||
throw e;
|
||||
}
|
||||
_badEncryptedMessage(event, e.message);
|
||||
@@ -1201,6 +1253,33 @@ MatrixClient.prototype.sendReadReceipt = function(event, callback) {
|
||||
return this.sendReceipt(event, "m.read", callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a marker to indicate the point in a room before which the user has read every
|
||||
* event. This can be retrieved from room account data (the event type is `m.fully_read`)
|
||||
* and displayed as a horizontal line in the timeline that is visually distinct to the
|
||||
* position of the user's own read receipt.
|
||||
* @param {string} roomId ID of the room that has been read
|
||||
* @param {string} eventId ID of the event that has been read
|
||||
* @param {string} rrEvent the event tracked by the read receipt. This is here for
|
||||
* convenience because the RR and the RM are commonly updated at the same time as each
|
||||
* other. The local echo of this receipt will be done if set. Optional.
|
||||
* @return {module:client.Promise} Resolves: the empty object, {}.
|
||||
*/
|
||||
MatrixClient.prototype.setRoomReadMarkers = function(roomId, eventId, rrEvent) {
|
||||
const rmEventId = eventId;
|
||||
let rrEventId;
|
||||
|
||||
// Add the optional RR update, do local echo like `sendReceipt`
|
||||
if (rrEvent) {
|
||||
rrEventId = rrEvent.getId();
|
||||
const room = this.getRoom(roomId);
|
||||
if (room) {
|
||||
room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read");
|
||||
}
|
||||
}
|
||||
|
||||
return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a preview of the given URL as of (roughly) the given point in time,
|
||||
|
||||
+251
-150
@@ -26,35 +26,35 @@ import q from 'q';
|
||||
import DeviceInfo from './deviceinfo';
|
||||
import olmlib from './olmlib';
|
||||
|
||||
// constants for DeviceList._deviceTrackingStatus
|
||||
// const TRACKING_STATUS_NOT_TRACKED = 0;
|
||||
const TRACKING_STATUS_PENDING_DOWNLOAD = 1;
|
||||
const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
|
||||
const TRACKING_STATUS_UP_TO_DATE = 3;
|
||||
|
||||
/**
|
||||
* @alias module:crypto/DeviceList
|
||||
*/
|
||||
export default class DeviceList {
|
||||
constructor(baseApis, sessionStore, olmDevice) {
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
this._olmDevice = olmDevice;
|
||||
this._serialiser = new DeviceListUpdateSerialiser(
|
||||
baseApis, sessionStore, olmDevice,
|
||||
);
|
||||
|
||||
// users with outdated device lists
|
||||
// userId -> true
|
||||
this._pendingUsersWithNewDevices = {};
|
||||
// which users we are tracking device status for.
|
||||
// userId -> TRACKING_STATUS_*
|
||||
this._deviceTrackingStatus = sessionStore.getEndToEndDeviceTrackingStatus() || {};
|
||||
for (const u of Object.keys(this._deviceTrackingStatus)) {
|
||||
// if a download was in progress when we got shut down, it isn't any more.
|
||||
if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
|
||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||
}
|
||||
}
|
||||
|
||||
// userId -> true
|
||||
// userId -> promise
|
||||
this._keyDownloadsInProgressByUser = {};
|
||||
|
||||
// deferred which is resolved when the current device query resolves.
|
||||
// (null if there is no current request).
|
||||
this._currentQueryDeferred = null;
|
||||
|
||||
// deferred which is resolved when the *next* device query resolves.
|
||||
//
|
||||
// Normally it is meaningless for this to be non-null when
|
||||
// _currentQueryDeferred is null, but it can happen if the previous
|
||||
// query has finished but the next one has not yet started (because the
|
||||
// previous query failed, in which case we deliberately delay starting
|
||||
// the next query to avoid tight-looping).
|
||||
this._queuedQueryDeferred = null;
|
||||
|
||||
this.lastKnownSyncToken = null;
|
||||
}
|
||||
|
||||
@@ -68,43 +68,27 @@ export default class DeviceList {
|
||||
* module:crypto/deviceinfo|DeviceInfo}.
|
||||
*/
|
||||
downloadKeys(userIds, forceDownload) {
|
||||
let needsRefresh = false;
|
||||
let waitForCurrentQuery = false;
|
||||
const usersToDownload = [];
|
||||
const promises = [];
|
||||
|
||||
userIds.forEach((u) => {
|
||||
if (this._pendingUsersWithNewDevices[u]) {
|
||||
// we already know this user's devices are outdated
|
||||
needsRefresh = true;
|
||||
} else if (this._keyDownloadsInProgressByUser[u]) {
|
||||
// already a download in progress - just wait for it.
|
||||
// (even if forceDownload is true)
|
||||
waitForCurrentQuery = true;
|
||||
} else if (forceDownload) {
|
||||
console.log("Invalidating device list for " + u +
|
||||
" for forceDownload");
|
||||
this.invalidateUserDeviceList(u);
|
||||
needsRefresh = true;
|
||||
} else if (!this.getStoredDevicesForUser(u)) {
|
||||
console.log("Invalidating device list for " + u +
|
||||
" due to empty cache");
|
||||
this.invalidateUserDeviceList(u);
|
||||
needsRefresh = true;
|
||||
const trackingStatus = this._deviceTrackingStatus[u];
|
||||
if (this._keyDownloadsInProgressByUser[u]) {
|
||||
// already a key download in progress/queued for this user; its results
|
||||
// will be good enough for us.
|
||||
promises.push(this._keyDownloadsInProgressByUser[u]);
|
||||
} else if (forceDownload || trackingStatus != TRACKING_STATUS_UP_TO_DATE) {
|
||||
usersToDownload.push(u);
|
||||
}
|
||||
});
|
||||
|
||||
let promise;
|
||||
if (needsRefresh) {
|
||||
console.log("downloadKeys: waiting for next key query");
|
||||
promise = this._startOrQueueDeviceQuery();
|
||||
} else if(waitForCurrentQuery) {
|
||||
console.log("downloadKeys: waiting for in-flight query to complete");
|
||||
promise = this._currentQueryDeferred.promise;
|
||||
} else {
|
||||
// we're all up-to-date.
|
||||
promise = q();
|
||||
if (usersToDownload.length != 0) {
|
||||
console.log("downloadKeys: downloading for", usersToDownload);
|
||||
const downloadPromise = this._doKeyDownload(usersToDownload);
|
||||
promises.push(downloadPromise);
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
return q.all(promises).then(() => {
|
||||
return this._getDevicesFromStore(userIds);
|
||||
});
|
||||
}
|
||||
@@ -216,14 +200,15 @@ export default class DeviceList {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the cached device list for the given user outdated.
|
||||
* flag the given user for device-list tracking, if they are not already.
|
||||
*
|
||||
* This doesn't set off an update, so that several users can be batched
|
||||
* together. Call refreshOutdatedDeviceLists() for that.
|
||||
* This will mean that a subsequent call to refreshOutdatedDeviceLists()
|
||||
* will download the device list for the user, and that subsequent calls to
|
||||
* invalidateUserDeviceList will trigger more updates.
|
||||
*
|
||||
* @param {String} userId
|
||||
*/
|
||||
invalidateUserDeviceList(userId) {
|
||||
startTrackingDeviceList(userId) {
|
||||
// sanity-check the userId. This is mostly paranoia, but if synapse
|
||||
// can't parse the userId we give it as an mxid, it 500s the whole
|
||||
// request and we can never update the device lists again (because
|
||||
@@ -234,124 +219,229 @@ export default class DeviceList {
|
||||
if (typeof userId !== 'string') {
|
||||
throw new Error('userId must be a string; was '+userId);
|
||||
}
|
||||
this._pendingUsersWithNewDevices[userId] = true;
|
||||
if (!this._deviceTrackingStatus[userId]) {
|
||||
console.log('Now tracking device list for ' + userId);
|
||||
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||
}
|
||||
// we don't yet persist the tracking status, since there may be a lot
|
||||
// of calls; instead we wait for the forthcoming
|
||||
// refreshOutdatedDeviceLists.
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is not already a device list query in progress, and we have
|
||||
* users who have outdated device lists, start a query now.
|
||||
* Mark the cached device list for the given user outdated.
|
||||
*
|
||||
* If we are not tracking this user's devices, we'll do nothing. Otherwise
|
||||
* we flag the user as needing an update.
|
||||
*
|
||||
* This doesn't actually set off an update, so that several users can be
|
||||
* batched together. Call refreshOutdatedDeviceLists() for that.
|
||||
*
|
||||
* @param {String} userId
|
||||
*/
|
||||
invalidateUserDeviceList(userId) {
|
||||
if (this._deviceTrackingStatus[userId]) {
|
||||
console.log("Marking device list outdated for", userId);
|
||||
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||
}
|
||||
// we don't yet persist the tracking status, since there may be a lot
|
||||
// of calls; instead we wait for the forthcoming
|
||||
// refreshOutdatedDeviceLists.
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all tracked device lists as outdated.
|
||||
*
|
||||
* This will flag each user whose devices we are tracking as in need of an
|
||||
* update.
|
||||
*/
|
||||
invalidateAllDeviceLists() {
|
||||
for (const userId of Object.keys(this._deviceTrackingStatus)) {
|
||||
this.invalidateUserDeviceList(userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have users who have outdated device lists, start key downloads for them
|
||||
*
|
||||
* @returns {Promise} which completes when the download completes; normally there
|
||||
* is no need to wait for this (it's mostly for the unit tests).
|
||||
*/
|
||||
refreshOutdatedDeviceLists() {
|
||||
if (this._currentQueryDeferred) {
|
||||
// request already in progress - do nothing. (We will automatically
|
||||
// make another request if there are more users with outdated
|
||||
// device lists when the current request completes).
|
||||
const usersToDownload = [];
|
||||
for (const userId of Object.keys(this._deviceTrackingStatus)) {
|
||||
const stat = this._deviceTrackingStatus[userId];
|
||||
if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) {
|
||||
usersToDownload.push(userId);
|
||||
}
|
||||
}
|
||||
if (usersToDownload.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._startDeviceQuery();
|
||||
// we didn't persist the tracking status during
|
||||
// invalidateUserDeviceList, so do it now.
|
||||
this._persistDeviceTrackingStatus();
|
||||
|
||||
return this._doKeyDownload(usersToDownload);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fire off download update requests for the given users, and update the
|
||||
* device list tracking status for them, and the
|
||||
* _keyDownloadsInProgressByUser map for them.
|
||||
*
|
||||
* @param {String[]} users list of userIds
|
||||
*
|
||||
* @return {module:client.Promise} resolves when all the users listed have
|
||||
* been updated. rejects if there was a problem updating any of the
|
||||
* users.
|
||||
*/
|
||||
_doKeyDownload(users) {
|
||||
if (users.length === 0) {
|
||||
// nothing to do
|
||||
return q();
|
||||
}
|
||||
|
||||
const prom = this._serialiser.updateDevicesForUsers(
|
||||
users, this.lastKnownSyncToken,
|
||||
).then(() => {
|
||||
finished(true);
|
||||
}, (e) => {
|
||||
console.error(
|
||||
'Error downloading keys for ' + users + ":", e,
|
||||
);
|
||||
finished(false);
|
||||
throw e;
|
||||
});
|
||||
|
||||
users.forEach((u) => {
|
||||
this._keyDownloadsInProgressByUser[u] = prom;
|
||||
const stat = this._deviceTrackingStatus[u];
|
||||
if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) {
|
||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_DOWNLOAD_IN_PROGRESS;
|
||||
}
|
||||
});
|
||||
|
||||
const finished = (success) => {
|
||||
users.forEach((u) => {
|
||||
// we may have queued up another download request for this user
|
||||
// since we started this request. If that happens, we should
|
||||
// ignore the completion of the first one.
|
||||
if (this._keyDownloadsInProgressByUser[u] !== prom) {
|
||||
console.log('Another update in the queue for', u,
|
||||
'- not marking up-to-date');
|
||||
return;
|
||||
}
|
||||
delete this._keyDownloadsInProgressByUser[u];
|
||||
const stat = this._deviceTrackingStatus[u];
|
||||
if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
|
||||
if (success) {
|
||||
// we didn't get any new invalidations since this download started:
|
||||
// this user's device list is now up to date.
|
||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE;
|
||||
console.log("Device list for", u, "now up to date");
|
||||
} else {
|
||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||
}
|
||||
}
|
||||
});
|
||||
this._persistDeviceTrackingStatus();
|
||||
};
|
||||
|
||||
return prom;
|
||||
}
|
||||
|
||||
_persistDeviceTrackingStatus() {
|
||||
this._sessionStore.storeEndToEndDeviceTrackingStatus(this._deviceTrackingStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialises updates to device lists
|
||||
*
|
||||
* Ensures that results from /keys/query are not overwritten if a second call
|
||||
* completes *before* an earlier one.
|
||||
*
|
||||
* It currently does this by ensuring only one call to /keys/query happens at a
|
||||
* time (and queuing other requests up).
|
||||
*/
|
||||
class DeviceListUpdateSerialiser {
|
||||
constructor(baseApis, sessionStore, olmDevice) {
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
this._olmDevice = olmDevice;
|
||||
|
||||
this._downloadInProgress = false;
|
||||
|
||||
// users which are queued for download
|
||||
// userId -> true
|
||||
this._keyDownloadsQueuedByUser = {};
|
||||
|
||||
// deferred which is resolved when the queued users are downloaded.
|
||||
//
|
||||
// non-null indicates that we have users queued for download.
|
||||
this._queuedQueryDeferred = null;
|
||||
|
||||
// sync token to be used for the next query: essentially the
|
||||
// most recent one we know about
|
||||
this._nextSyncToken = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is currently a device list query in progress, returns a promise
|
||||
* which will resolve when the *next* query completes. Otherwise, starts
|
||||
* a new query, and returns a promise which resolves when it completes.
|
||||
* Make a key query request for the given users
|
||||
*
|
||||
* @return {Promise}
|
||||
* @param {String[]} users list of user ids
|
||||
*
|
||||
* @param {String} syncToken sync token to pass in the query request, to
|
||||
* help the HS give the most recent results
|
||||
*
|
||||
* @return {module:client.Promise} resolves when all the users listed have
|
||||
* been updated. rejects if there was a problem updating any of the
|
||||
* users.
|
||||
*/
|
||||
_startOrQueueDeviceQuery() {
|
||||
if (!this._currentQueryDeferred) {
|
||||
this._startDeviceQuery();
|
||||
if (!this._currentQueryDeferred) {
|
||||
return q();
|
||||
}
|
||||
|
||||
return this._currentQueryDeferred.promise;
|
||||
}
|
||||
updateDevicesForUsers(users, syncToken) {
|
||||
users.forEach((u) => {
|
||||
this._keyDownloadsQueuedByUser[u] = true;
|
||||
});
|
||||
this._nextSyncToken = syncToken;
|
||||
|
||||
if (!this._queuedQueryDeferred) {
|
||||
this._queuedQueryDeferred = q.defer();
|
||||
}
|
||||
|
||||
return this._queuedQueryDeferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* kick off a new device query
|
||||
*
|
||||
* Throws if there is already a query in progress.
|
||||
*/
|
||||
_startDeviceQuery() {
|
||||
if (this._currentQueryDeferred) {
|
||||
throw new Error("DeviceList._startDeviceQuery called with request active");
|
||||
if (this._downloadInProgress) {
|
||||
// just queue up these users
|
||||
console.log('Queued key download for', users);
|
||||
return this._queuedQueryDeferred.promise;
|
||||
}
|
||||
|
||||
this._currentQueryDeferred = this._queuedQueryDeferred || q.defer();
|
||||
// start a new download.
|
||||
return this._doQueuedQueries();
|
||||
}
|
||||
|
||||
_doQueuedQueries() {
|
||||
if (this._downloadInProgress) {
|
||||
throw new Error(
|
||||
"DeviceListUpdateSerialiser._doQueuedQueries called with request active",
|
||||
);
|
||||
}
|
||||
|
||||
const downloadUsers = Object.keys(this._keyDownloadsQueuedByUser);
|
||||
this._keyDownloadsQueuedByUser = {};
|
||||
const deferred = this._queuedQueryDeferred;
|
||||
this._queuedQueryDeferred = null;
|
||||
|
||||
const users = Object.keys(this._pendingUsersWithNewDevices);
|
||||
if (users.length === 0) {
|
||||
// nothing to do
|
||||
this._currentQueryDeferred.resolve();
|
||||
this._currentQueryDeferred = null;
|
||||
console.log('Starting key download for', downloadUsers);
|
||||
this._downloadInProgress = true;
|
||||
|
||||
// that means we're up-to-date with the lastKnownSyncToken.
|
||||
const token = this.lastKnownSyncToken;
|
||||
if (token !== null) {
|
||||
this._sessionStore.storeEndToEndDeviceSyncToken(token);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._doKeyDownloadForUsers(users).done(() => {
|
||||
users.forEach((u) => {
|
||||
delete this._keyDownloadsInProgressByUser[u];
|
||||
});
|
||||
|
||||
this._currentQueryDeferred.resolve();
|
||||
this._currentQueryDeferred = null;
|
||||
|
||||
// flush out any more requests that were blocked up while that
|
||||
// was going on.
|
||||
this._startDeviceQuery();
|
||||
}, (e) => {
|
||||
console.error(
|
||||
'Error updating device key cache for ' + users + ":", e,
|
||||
);
|
||||
|
||||
// reinstate the pending flags on any users which failed; this will
|
||||
// mean that we will do another download in the future (actually on
|
||||
// the next /sync).
|
||||
users.forEach((u) => {
|
||||
delete this._keyDownloadsInProgressByUser[u];
|
||||
this._pendingUsersWithNewDevices[u] = true;
|
||||
});
|
||||
|
||||
this._currentQueryDeferred.reject(e);
|
||||
this._currentQueryDeferred = null;
|
||||
});
|
||||
|
||||
users.forEach((u) => {
|
||||
delete this._pendingUsersWithNewDevices[u];
|
||||
this._keyDownloadsInProgressByUser[u] = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} downloadUsers list of userIds
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
_doKeyDownloadForUsers(downloadUsers) {
|
||||
console.log('Starting key download for ' + downloadUsers);
|
||||
|
||||
const token = this.lastKnownSyncToken;
|
||||
const opts = {};
|
||||
if (token) {
|
||||
opts.token = token;
|
||||
if (this._nextSyncToken) {
|
||||
opts.token = this._nextSyncToken;
|
||||
}
|
||||
return this._baseApis.downloadKeysForUsers(
|
||||
|
||||
this._baseApis.downloadKeysForUsers(
|
||||
downloadUsers, opts,
|
||||
).then((res) => {
|
||||
const dk = res.device_keys || {};
|
||||
@@ -369,12 +459,23 @@ export default class DeviceList {
|
||||
}
|
||||
|
||||
return prom;
|
||||
}).then(() => {
|
||||
if (token !== null) {
|
||||
this._sessionStore.storeEndToEndDeviceSyncToken(token);
|
||||
}
|
||||
}).done(() => {
|
||||
console.log('Completed key download for ' + downloadUsers);
|
||||
|
||||
this._downloadInProgress = false;
|
||||
deferred.resolve();
|
||||
|
||||
// if we have queued users, fire off another request.
|
||||
if (this._queuedQueryDeferred) {
|
||||
this._doQueuedQueries();
|
||||
}
|
||||
}, (e) => {
|
||||
console.warn('Error downloading keys for ' + downloadUsers + ':', e);
|
||||
this._downloadInProgressInProgress = false;
|
||||
deferred.reject(e);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
_processQueryResponseForUser(userId, response) {
|
||||
|
||||
+112
-114
@@ -13,14 +13,12 @@ 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.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Internal module. Defines the base classes of the encryption implementations
|
||||
*
|
||||
* @module crypto/algorithms/base
|
||||
* @module
|
||||
*/
|
||||
const utils = require("../../utils");
|
||||
|
||||
/**
|
||||
* map of registered encryption algorithm classes. A map from string to {@link
|
||||
@@ -28,7 +26,7 @@ const utils = require("../../utils");
|
||||
*
|
||||
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
|
||||
*/
|
||||
module.exports.ENCRYPTION_CLASSES = {};
|
||||
export const ENCRYPTION_CLASSES = {};
|
||||
|
||||
/**
|
||||
* map of registered encryption algorithm classes. Map from string to {@link
|
||||
@@ -36,12 +34,11 @@ module.exports.ENCRYPTION_CLASSES = {};
|
||||
*
|
||||
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
|
||||
*/
|
||||
module.exports.DECRYPTION_CLASSES = {};
|
||||
export const DECRYPTION_CLASSES = {};
|
||||
|
||||
/**
|
||||
* base type for encryption implementations
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:crypto/algorithms/base.EncryptionAlgorithm
|
||||
*
|
||||
* @param {object} params parameters
|
||||
@@ -53,45 +50,45 @@ module.exports.DECRYPTION_CLASSES = {};
|
||||
* @param {string} params.roomId The ID of the room we will be sending to
|
||||
* @param {object} params.config The body of the m.room.encryption event
|
||||
*/
|
||||
const EncryptionAlgorithm = function(params) {
|
||||
this._userId = params.userId;
|
||||
this._deviceId = params.deviceId;
|
||||
this._crypto = params.crypto;
|
||||
this._olmDevice = params.olmDevice;
|
||||
this._baseApis = params.baseApis;
|
||||
this._roomId = params.roomId;
|
||||
};
|
||||
/** */
|
||||
module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
|
||||
class EncryptionAlgorithm {
|
||||
constructor(params) {
|
||||
this._userId = params.userId;
|
||||
this._deviceId = params.deviceId;
|
||||
this._crypto = params.crypto;
|
||||
this._olmDevice = params.olmDevice;
|
||||
this._baseApis = params.baseApis;
|
||||
this._roomId = params.roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a message event
|
||||
*
|
||||
* @method module:crypto/algorithms/base.EncryptionAlgorithm#encryptMessage
|
||||
* @abstract
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {string} eventType
|
||||
* @param {object} plaintext event content
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
/**
|
||||
* Encrypt a message event
|
||||
*
|
||||
* @method module:crypto/algorithms/base.EncryptionAlgorithm.encryptMessage
|
||||
* @abstract
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {string} eventType
|
||||
* @param {object} plaintext event content
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
|
||||
/**
|
||||
* Called when the membership of a member of the room changes.
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event event causing the change
|
||||
* @param {module:models/room-member} member user whose membership changed
|
||||
* @param {string=} oldMembership previous membership
|
||||
*/
|
||||
EncryptionAlgorithm.prototype.onRoomMembership = function(
|
||||
event, member, oldMembership,
|
||||
) {};
|
||||
/**
|
||||
* Called when the membership of a member of the room changes.
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event event causing the change
|
||||
* @param {module:models/room-member} member user whose membership changed
|
||||
* @param {string=} oldMembership previous membership
|
||||
* @public
|
||||
*/
|
||||
onRoomMembership(event, member, oldMembership) {
|
||||
}
|
||||
}
|
||||
export {EncryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
|
||||
|
||||
/**
|
||||
* base type for decryption implementations
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:crypto/algorithms/base.DecryptionAlgorithm
|
||||
*
|
||||
* @param {object} params parameters
|
||||
@@ -101,55 +98,55 @@ EncryptionAlgorithm.prototype.onRoomMembership = function(
|
||||
* @param {string=} params.roomId The ID of the room we will be receiving
|
||||
* from. Null for to-device events.
|
||||
*/
|
||||
const DecryptionAlgorithm = function(params) {
|
||||
this._userId = params.userId;
|
||||
this._crypto = params.crypto;
|
||||
this._olmDevice = params.olmDevice;
|
||||
this._roomId = params.roomId;
|
||||
};
|
||||
/** */
|
||||
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
|
||||
class DecryptionAlgorithm {
|
||||
constructor(params) {
|
||||
this._userId = params.userId;
|
||||
this._crypto = params.crypto;
|
||||
this._olmDevice = params.olmDevice;
|
||||
this._roomId = params.roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an event
|
||||
*
|
||||
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
|
||||
* @abstract
|
||||
*
|
||||
* @param {object} event raw event
|
||||
*
|
||||
* @return {null} if the event referred to an unknown megolm session
|
||||
* @return {module:crypto.DecryptionResult} decryption result
|
||||
*
|
||||
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
||||
* problem decrypting the event
|
||||
*/
|
||||
/**
|
||||
* Decrypt an event
|
||||
*
|
||||
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
|
||||
* @abstract
|
||||
*
|
||||
* @param {object} event raw event
|
||||
*
|
||||
* @return {null} if the event referred to an unknown megolm session
|
||||
* @return {module:crypto.DecryptionResult} decryption result
|
||||
*
|
||||
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
||||
* problem decrypting the event
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handle a key event
|
||||
*
|
||||
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} params event key event
|
||||
*/
|
||||
DecryptionAlgorithm.prototype.onRoomKeyEvent = function(params) {
|
||||
// ignore by default
|
||||
};
|
||||
/**
|
||||
* Handle a key event
|
||||
*
|
||||
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} params event key event
|
||||
*/
|
||||
onRoomKeyEvent(params) {
|
||||
// ignore by default
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a room key
|
||||
*
|
||||
* @param {module:crypto/OlmDevice.MegolmSessionData} session
|
||||
*/
|
||||
DecryptionAlgorithm.prototype.importRoomKey = function(session) {
|
||||
// ignore by default
|
||||
};
|
||||
/**
|
||||
* Import a room key
|
||||
*
|
||||
* @param {module:crypto/OlmDevice.MegolmSessionData} session
|
||||
*/
|
||||
importRoomKey(session) {
|
||||
// ignore by default
|
||||
}
|
||||
}
|
||||
export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
|
||||
|
||||
/**
|
||||
* Exception thrown when decryption fails
|
||||
*
|
||||
* @alias module:crypto/algorithms/base.DecryptionError
|
||||
* @constructor
|
||||
* @param {string} msg user-visible message describing the problem
|
||||
*
|
||||
* @param {Object=} details key/value pairs reported in the logs but not shown
|
||||
@@ -157,50 +154,51 @@ DecryptionAlgorithm.prototype.importRoomKey = function(session) {
|
||||
*
|
||||
* @extends Error
|
||||
*/
|
||||
const DecryptionError = function(msg, details) {
|
||||
this.name = 'DecryptionError';
|
||||
this.message = msg;
|
||||
this.details = details;
|
||||
};
|
||||
utils.inherits(DecryptionError, Error);
|
||||
|
||||
/** override the string used when logging
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
DecryptionError.prototype.toString = function() {
|
||||
let result = this.name + '[msg: ' + this.message;
|
||||
|
||||
if (this.details) {
|
||||
result += ', ' +
|
||||
Object.keys(this.details).map(
|
||||
(k) => k + ': ' + this.details[k],
|
||||
).join(', ');
|
||||
class DecryptionError extends Error {
|
||||
constructor(msg, details) {
|
||||
super(msg);
|
||||
this.name = 'DecryptionError';
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
result += ']';
|
||||
/**
|
||||
* override the string used when logging
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
toString() {
|
||||
let result = this.name + '[msg: ' + this.message;
|
||||
|
||||
return result;
|
||||
};
|
||||
if (this.details) {
|
||||
result += ', ' +
|
||||
Object.keys(this.details).map(
|
||||
(k) => k + ': ' + this.details[k],
|
||||
).join(', ');
|
||||
}
|
||||
|
||||
module.exports.DecryptionError = DecryptionError;
|
||||
result += ']';
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
export {DecryptionError}; // https://github.com/jsdoc3/jsdoc/issues/1272
|
||||
|
||||
/**
|
||||
* Exception thrown specifically when we want to warn the user to consider
|
||||
* the security of their conversation before continuing
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} msg message describing the problem
|
||||
* @param {Object} devices userId -> {deviceId -> object}
|
||||
* set of unknown devices per user we're warning about
|
||||
* @extends Error
|
||||
*/
|
||||
module.exports.UnknownDeviceError = function(msg, devices) {
|
||||
this.name = "UnknownDeviceError";
|
||||
this.message = msg;
|
||||
this.devices = devices;
|
||||
};
|
||||
utils.inherits(module.exports.UnknownDeviceError, Error);
|
||||
export class UnknownDeviceError extends Error {
|
||||
constructor(msg, devices) {
|
||||
super(msg);
|
||||
this.name = "UnknownDeviceError";
|
||||
this.devices = devices;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an encryption/decryption class for a particular algorithm
|
||||
@@ -215,7 +213,7 @@ utils.inherits(module.exports.UnknownDeviceError, Error);
|
||||
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
|
||||
* implementation
|
||||
*/
|
||||
module.exports.registerAlgorithm = function(algorithm, encryptor, decryptor) {
|
||||
module.exports.ENCRYPTION_CLASSES[algorithm] = encryptor;
|
||||
module.exports.DECRYPTION_CLASSES[algorithm] = decryptor;
|
||||
};
|
||||
export function registerAlgorithm(algorithm, encryptor, decryptor) {
|
||||
ENCRYPTION_CLASSES[algorithm] = encryptor;
|
||||
DECRYPTION_CLASSES[algorithm] = decryptor;
|
||||
}
|
||||
|
||||
+52
-72
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -15,7 +16,6 @@ limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
|
||||
/**
|
||||
* @module crypto
|
||||
*/
|
||||
@@ -50,18 +50,22 @@ const DeviceList = require('./DeviceList').default;
|
||||
* @param {string} deviceId The identifier for this device.
|
||||
*
|
||||
* @param {Object} clientStore the MatrixClient data store.
|
||||
*
|
||||
* @param {module:crypto/store/base~CryptoStore} cryptoStore
|
||||
* storage for the crypto layer.
|
||||
*/
|
||||
function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId,
|
||||
clientStore) {
|
||||
clientStore, cryptoStore) {
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
this._userId = userId;
|
||||
this._deviceId = deviceId;
|
||||
this._clientStore = clientStore;
|
||||
this._cryptoStore = cryptoStore;
|
||||
|
||||
this._olmDevice = new OlmDevice(sessionStore);
|
||||
this._deviceList = new DeviceList(baseApis, sessionStore, this._olmDevice);
|
||||
this._initialDeviceListInvalidationDone = false;
|
||||
this._initialDeviceListInvalidationPending = false;
|
||||
|
||||
this._clientRunning = false;
|
||||
|
||||
@@ -558,9 +562,13 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
|
||||
* Configure a room to use encryption (ie, save a flag in the sessionstore).
|
||||
*
|
||||
* @param {string} roomId The room ID to enable encryption in.
|
||||
*
|
||||
* @param {object} config The encryption config for the room.
|
||||
*
|
||||
* @param {boolean=} inhibitDeviceQuery true to suppress device list query for
|
||||
* users in the room (for now)
|
||||
*/
|
||||
Crypto.prototype.setRoomEncryption = function(roomId, config) {
|
||||
Crypto.prototype.setRoomEncryption = function(roomId, config, inhibitDeviceQuery) {
|
||||
// if we already have encryption in this room, we should ignore this event
|
||||
// (for now at least. maybe we should alert the user somehow?)
|
||||
const existingConfig = this._sessionStore.getEndToEndRoom(roomId);
|
||||
@@ -590,21 +598,16 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
|
||||
});
|
||||
this._roomEncryptors[roomId] = alg;
|
||||
|
||||
// if encryption was not previously enabled in this room, we will have been
|
||||
// ignoring new device events for these users so far. We may well have
|
||||
// up-to-date lists for some users, for instance if we were sharing other
|
||||
// e2e rooms with them, so there is room for optimisation here, but for now
|
||||
// we just invalidate everyone in the room.
|
||||
if (!existingConfig) {
|
||||
console.log("Enabling encryption in " + roomId + " for the first time; " +
|
||||
"invalidating device lists for all users therein");
|
||||
const room = this._clientStore.getRoom(roomId);
|
||||
const members = room.getJoinedMembers();
|
||||
members.forEach((m) => {
|
||||
this._deviceList.invalidateUserDeviceList(m.userId);
|
||||
});
|
||||
// the actual refresh happens once we've finished processing the sync,
|
||||
// in _onSyncCompleted.
|
||||
// make sure we are tracking the device lists for all users in this room.
|
||||
console.log("Enabling encryption in " + roomId + "; " +
|
||||
"starting to track device lists for all users therein");
|
||||
const room = this._clientStore.getRoom(roomId);
|
||||
const members = room.getJoinedMembers();
|
||||
members.forEach((m) => {
|
||||
this._deviceList.startTrackingDeviceList(m.userId);
|
||||
});
|
||||
if (!inhibitDeviceQuery) {
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -795,7 +798,9 @@ Crypto.prototype._onCryptoEvent = function(event) {
|
||||
const content = event.getContent();
|
||||
|
||||
try {
|
||||
this.setRoomEncryption(roomId, content);
|
||||
// inhibit the device list refresh for now - it will happen once we've
|
||||
// finished processing the sync, in _onSyncCompleted.
|
||||
this.setRoomEncryption(roomId, content, true);
|
||||
} catch (e) {
|
||||
console.error("Error configuring encryption in room " + roomId +
|
||||
":", e);
|
||||
@@ -814,6 +819,8 @@ Crypto.prototype._onSyncCompleted = function(syncData) {
|
||||
const nextSyncToken = syncData.nextSyncToken;
|
||||
|
||||
if (!syncData.oldSyncToken) {
|
||||
console.log("Completed initial sync");
|
||||
|
||||
// an initialsync.
|
||||
this._sendNewDeviceEvents();
|
||||
|
||||
@@ -821,37 +828,40 @@ Crypto.prototype._onSyncCompleted = function(syncData) {
|
||||
// invalidate devices which have changed since then.
|
||||
const oldSyncToken = this._sessionStore.getEndToEndDeviceSyncToken();
|
||||
if (oldSyncToken !== null) {
|
||||
this._initialDeviceListInvalidationPending = true;
|
||||
this._invalidateDeviceListsSince(
|
||||
oldSyncToken, nextSyncToken,
|
||||
).catch((e) => {
|
||||
// if that failed, we fall back to invalidating everyone.
|
||||
console.warn("Error fetching changed device list", e);
|
||||
this._invalidateDeviceListForAllActiveUsers();
|
||||
this._deviceList.invalidateAllDeviceLists();
|
||||
}).done(() => {
|
||||
this._initialDeviceListInvalidationDone = true;
|
||||
this._initialDeviceListInvalidationPending = false;
|
||||
this._deviceList.lastKnownSyncToken = nextSyncToken;
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
});
|
||||
} else {
|
||||
// otherwise, we have to invalidate all devices for all users we
|
||||
// share a room with.
|
||||
// are tracking.
|
||||
console.log("Completed first initialsync; invalidating all " +
|
||||
"device list caches");
|
||||
this._invalidateDeviceListForAllActiveUsers();
|
||||
this._initialDeviceListInvalidationDone = true;
|
||||
this._deviceList.invalidateAllDeviceLists();
|
||||
}
|
||||
}
|
||||
|
||||
if (this._initialDeviceListInvalidationDone) {
|
||||
// if we've got an up-to-date list of users with outdated device lists,
|
||||
// tell the device list about the new sync token (but not otherwise, because
|
||||
// otherwise we'll start thinking we're more in sync than we are.)
|
||||
this._deviceList.lastKnownSyncToken = nextSyncToken;
|
||||
|
||||
// catch up on any new devices we got told about during the sync.
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
if (!this._initialDeviceListInvalidationPending) {
|
||||
// we can now store our sync token so that we can get an update on
|
||||
// restart rather than having to invalidate everyone.
|
||||
//
|
||||
// (we don't really need to do this on every sync - we could just
|
||||
// do it periodically)
|
||||
this._sessionStore.storeEndToEndDeviceSyncToken(nextSyncToken);
|
||||
}
|
||||
|
||||
// catch up on any new devices we got told about during the sync.
|
||||
this._deviceList.lastKnownSyncToken = nextSyncToken;
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
|
||||
// we don't start uploading one-time keys until we've caught up with
|
||||
// to-device messages, to help us avoid throwing away one-time-keys that we
|
||||
// are about to receive messages for
|
||||
@@ -928,49 +938,18 @@ Crypto.prototype._invalidateDeviceListsSince = function(
|
||||
return this._baseApis.getKeyChanges(
|
||||
oldSyncToken, lastKnownSyncToken,
|
||||
).then((r) => {
|
||||
console.log("got key changes since", oldSyncToken, ":", r.changed);
|
||||
|
||||
if (!r.changed || !Array.isArray(r.changed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only invalidate users we share an e2e room with - we don't
|
||||
// care about users in non-e2e rooms.
|
||||
const filteredUserIds = this._getE2eRoomMembers();
|
||||
r.changed.forEach((u) => {
|
||||
if (u in filteredUserIds) {
|
||||
this._deviceList.invalidateUserDeviceList(u);
|
||||
}
|
||||
this._deviceList.invalidateUserDeviceList(u);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Invalidate any stored device list for any users we share an e2e room with
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Crypto.prototype._invalidateDeviceListForAllActiveUsers = function() {
|
||||
Object.keys(this._getE2eRoomMembers()).forEach((m) => {
|
||||
this._deviceList.invalidateUserDeviceList(m);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* get the users we share an e2e-enabled room with
|
||||
*
|
||||
* @returns {Object<string>} userid->userid map (should be a Set but argh ES6)
|
||||
*/
|
||||
Crypto.prototype._getE2eRoomMembers = function() {
|
||||
const userIds = Object.create(null);
|
||||
|
||||
const rooms = this._getE2eRooms();
|
||||
for (const r of rooms) {
|
||||
const members = r.getJoinedMembers();
|
||||
members.forEach((m) => { userIds[m.userId] = m.userId; });
|
||||
}
|
||||
|
||||
return userIds;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of the e2e-enabled rooms we are members of
|
||||
*
|
||||
@@ -1039,6 +1018,12 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (member.membership == 'join') {
|
||||
console.log('Join event for ' + member.userId + ' in ' + roomId);
|
||||
// make sure we are tracking the deviceList for this user
|
||||
this._deviceList.startTrackingDeviceList(member.userId);
|
||||
}
|
||||
|
||||
alg.onRoomMembership(event, member, oldMembership);
|
||||
};
|
||||
|
||||
@@ -1140,11 +1125,6 @@ Crypto.prototype._signObject = function(obj) {
|
||||
obj.signatures = sigs;
|
||||
};
|
||||
|
||||
/**
|
||||
* @see module:crypto/algorithms/base.DecryptionError
|
||||
*/
|
||||
Crypto.DecryptionError = algorithms.DecryptionError;
|
||||
|
||||
|
||||
/** */
|
||||
module.exports = Crypto;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Internal module. Defintions for storage for the crypto module
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstraction of things that can store data required for end-to-end encryption
|
||||
*
|
||||
* @interface CryptoStore
|
||||
*/
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations 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.
|
||||
*/
|
||||
|
||||
import q from 'q';
|
||||
|
||||
/**
|
||||
* Internal module. indexeddb storage for e2e.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
const VERSION = 1;
|
||||
|
||||
/**
|
||||
* @implements {module:crypto/store/base~CryptoStore}
|
||||
*/
|
||||
export default class IndexedDBCryptoStore {
|
||||
/**
|
||||
* Create a new IndexedDBCryptoStore
|
||||
*
|
||||
* @param {IDBFactory} indexedDB global indexedDB instance
|
||||
* @param {string} dbName name of db to connect to
|
||||
*/
|
||||
constructor(indexedDB, dbName) {
|
||||
if (!indexedDB) {
|
||||
throw new Error("must pass indexedDB into IndexedDBCryptoStore");
|
||||
}
|
||||
this._indexedDB = indexedDB;
|
||||
this._dbName = dbName;
|
||||
this._dbPromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the database exists and is up-to-date
|
||||
*
|
||||
* @return {Promise} resolves to an instance of IDBDatabase when
|
||||
* the database is ready
|
||||
*/
|
||||
connect() {
|
||||
if (this._dbPromise) {
|
||||
return this._dbPromise;
|
||||
}
|
||||
|
||||
this._dbPromise = new q.Promise((resolve, reject) => {
|
||||
const req = this._indexedDB.open(this._dbName, VERSION);
|
||||
|
||||
req.onupgradeneeded = (ev) => {
|
||||
const db = ev.target.result;
|
||||
const oldVersion = ev.oldVersion;
|
||||
console.log(
|
||||
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
|
||||
+ ` to ${VERSION}`,
|
||||
);
|
||||
if (oldVersion < 1) { // The database did not previously exist.
|
||||
createDatabase(db);
|
||||
}
|
||||
// Expand as needed.
|
||||
};
|
||||
|
||||
req.onblocked = () => {
|
||||
reject(new Error(
|
||||
"unable to upgrade indexeddb because it is open elsewhere",
|
||||
));
|
||||
};
|
||||
|
||||
req.onerror = (ev) => {
|
||||
reject(new Error(
|
||||
"unable to connect to indexeddb: " + ev.target.error,
|
||||
));
|
||||
};
|
||||
|
||||
req.onsuccess = (r) => {
|
||||
resolve(r.target.result);
|
||||
};
|
||||
});
|
||||
return this._dbPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all data from this store.
|
||||
*
|
||||
* @returns {Promise} resolves when the store has been cleared.
|
||||
*/
|
||||
deleteAllData() {
|
||||
return new q.Promise((resolve, reject) => {
|
||||
console.log(`Removing indexeddb instance: ${this._dbName}`);
|
||||
const req = this._indexedDB.deleteDatabase(this._dbName);
|
||||
req.onerror = (ev) => {
|
||||
reject(new Error(
|
||||
"unable to delete indexeddb: " + ev.target.error,
|
||||
));
|
||||
};
|
||||
|
||||
req.onsuccess = () => {
|
||||
console.log(`Removed indexeddb instance: ${this._dbName}`);
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createDatabase(db) {
|
||||
const outgoingRoomKeyRequestsStore =
|
||||
db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });
|
||||
|
||||
// we assume that the RoomKeyRequestBody will have room_id and session_id
|
||||
// properties, to make the index efficient.
|
||||
outgoingRoomKeyRequestsStore.createIndex("session",
|
||||
["requestBody.room_id", "requestBody.session_id"],
|
||||
);
|
||||
|
||||
outgoingRoomKeyRequestsStore.createIndex("state", "state");
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations 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.
|
||||
*/
|
||||
|
||||
import q from 'q';
|
||||
|
||||
/**
|
||||
* Internal module. in-memory storage for e2e.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/**
|
||||
* @implements {module:crypto/store/base~CryptoStore}
|
||||
*/
|
||||
export default class MemoryCryptoStore {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all data from this store.
|
||||
*
|
||||
* @returns {Promise} Promise which resolves when the store has been cleared.
|
||||
*/
|
||||
deleteAllData() {
|
||||
return q();
|
||||
}
|
||||
}
|
||||
@@ -318,6 +318,13 @@ InteractiveAuth.prototype = {
|
||||
}
|
||||
this._currentStage = nextStage;
|
||||
|
||||
if (nextStage == 'm.login.dummy') {
|
||||
this.submitAuthDict({
|
||||
type: 'm.login.dummy',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._data.errcode || this._data.error) {
|
||||
this._stateUpdatedCallback(nextStage, {
|
||||
errcode: this._data.errcode || "",
|
||||
|
||||
@@ -63,6 +63,11 @@ module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
|
||||
module.exports.InteractiveAuth = require("./interactive-auth");
|
||||
|
||||
|
||||
module.exports.MemoryCryptoStore =
|
||||
require("./crypto/store/memory-crypto-store").default;
|
||||
module.exports.IndexedDBCryptoStore =
|
||||
require("./crypto/store/indexeddb-crypto-store").default;
|
||||
|
||||
/**
|
||||
* Create a new Matrix Call.
|
||||
* @function
|
||||
@@ -73,6 +78,23 @@ module.exports.InteractiveAuth = require("./interactive-auth");
|
||||
*/
|
||||
module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCall;
|
||||
|
||||
|
||||
/**
|
||||
* Set an audio input device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
module.exports.setMatrixCallAudioInput = require('./webrtc/call').setAudioInput;
|
||||
/**
|
||||
* Set a video input device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
module.exports.setMatrixCallVideoInput = require('./webrtc/call').setVideoInput;
|
||||
|
||||
|
||||
// expose the underlying request object so different environments can use
|
||||
// different request libs (e.g. request or browser-request)
|
||||
let request;
|
||||
@@ -107,6 +129,19 @@ module.exports.wrapRequest = function(wrapper) {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
let cryptoStoreFactory = () => new module.exports.MemoryCryptoStore;
|
||||
|
||||
/**
|
||||
* Configure a different factory to be used for creating crypto stores
|
||||
*
|
||||
* @param {Function} fac a function which will return a new
|
||||
* {@link module:crypto.store.base~CryptoStore}.
|
||||
*/
|
||||
module.exports.setCryptoStoreFactory = function(fac) {
|
||||
cryptoStoreFactory = fac;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
|
||||
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
||||
@@ -119,6 +154,13 @@ module.exports.wrapRequest = function(wrapper) {
|
||||
* {@link module:scheduler~MatrixScheduler}.
|
||||
* @param {requestFunction} opts.request If not set, defaults to the function
|
||||
* supplied to {@link request} which defaults to the request module from NPM.
|
||||
*
|
||||
* @param {module:crypto.store.base~CryptoStore=} opts.cryptoStore
|
||||
* crypto store implementation. Calls the factory supplied to
|
||||
* {@link setCryptoStoreFactory} if unspecified; or if no factory has been
|
||||
* specified, uses a default implementation (indexeddb in the browser,
|
||||
* in-memory otherwise).
|
||||
*
|
||||
* @return {MatrixClient} A new matrix client.
|
||||
* @see {@link module:client~MatrixClient} for the full list of options for
|
||||
* <code>opts</code>.
|
||||
@@ -134,6 +176,7 @@ module.exports.createClient = function(opts) {
|
||||
localStorage: global.localStorage,
|
||||
});
|
||||
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
|
||||
opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory();
|
||||
return new module.exports.MatrixClient(opts);
|
||||
};
|
||||
|
||||
|
||||
@@ -246,6 +246,45 @@ RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
|
||||
return this._displayNameToUserIds[displayName] || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if userId is in room, event is not redacted and either sender of
|
||||
* mxEvent or has power level sufficient to redact events other than their own.
|
||||
* @param {MatrixEvent} mxEvent The event to test permission for
|
||||
* @param {string} userId The user ID of the user to test permission for
|
||||
* @return {boolean} true if the given used ID can redact given event
|
||||
*/
|
||||
RoomState.prototype.maySendRedactionForEvent = function(mxEvent, userId) {
|
||||
const member = this.getMember(userId);
|
||||
if (!member || member.membership === 'leave') return false;
|
||||
|
||||
if (mxEvent.status || mxEvent.isRedacted()) return false;
|
||||
if (mxEvent.getSender() === userId) return true;
|
||||
|
||||
return this._hasSufficientPowerLevelFor('redact', member.powerLevel);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the given power level is sufficient for action
|
||||
* @param {string} action The type of power level to check
|
||||
* @param {number} powerLevel The power level of the member
|
||||
* @return {boolean} true if the given power level is sufficient
|
||||
*/
|
||||
RoomState.prototype._hasSufficientPowerLevelFor = function(action, powerLevel) {
|
||||
const powerLevelsEvent = this.getStateEvents('m.room.power_levels', '');
|
||||
|
||||
let powerLevels = {};
|
||||
if (powerLevelsEvent) {
|
||||
powerLevels = powerLevelsEvent.getContent();
|
||||
}
|
||||
|
||||
let requiredLevel = 50;
|
||||
if (powerLevels[action] !== undefined) {
|
||||
requiredLevel = powerLevels[action];
|
||||
}
|
||||
|
||||
return powerLevel >= requiredLevel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Short-form for maySendEvent('m.room.message', userId)
|
||||
* @param {string} userId The user ID of the user to test permission for
|
||||
@@ -269,7 +308,6 @@ RoomState.prototype.maySendEvent = function(eventType, userId) {
|
||||
return this._maySendEventOfType(eventType, userId, false);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the given MatrixClient has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
|
||||
+1
-19
@@ -27,6 +27,7 @@ const ContentRepo = require("../content-repo");
|
||||
const EventTimeline = require("./event-timeline");
|
||||
const EventTimelineSet = require("./event-timeline-set");
|
||||
|
||||
import reEmit from '../reemit';
|
||||
|
||||
function synthesizeReceipt(userId, event, receiptType) {
|
||||
// console.log("synthesizing receipt for "+event.getId());
|
||||
@@ -1252,25 +1253,6 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: copypasted from sync.js
|
||||
function reEmit(reEmitEntity, emittableEntity, eventNames) {
|
||||
utils.forEach(eventNames, function(eventName) {
|
||||
// setup a listener on the entity (the Room, User, etc) for this event
|
||||
emittableEntity.on(eventName, function() {
|
||||
// take the args from the listener and reuse them, adding the
|
||||
// event name to the arg list so it works with .emit()
|
||||
// Transformation Example:
|
||||
// listener on "foo" => function(a,b) { ... }
|
||||
// Re-emit on "thing" => thing.emit("foo", a, b)
|
||||
const newArgs = [eventName];
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
newArgs.push(arguments[i]);
|
||||
}
|
||||
reEmitEntity.emit(...newArgs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The Room class.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
*/
|
||||
|
||||
/**
|
||||
* re-emit events raised by one EventEmitter from another
|
||||
*
|
||||
* @param {external:EventEmitter} reEmitEntity
|
||||
* entity from which we want events to be emitted
|
||||
* @param {external:EventEmitter} emittableEntity
|
||||
* entity from which events are currently emitted
|
||||
* @param {Array<string>} eventNames
|
||||
* list of events to be reemitted
|
||||
*/
|
||||
export default function reEmit(reEmitEntity, emittableEntity, eventNames) {
|
||||
for (const eventName of eventNames) {
|
||||
// setup a listener on the entity (the Room, User, etc) for this event
|
||||
emittableEntity.on(eventName, function(...args) {
|
||||
// take the args from the listener and reuse them, adding the
|
||||
// event name to the arg list so it works with .emit()
|
||||
// Transformation Example:
|
||||
// listener on "foo" => function(a,b) { ... }
|
||||
// Re-emit on "thing" => thing.emit("foo", a, b)
|
||||
reEmitEntity.emit(eventName, ...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,14 @@ WebStorageSessionStore.prototype = {
|
||||
return getJsonItem(this.store, keyEndToEndDevicesForUser(userId));
|
||||
},
|
||||
|
||||
storeEndToEndDeviceTrackingStatus: function(statusMap) {
|
||||
setJsonItem(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS, statusMap);
|
||||
},
|
||||
|
||||
getEndToEndDeviceTrackingStatus: function() {
|
||||
return getJsonItem(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS);
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the sync token corresponding to the device list.
|
||||
*
|
||||
@@ -202,6 +210,7 @@ WebStorageSessionStore.prototype = {
|
||||
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
const KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
|
||||
const KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token";
|
||||
const KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS = E2E_PREFIX + "device_tracking";
|
||||
|
||||
function keyEndToEndDevicesForUser(userId) {
|
||||
return E2E_PREFIX + "devices/" + userId;
|
||||
|
||||
+2
-18
@@ -31,6 +31,8 @@ const utils = require("./utils");
|
||||
const Filter = require("./filter");
|
||||
const EventTimeline = require("./models/event-timeline");
|
||||
|
||||
import reEmit from './reemit';
|
||||
|
||||
const DEBUG = true;
|
||||
|
||||
// /sync requests allow you to set a timeout= but the request may continue
|
||||
@@ -1252,23 +1254,5 @@ function createNewUser(client, userId) {
|
||||
return user;
|
||||
}
|
||||
|
||||
function reEmit(reEmitEntity, emittableEntity, eventNames) {
|
||||
utils.forEach(eventNames, function(eventName) {
|
||||
// setup a listener on the entity (the Room, User, etc) for this event
|
||||
emittableEntity.on(eventName, function() {
|
||||
// take the args from the listener and reuse them, adding the
|
||||
// event name to the arg list so it works with .emit()
|
||||
// Transformation Example:
|
||||
// listener on "foo" => function(a,b) { ... }
|
||||
// Re-emit on "thing" => thing.emit("foo", a, b)
|
||||
const newArgs = [eventName];
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
newArgs.push(arguments[i]);
|
||||
}
|
||||
reEmitEntity.emit(...newArgs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** */
|
||||
module.exports = SyncApi;
|
||||
|
||||
+123
-64
@@ -91,7 +91,7 @@ function MatrixCall(opts) {
|
||||
utils.checkObjectHasKeys(server, ["urls"]);
|
||||
});
|
||||
|
||||
this.callId = "c" + new Date().getTime();
|
||||
this.callId = "c" + new Date().getTime() + Math.random();
|
||||
this.state = 'fledgling';
|
||||
this.didConnect = false;
|
||||
|
||||
@@ -155,7 +155,7 @@ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoEle
|
||||
/**
|
||||
* Place a screen-sharing call to this room. This includes audio.
|
||||
* <b>This method is EXPERIMENTAL and subject to change without warning. It
|
||||
* only works in Google Chrome.</b>
|
||||
* only works in Google Chrome and Firefox >= 44.</b>
|
||||
* @param {Element} remoteVideoElement a <code><video></code> DOM element
|
||||
* to render video to.
|
||||
* @param {Element} localVideoElement a <code><video></code> DOM element
|
||||
@@ -166,7 +166,7 @@ MatrixCall.prototype.placeScreenSharingCall =
|
||||
function(remoteVideoElement, localVideoElement) {
|
||||
debuglog("placeScreenSharingCall");
|
||||
checkForErrorListener(this);
|
||||
const screenConstraints = _getChromeScreenSharingConstraints(this);
|
||||
const screenConstraints = _getScreenSharingConstraints(this);
|
||||
if (!screenConstraints) {
|
||||
return;
|
||||
}
|
||||
@@ -335,6 +335,7 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
|
||||
MatrixCall.prototype.setRemoteAudioElement = function(element) {
|
||||
this.remoteVideoElement.muted = true;
|
||||
this.remoteAudioElement = element;
|
||||
this.remoteAudioElement.muted = false;
|
||||
_tryPlayRemoteAudioStream(this);
|
||||
};
|
||||
|
||||
@@ -409,12 +410,12 @@ MatrixCall.prototype.answer = function() {
|
||||
if (!this.localAVStream && !this.waitForLocalAVStream) {
|
||||
this.webRtc.getUserMedia(
|
||||
_getUserMediaVideoContraints(this.type),
|
||||
hookCallback(self, self._gotUserMediaForAnswer),
|
||||
hookCallback(self, self._getUserMediaFailed),
|
||||
hookCallback(self, self._maybeGotUserMediaForAnswer),
|
||||
hookCallback(self, self._maybeGotUserMediaForAnswer),
|
||||
);
|
||||
setState(this, 'wait_local_media');
|
||||
} else if (this.localAVStream) {
|
||||
this._gotUserMediaForAnswer(this.localAVStream);
|
||||
this._maybeGotUserMediaForAnswer(this.localAVStream);
|
||||
} else if (this.waitForLocalAVStream) {
|
||||
setState(this, 'wait_local_media');
|
||||
}
|
||||
@@ -433,11 +434,11 @@ MatrixCall.prototype._replacedBy = function(newCall) {
|
||||
newCall.waitForLocalAVStream = true;
|
||||
} else if (this.state == 'create_offer') {
|
||||
debuglog("Handing local stream to new call");
|
||||
newCall._gotUserMediaForAnswer(this.localAVStream);
|
||||
newCall._maybeGotUserMediaForAnswer(this.localAVStream);
|
||||
delete(this.localAVStream);
|
||||
} else if (this.state == 'invite_sent') {
|
||||
debuglog("Handing local stream to new call");
|
||||
newCall._gotUserMediaForAnswer(this.localAVStream);
|
||||
newCall._maybeGotUserMediaForAnswer(this.localAVStream);
|
||||
delete(this.localAVStream);
|
||||
}
|
||||
newCall.localVideoElement = this.localVideoElement;
|
||||
@@ -523,48 +524,69 @@ MatrixCall.prototype.isMicrophoneMuted = function() {
|
||||
* @private
|
||||
* @param {Object} stream
|
||||
*/
|
||||
MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
|
||||
MatrixCall.prototype._maybeGotUserMediaForInvite = function(stream) {
|
||||
if (this.successor) {
|
||||
this.successor._gotUserMediaForAnswer(stream);
|
||||
this.successor._maybeGotUserMediaForAnswer(stream);
|
||||
return;
|
||||
}
|
||||
if (this.state == 'ended') {
|
||||
return;
|
||||
}
|
||||
debuglog("_gotUserMediaForInvite -> " + this.type);
|
||||
debuglog("_maybeGotUserMediaForInvite -> " + this.type);
|
||||
const self = this;
|
||||
const videoEl = this.getLocalVideoElement();
|
||||
|
||||
if (videoEl && this.type == 'video') {
|
||||
videoEl.autoplay = true;
|
||||
if (this.screenSharingStream) {
|
||||
debuglog("Setting screen sharing stream to the local video element");
|
||||
this.assignElement(videoEl, this.screenSharingStream, "localVideo");
|
||||
} else {
|
||||
this.assignElement(videoEl, stream, "localVideo");
|
||||
}
|
||||
videoEl.muted = true;
|
||||
setTimeout(function() {
|
||||
const vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
self.playElement(vel, "localVideo");
|
||||
const error = stream;
|
||||
const constraints = {
|
||||
'mandatory': {
|
||||
'OfferToReceiveAudio': true,
|
||||
'OfferToReceiveVideo': self.type === 'video',
|
||||
},
|
||||
};
|
||||
if (stream instanceof MediaStream) {
|
||||
const videoEl = this.getLocalVideoElement();
|
||||
|
||||
if (videoEl && this.type == 'video') {
|
||||
videoEl.autoplay = true;
|
||||
if (this.screenSharingStream) {
|
||||
debuglog("Setting screen sharing stream to the local video" +
|
||||
" element");
|
||||
this.assignElement(videoEl, this.screenSharingStream, "localVideo");
|
||||
} else {
|
||||
this.assignElement(videoEl, stream, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
videoEl.muted = true;
|
||||
setTimeout(function() {
|
||||
const vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
self.playElement(vel, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (this.screenSharingStream) {
|
||||
this.screenSharingStream.addTrack(stream.getAudioTracks()[0]);
|
||||
stream = this.screenSharingStream;
|
||||
}
|
||||
|
||||
this.localAVStream = stream;
|
||||
// why do we enable audio (and only audio) tracks here? -- matthew
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
this.peerConn = _createPeerConnection(this);
|
||||
this.peerConn.addStream(stream);
|
||||
} else if (error.name === 'PermissionDeniedError') {
|
||||
debuglog('User denied access to camera/microphone.' +
|
||||
' Or possibly you are using an insecure domain. Receiving only.');
|
||||
this.peerConn = _createPeerConnection(this);
|
||||
} else {
|
||||
debuglog('Failed to getUserMedia.');
|
||||
this._getUserMediaFailed(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.screenSharingStream) {
|
||||
this.screenSharingStream.addTrack(stream.getAudioTracks()[0]);
|
||||
stream = this.screenSharingStream;
|
||||
}
|
||||
|
||||
this.localAVStream = stream;
|
||||
// why do we enable audio (and only audio) tracks here? -- matthew
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
this.peerConn = _createPeerConnection(this);
|
||||
this.peerConn.addStream(stream);
|
||||
this.peerConn.createOffer(
|
||||
hookCallback(self, self._gotLocalOffer),
|
||||
hookCallback(self, self._getLocalOfferFailed),
|
||||
constraints,
|
||||
);
|
||||
setState(self, 'create_offer');
|
||||
};
|
||||
@@ -574,33 +596,44 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
|
||||
* @private
|
||||
* @param {Object} stream
|
||||
*/
|
||||
MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
|
||||
MatrixCall.prototype._maybeGotUserMediaForAnswer = function(stream) {
|
||||
const self = this;
|
||||
if (self.state == 'ended') {
|
||||
return;
|
||||
}
|
||||
const localVidEl = self.getLocalVideoElement();
|
||||
|
||||
if (localVidEl && self.type == 'video') {
|
||||
localVidEl.autoplay = true;
|
||||
this.assignElement(localVidEl, stream, "localVideo");
|
||||
localVidEl.muted = true;
|
||||
setTimeout(function() {
|
||||
const vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
self.playElement(vel, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
const error = stream;
|
||||
if (stream instanceof MediaStream) {
|
||||
const localVidEl = self.getLocalVideoElement();
|
||||
|
||||
if (localVidEl && self.type == 'video') {
|
||||
localVidEl.autoplay = true;
|
||||
this.assignElement(localVidEl, stream, "localVideo");
|
||||
localVidEl.muted = true;
|
||||
setTimeout(function() {
|
||||
const vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
self.playElement(vel, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
self.localAVStream = stream;
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
self.peerConn.addStream(stream);
|
||||
} else if (error.name === 'PermissionDeniedError') {
|
||||
debuglog('User denied access to camera/microphone.' +
|
||||
' Or possibly you are using an insecure domain. Receiving only.');
|
||||
} else {
|
||||
debuglog('Failed to getUserMedia.');
|
||||
this._getUserMediaFailed(error);
|
||||
return;
|
||||
}
|
||||
|
||||
self.localAVStream = stream;
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
self.peerConn.addStream(stream);
|
||||
|
||||
const constraints = {
|
||||
'mandatory': {
|
||||
'OfferToReceiveAudio': true,
|
||||
'OfferToReceiveVideo': self.type == 'video',
|
||||
'OfferToReceiveVideo': self.type === 'video',
|
||||
},
|
||||
};
|
||||
self.peerConn.createAnswer(function(description) {
|
||||
@@ -1126,8 +1159,8 @@ const _placeCallWithConstraints = function(self, constraints) {
|
||||
self.client.callList[self.callId] = self;
|
||||
self.webRtc.getUserMedia(
|
||||
constraints,
|
||||
hookCallback(self, self._gotUserMediaForInvite),
|
||||
hookCallback(self, self._getUserMediaFailed),
|
||||
hookCallback(self, self._maybeGotUserMediaForInvite),
|
||||
hookCallback(self, self._maybeGotUserMediaForInvite),
|
||||
);
|
||||
setState(self, 'wait_local_media');
|
||||
self.direction = 'outbound';
|
||||
@@ -1160,7 +1193,7 @@ const _createPeerConnection = function(self) {
|
||||
return pc;
|
||||
};
|
||||
|
||||
const _getChromeScreenSharingConstraints = function(call) {
|
||||
const _getScreenSharingConstraints = function(call) {
|
||||
const screen = global.screen;
|
||||
if (!screen) {
|
||||
call.emit("error", callError(
|
||||
@@ -1172,6 +1205,7 @@ const _getChromeScreenSharingConstraints = function(call) {
|
||||
|
||||
return {
|
||||
video: {
|
||||
mediaSource: 'screen',
|
||||
mandatory: {
|
||||
chromeMediaSource: "screen",
|
||||
chromeMediaSourceId: "" + Date.now(),
|
||||
@@ -1187,16 +1221,25 @@ const _getChromeScreenSharingConstraints = function(call) {
|
||||
const _getUserMediaVideoContraints = function(callType) {
|
||||
switch (callType) {
|
||||
case 'voice':
|
||||
return ({audio: true, video: false});
|
||||
return {
|
||||
audio: {
|
||||
deviceId: audioInput ? {exact: audioInput} : undefined,
|
||||
}, video: false,
|
||||
};
|
||||
case 'video':
|
||||
return ({audio: true, video: {
|
||||
mandatory: {
|
||||
minWidth: 640,
|
||||
maxWidth: 640,
|
||||
minHeight: 360,
|
||||
maxHeight: 360,
|
||||
return {
|
||||
audio: {
|
||||
deviceId: audioInput ? {exact: audioInput} : undefined,
|
||||
}, video: {
|
||||
deviceId: videoInput ? {exact: videoInput} : undefined,
|
||||
mandatory: {
|
||||
minWidth: 640,
|
||||
maxWidth: 640,
|
||||
minHeight: 360,
|
||||
maxHeight: 360,
|
||||
},
|
||||
},
|
||||
}});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1228,6 +1271,22 @@ const forAllTracksOnStream = function(s, f) {
|
||||
/** The MatrixCall class. */
|
||||
module.exports.MatrixCall = MatrixCall;
|
||||
|
||||
let audioInput;
|
||||
let videoInput;
|
||||
/**
|
||||
* Set an audio input device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
module.exports.setAudioInput = function(deviceId) { audioInput = deviceId; };
|
||||
/**
|
||||
* Set a video input device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
module.exports.setVideoInput = function(deviceId) { videoInput = deviceId; };
|
||||
|
||||
/**
|
||||
* Create a new Matrix call for the browser.
|
||||
|
||||
Reference in New Issue
Block a user