Compare commits

...

100 Commits

Author SHA1 Message Date
RiotRobot ae6e2cca27 v9.10.0-rc.1 2021-03-25 12:06:34 +00:00
RiotRobot bd920eef1f Prepare changelog for v9.10.0-rc.1 2021-03-25 12:06:34 +00:00
Travis Ralston ed6d4e5f6c Merge pull request #1647 from SimonBrandner/dont-send-hangup
Don't send m.call.hangup if m.call.invite wasn't sent either
2021-03-22 00:42:04 -06:00
Travis Ralston accfa325b5 Merge pull request #1641 from NicolaiSoeborg/fix-registerGuest
docs: registerGuest()
2021-03-21 20:37:49 -06:00
Šimon Brandner b6ef8d95cd Don't send hangup if invite wasn't sent
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-21 21:29:00 +01:00
Travis Ralston c1144e3810 Merge pull request #1639 from Johennes/feature/chunk-device-keys
Download device keys in chunks of 250
2021-03-18 13:17:15 -06:00
Johannes Marbach 8663fd402b Download device keys in chunks of 250
Depending on the number of users in the request, the server might
overload. To prevent this, the download is broken into chunks of
250 users each. Additionally, no more than 3 requests are kicked off
at the same time to avoid running into rate limiting. Responses are
processed once all chunks have been downloaded.

Fixes: #1619

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2021-03-17 20:56:25 +01:00
David Baker f34052fd31 Merge pull request #1646 from matrix-org/dbkr/more_call_fixes
More VoIP connectivity fixes
2021-03-17 15:00:09 +00:00
David Baker 27d75a269f unintentional comment 2021-03-16 19:18:45 +00:00
David Baker d208a7fc5f Remove unintentionally committed stuff 2021-03-16 19:17:04 +00:00
David Baker 702e16e3df More VoIP connectivity fixes
* Don't ignore other candidates when we see a null one (continue
   rather than return)
 * await on addICECandidate()
 * Don't add ice candidates until we've set a remote description
 * More & better logging
2021-03-16 19:13:03 +00:00
Travis Ralston 12050b14f0 Merge pull request #1644 from SimonBrandner/fix-optional
Make selectDesktopCapturerSource param optional
2021-03-15 21:12:03 -06:00
Nicolai Søborg 2d4a4f1736 docs: registerGuest 2021-03-15 22:46:43 +00:00
RiotRobot cd38fb9b4c Resetting package fields for development 2021-03-15 14:34:56 +00:00
RiotRobot 7941b16ec4 Merge branch 'master' into develop 2021-03-15 14:34:56 +00:00
RiotRobot 3ff517e76e v9.9.0 2021-03-15 14:31:58 +00:00
RiotRobot 9559b26310 Prepare changelog for v9.9.0 2021-03-15 14:31:57 +00:00
Šimon Brandner 56ea4b8741 Make selectDesktopCapturerSource param optional
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-13 15:03:36 +01:00
Nicolai Søborg 6dabfcda6f setGuest(true) when registerGuest()
Signed-off-by: Nicolai Søborg <git@xn--sb-lka.org>
2021-03-11 22:39:51 +01:00
J. Ryan Stinnett 0b7b35f800 Merge pull request #1632 from matrix-org/matthew/rework-cross-signing-login
Expose APIs needed for reworked cross-signing login flow
2021-03-11 12:54:18 +00:00
RiotRobot 0ffdf7c0f1 v9.9.0-rc.1 2021-03-10 17:21:50 +00:00
RiotRobot 13b6db8eb4 Prepare changelog for v9.9.0-rc.1 2021-03-10 17:21:49 +00:00
J. Ryan Stinnett 481acb2a1a Merge pull request #1638 from matrix-org/jryans/rm-olm-profiling
Remove detailed Olm session logging
2021-03-10 12:44:22 +00:00
J. Ryan Stinnett 683092140d Remove OTK claim timeout logging 2021-03-10 12:43:45 +00:00
J. Ryan Stinnett 1bb8c2d1a5 Remove detailed Olm session logging
Now that we understand the Olm session deadlock, we shouldn't need this detailed
per-session logging.

Fixes https://github.com/vector-im/element-web/issues/16647
2021-03-10 12:43:45 +00:00
J. Ryan Stinnett 60fd3b0786 Remove extra space in log message 2021-03-10 11:25:44 +00:00
J. Ryan Stinnett cd4abc4e9b Disable crypto transaction profiling 2021-03-10 11:05:17 +00:00
Michael Telatynski e6a21cc487 Merge pull request #1637 from matrix-org/t3chguy/spaces4.5
Add space summary suggested only param
2021-03-10 10:55:27 +00:00
David Baker 6c5fc153bf Merge pull request #1634 from matrix-org/dbkr/check_turn_interval
Check TURN servers periodically, and at start of calls
2021-03-09 17:06:02 +00:00
David Baker 07f15b41a2 Don't start the timer if voip not supported 2021-03-09 14:39:21 +00:00
David Baker 8375638d76 Fix tests
Bit of a re-organisation so a peerconnection exists when the tests
go to mock things out. placeCall methods return promises to make this
possible.
2021-03-09 14:09:55 +00:00
J. Ryan Stinnett bed7543b46 Merge pull request #1624 from robintown/invite-reasons
Support sending invite reasons
2021-03-09 11:44:04 +00:00
Travis Ralston dc55236263 Merge pull request #1636 from matrix-org/dependabot/npm_and_yarn/elliptic-6.5.4
Bump elliptic from 6.5.3 to 6.5.4
2021-03-08 18:23:50 -07:00
dependabot[bot] 5f3427c5d1 Bump elliptic from 6.5.3 to 6.5.4
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 01:23:08 +00:00
Travis Ralston 66e5af185d Merge pull request #1635 from matrix-org/travis/media-customization
Add a function to get a room's MXC URI
2021-03-08 18:23:00 -07:00
Travis Ralston 0ff611e033 Enum and linter 2021-03-08 18:16:39 -07:00
Travis Ralston 737cadaabc Add a function to get a room's MXC URI
This matches the RoomMember function of the same name.
2021-03-08 18:13:14 -07:00
Matthew Hodgson 9fb2fbaeec factor out getDehydratedDevice 2021-03-09 00:09:22 +00:00
David Baker 51e817a3a2 This is in ms, not seconds 2021-03-08 18:54:50 +00:00
David Baker 59c93b59bf Check TURN servers periodically, and at start of calls
Hopefully this should make our turn-credential checking code a bit
more robust (and possibly fix a seconds / ms mismatch).
2021-03-08 18:49:25 +00:00
David Baker c18ef051fc Merge pull request #1633 from matrix-org/dbkr/stop_streams_if_call_ended
Stop streams if the call has ended
2021-03-08 17:01:31 +00:00
David Baker 1ac5c9acbd Stop streams if the call has ended
When we get user media, don't forget to close the streams if the
call's ended by the time we got media.
2021-03-08 16:55:48 +00:00
J. Ryan Stinnett a034ca171e Merge pull request #1631 from SimonBrandner/remove-export
Remove export keyword from global.d.ts
2021-03-08 14:11:37 +00:00
Matthew Hodgson 977682d37f fix lint 2021-03-08 09:24:25 +00:00
Matthew Hodgson 0bafe263d7 fix lint 2021-03-08 05:05:14 +00:00
Matthew Hodgson 1a8fced80e Merge branch 'develop' into matthew/rework-cross-signing-login 2021-03-08 04:59:40 +00:00
Matthew Hodgson 1c4d0b5e99 expose getDevice API 2021-03-08 04:59:29 +00:00
Matthew Hodgson 844a2b457c expose getDehydratedDevice API 2021-03-08 04:59:19 +00:00
Matthew Hodgson ccf06f2216 don't cancel ourselves when selecting a self-verification partner 2021-03-08 04:58:55 +00:00
Šimon Brandner f630a9f297 Remove export
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-06 16:14:12 +01:00
Michael Telatynski 2f71c93b53 Add space summary suggested only param 2021-03-05 16:12:39 +00:00
J. Ryan Stinnett 92032a17a8 Merge pull request #1445 from florianjacob/patch-1
Fix IndexedDB store creation example
2021-03-04 16:28:05 +00:00
David Baker e531456d42 Merge pull request #1613 from SimonBrandner/constraint-cleanup
An attempt to  cleanup how constraints are handled in calls
2021-03-03 15:03:53 +00:00
Šimon Brandner f0b2d2fe4d Null-check screenshareConstraints
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-03 15:38:49 +01:00
Šimon Brandner 427500220d Remove AudioVideo ConstraintsType
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-03 15:35:02 +01:00
Šimon Brandner 32e19ead74 Merge branch 'develop' into constraint-cleanup 2021-03-03 15:30:46 +01:00
J. Ryan Stinnett d11adb6f43 Merge pull request #1628 from matrix-org/jryans/opt-display-name
Extract display name patterns to constants
2021-03-03 11:44:30 +00:00
Travis Ralston f6155a50f6 Merge pull request #1630 from matrix-org/dependabot/npm_and_yarn/pug-code-gen-2.0.3
Bump pug-code-gen from 2.0.2 to 2.0.3
2021-03-02 21:55:39 -07:00
dependabot[bot] 4efee9445d Bump pug-code-gen from 2.0.2 to 2.0.3
Bumps [pug-code-gen](https://github.com/pugjs/pug) from 2.0.2 to 2.0.3.
- [Release notes](https://github.com/pugjs/pug/releases)
- [Commits](https://github.com/pugjs/pug/compare/pug-code-gen@2.0.2...pug@2.0.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-03 03:35:37 +00:00
J. Ryan Stinnett 20746a433f Extract display name patterns to constants
This changes to constant patterns for calculating display names, which cuts out
~18% of the time spent and reduces GC pressure as well.
2021-03-02 17:52:57 +00:00
J. Ryan Stinnett 31dacc4206 Merge pull request #1627 from matrix-org/jryans/olm-session-deadlock
Avoid deadlocks when ensuring Olm sessions for devices
2021-03-02 14:35:04 +00:00
J. Ryan Stinnett 88e5c59a85 Fix lint warning on OTK result variable 2021-03-02 13:03:12 +00:00
J. Ryan Stinnett cf74920b36 Remove redundant Olm session in progress deletion
This removes extra steps that duplicated deletion of an in progress Olm session.
Resolving the promise handles removing the session from the in progress set, so
there's no need to do it again. There's also no need to delete from
`resolveSession`, as it's okay to resolve a promise multiple times.
2021-03-02 12:58:09 +00:00
J. Ryan Stinnett 972c900b58 Remove unused support for rejecting in progress Olm sessions
This removes the unused `reject` path for in progress Olm sessions to simplify
understanding the code.
2021-03-02 12:55:43 +00:00
J. Ryan Stinnett 12d5fd79f7 Avoid deadlocks when ensuring Olm sessions for devices
This reworks tracking the Olm sessions a particular task is updating to avoid
deadlocks. By ensuring we synchronously mark all sessions a task cares about as
in progress from the start, we know that no other tasks will own updating a
session in common, which avoids deadlocks across multiple tasks that might be
working on a shared set of devices.

Fixes https://github.com/vector-im/element-web/issues/16194
2021-03-02 12:50:49 +00:00
J. Ryan Stinnett a29f6979b2 Merge pull request #1626 from matrix-org/jryans/replacement-senders
Filter out edits from other senders in history
2021-03-02 12:33:21 +00:00
J. Ryan Stinnett 3a7146c77b Only log claim timeouts when a time was provided
This avoids logging immediately on various code paths (including tests) where no
timeout value is supplied.
2021-03-02 12:22:58 +00:00
J. Ryan Stinnett b178d8f629 Filter out edits from other senders in history
We currently don't support edits from other senders, but the server may not
filter them, so we filter them here on the client.
2021-03-02 12:15:27 +00:00
Šimon Brandner 0c94ee62a3 Pass in selectDesktopCapturerSource()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-02 13:00:57 +01:00
Šimon Brandner e7562898cd Add getScreenshareContraints()
This is nicer since we avoid some async functions

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-02 12:58:45 +01:00
RiotRobot fb73ab6878 Resetting package fields for development 2021-03-01 12:48:30 +00:00
RiotRobot 38f978791d Merge branch 'master' into develop 2021-03-01 12:48:30 +00:00
RiotRobot 5dd60de57d v9.8.0 2021-03-01 12:44:55 +00:00
RiotRobot 5efbfc2dba Prepare changelog for v9.8.0 2021-03-01 12:44:54 +00:00
J. Ryan Stinnett fcd1dbad89 Merge pull request #1618 from robintown/fix-content-helpers-export
Fix ContentHelpers export
2021-03-01 11:13:10 +00:00
J. Ryan Stinnett ad521bf4c2 Merge pull request #1621 from matrix-org/jryans/megolm-logs-2021-02-26
Add logging to in progress Olm sessions
2021-02-28 17:52:24 +00:00
J. Ryan Stinnett 8152fa44e0 Add more logging scopes to session IDs
This uses prefix chaining to correlate several scopes together.
2021-02-28 17:15:07 +00:00
J. Ryan Stinnett e217bf9e37 Enable prefixed loggers to chain 2021-02-28 17:15:07 +00:00
David Baker bfad21f811 Merge pull request #1623 from matrix-org/dbkr/ice_candidate_buffer
Don't ignore ICE candidates received before offer/answer
2021-02-27 15:11:26 +00:00
David Baker 81e68abce3 Merge pull request #1622 from matrix-org/dbkr/candidate_retries
Better handling of send failures on VoIP events
2021-02-27 15:11:00 +00:00
David Baker 7963bb352d Merge pull request #1620 from matrix-org/dbkr/log_turn_cred_expiry
Log when turn creds expire
2021-02-27 15:09:54 +00:00
Michael Telatynski 14d3882059 Merge pull request #1563 from matrix-org/t3chguy/spaces
Initial Spaces [MSC1772] support
2021-02-26 22:08:09 +00:00
Robin Townsend dede508e89 Support sending invite reasons
Added as the final argument to `invite` in order to keep backwards
compatibility.

Signed-off-by: Robin Townsend <robin@robin.town>
2021-02-26 16:46:18 -05:00
David Baker ea39b69f65 Don't ignore ICE candidates received before offer/answer
The main bug here was a race on the callee side because we await-ed
on setRemoteDescription before setting the opponent party ID, and
while we were await-ing, the callEventHandler could give us candidate
events which we'd duly ignore because we thought the party ID didn't
match.

This also meant that any candidates that arrived before the answer
would have been ignored. Save these up by party ID and then add the
ones from the party ID that we pick once the answer comes in.

Also fix the confusion on party IDs where we weren't sure whether
we hadn't picked an opponent or we'd picked an opponent without a
party ID. It's now undefined for the former and null for the latter,
as it claims to be in the comment.
2021-02-26 21:25:52 +00:00
David Baker eafecd36bc Better handling of send failures on VoIP events
Don't leave candidate message sin the queue, abort if we fail to
send the invite.
2021-02-26 18:42:05 +00:00
J. Ryan Stinnett 198c9a2507 Add logging to in progress Olm sessions
It seems like this might be where
https://github.com/vector-im/element-web/issues/16194 is deadlocking.
2021-02-26 17:27:06 +00:00
David Baker d07563013b Log when turn creds expire 2021-02-26 14:47:27 +00:00
Michael Telatynski 9e967832cd Update space summary API unstable prefix 2021-02-26 10:37:09 +00:00
Michael Telatynski bfe1987cd9 Add Spaces event types from MSC1772 2021-02-26 10:35:02 +00:00
Robin Townsend 1045538f1f Fix ContentHelpers export
This was previously exporting a promise, since it called the import
function manually but didn't await the result. However, since we have
Babel we can just use the new export … as … from syntax instead.

Signed-off-by: Robin Townsend <robin@robin.town>
2021-02-25 14:41:48 -05:00
J. Ryan Stinnett fccf08edcf Merge pull request #1617 from matrix-org/jryans/crypto-store-logging
Add logging to crypto store transactions
2021-02-25 16:58:45 +00:00
J. Ryan Stinnett f43fe366b5 Add logging to crypto store transactions
We churn through a huge number of crypto store transactions during startup,
which may be the cause of the symptoms in
https://github.com/vector-im/element-web/issues/16194.
2021-02-25 16:49:49 +00:00
Michael Telatynski 0f75f2ef9c Add base API for Space Summary MSC2946 2021-02-25 13:12:22 +00:00
Michael Telatynski 2cdc68f9c3 Merge pull request #1610 from matrix-org/t3chguy/spaces2
Room helper for getting type and checking if it is a space room
2021-02-25 11:30:49 +00:00
Šimon Brandner 5849ea8e63 Add AudioVideo constraint type
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-23 12:25:20 +01:00
Šimon Brandner 20afebf339 Set video to true
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-23 12:23:49 +01:00
Šimon Brandner 20eaba191e Simplifie placeScreenSharingCall()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-23 11:12:16 +01:00
Šimon Brandner ba58d3c544 Add screenshare type to getUserMediaContraints()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-23 10:47:43 +01:00
Michael Telatynski 8e0fc8d460 Room helper for getting type and checking if it is a space room 2021-02-19 14:21:22 +00:00
florianjacob 22713d8f89 correct IndexedDB store creation example 2020-08-26 14:24:13 +02:00
28 changed files with 926 additions and 299 deletions
+78
View File
@@ -1,3 +1,81 @@
Changes in [9.10.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.10.0-rc.1) (2021-03-25)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0...v9.10.0-rc.1)
* Don't send m.call.hangup if m.call.invite wasn't sent either
[\#1647](https://github.com/matrix-org/matrix-js-sdk/pull/1647)
* docs: registerGuest()
[\#1641](https://github.com/matrix-org/matrix-js-sdk/pull/1641)
* Download device keys in chunks of 250
[\#1639](https://github.com/matrix-org/matrix-js-sdk/pull/1639)
* More VoIP connectivity fixes
[\#1646](https://github.com/matrix-org/matrix-js-sdk/pull/1646)
* Make selectDesktopCapturerSource param optional
[\#1644](https://github.com/matrix-org/matrix-js-sdk/pull/1644)
* Expose APIs needed for reworked cross-signing login flow
[\#1632](https://github.com/matrix-org/matrix-js-sdk/pull/1632)
Changes in [9.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.9.0) (2021-03-15)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0-rc.1...v9.9.0)
* No changes since rc.1
Changes in [9.9.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.9.0-rc.1) (2021-03-10)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.8.0...v9.9.0-rc.1)
* Remove detailed Olm session logging
[\#1638](https://github.com/matrix-org/matrix-js-sdk/pull/1638)
* Add space summary suggested only param
[\#1637](https://github.com/matrix-org/matrix-js-sdk/pull/1637)
* Check TURN servers periodically, and at start of calls
[\#1634](https://github.com/matrix-org/matrix-js-sdk/pull/1634)
* Support sending invite reasons
[\#1624](https://github.com/matrix-org/matrix-js-sdk/pull/1624)
* Bump elliptic from 6.5.3 to 6.5.4
[\#1636](https://github.com/matrix-org/matrix-js-sdk/pull/1636)
* Add a function to get a room's MXC URI
[\#1635](https://github.com/matrix-org/matrix-js-sdk/pull/1635)
* Stop streams if the call has ended
[\#1633](https://github.com/matrix-org/matrix-js-sdk/pull/1633)
* Remove export keyword from global.d.ts
[\#1631](https://github.com/matrix-org/matrix-js-sdk/pull/1631)
* Fix IndexedDB store creation example
[\#1445](https://github.com/matrix-org/matrix-js-sdk/pull/1445)
* An attempt to cleanup how constraints are handled in calls
[\#1613](https://github.com/matrix-org/matrix-js-sdk/pull/1613)
* Extract display name patterns to constants
[\#1628](https://github.com/matrix-org/matrix-js-sdk/pull/1628)
* Bump pug-code-gen from 2.0.2 to 2.0.3
[\#1630](https://github.com/matrix-org/matrix-js-sdk/pull/1630)
* Avoid deadlocks when ensuring Olm sessions for devices
[\#1627](https://github.com/matrix-org/matrix-js-sdk/pull/1627)
* Filter out edits from other senders in history
[\#1626](https://github.com/matrix-org/matrix-js-sdk/pull/1626)
* Fix ContentHelpers export
[\#1618](https://github.com/matrix-org/matrix-js-sdk/pull/1618)
* Add logging to in progress Olm sessions
[\#1621](https://github.com/matrix-org/matrix-js-sdk/pull/1621)
* Don't ignore ICE candidates received before offer/answer
[\#1623](https://github.com/matrix-org/matrix-js-sdk/pull/1623)
* Better handling of send failures on VoIP events
[\#1622](https://github.com/matrix-org/matrix-js-sdk/pull/1622)
* Log when turn creds expire
[\#1620](https://github.com/matrix-org/matrix-js-sdk/pull/1620)
* Initial Spaces [MSC1772] support
[\#1563](https://github.com/matrix-org/matrix-js-sdk/pull/1563)
* Add logging to crypto store transactions
[\#1617](https://github.com/matrix-org/matrix-js-sdk/pull/1617)
* Room helper for getting type and checking if it is a space room
[\#1610](https://github.com/matrix-org/matrix-js-sdk/pull/1610)
Changes in [9.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.8.0) (2021-03-01)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.8.0-rc.1...v9.8.0)
* No changes since rc.1
Changes in [9.8.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.8.0-rc.1) (2021-02-24)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.7.0...v9.8.0-rc.1)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "9.8.0-rc.1",
"version": "9.10.0-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
@@ -15,7 +15,7 @@
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 73 src spec",
"lint:js": "eslint --max-warnings 72 src spec",
"lint:types": "tsc --noEmit",
"test": "jest spec/ --coverage --testEnvironment node",
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
+58 -2
View File
@@ -51,6 +51,36 @@ const signedDeviceList = {
},
};
const signedDeviceList2 = {
"failures": {},
"device_keys": {
"@test2:sw1v.org": {
"QJVRHWAKGH": {
"signatures": {
"@test2:sw1v.org": {
"ed25519:QJVRHWAKGH":
"w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" +
"1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3",
},
},
"user_id": "@test2:sw1v.org",
"keys": {
"ed25519:QJVRHWAKGH":
"Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
"curve25519:QJVRHWAKGH":
"YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
},
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2",
],
"device_id": "QJVRHWAKGH",
"unsigned": {},
},
},
},
};
describe('DeviceList', function() {
let downloadSpy;
let cryptoStore;
@@ -69,7 +99,7 @@ describe('DeviceList', function() {
}
});
function createTestDeviceList() {
function createTestDeviceList(keyDownloadChunkSize = 250) {
const baseApis = {
downloadKeysForUsers: downloadSpy,
getUserId: () => '@test1:sw1v.org',
@@ -78,7 +108,7 @@ describe('DeviceList', function() {
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
deviceLists.push(dl);
return dl;
}
@@ -150,4 +180,30 @@ describe('DeviceList', function() {
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});
it("should download device keys in batches", function() {
const dl = createTestDeviceList(1);
dl.startTrackingDeviceList('@test1:sw1v.org');
dl.startTrackingDeviceList('@test2:sw1v.org');
const queryDefer1 = utils.defer();
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
const queryDefer2 = utils.defer();
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toBeCalledTimes(2);
expect(downloadSpy).toHaveBeenNthCalledWith(1, ['@test1:sw1v.org'], {});
expect(downloadSpy).toHaveBeenNthCalledWith(2, ['@test2:sw1v.org'], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
queryDefer2.resolve(utils.deepCopy(signedDeviceList2));
return prom1.then(() => {
const storedKeys1 = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
});
});
});
+86
View File
@@ -190,5 +190,91 @@ describe("OlmDevice", function() {
// new session and will have called claimOneTimeKeys
expect(count).toBe(2);
});
it("avoids deadlocks when two tasks are ensuring the same devices", async function() {
// This test checks whether `ensureOlmSessionsForDevices` properly
// handles multiple tasks in flight ensuring some set of devices in
// common without deadlocks.
let claimRequestCount = 0;
const baseApis = {
claimOneTimeKeys: () => {
// simulate a very slow server (.5 seconds to respond)
claimRequestCount++;
return new Promise((resolve, reject) => {
setTimeout(reject, 500);
});
},
};
const deviceBobA = DeviceInfo.fromStorage({
keys: {
"curve25519:BOB-A": "akey",
},
}, "BOB-A");
const deviceBobB = DeviceInfo.fromStorage({
keys: {
"curve25519:BOB-B": "bkey",
},
}, "BOB-B");
// There's no required ordering of devices per user, so here we
// create two different orderings so that each task reserves a
// device the other task needs before continuing.
const devicesByUserAB = {
"@bob:example.com": [
deviceBobA,
deviceBobB,
],
};
const devicesByUserBA = {
"@bob:example.com": [
deviceBobB,
deviceBobA,
],
};
function alwaysSucceed(promise) {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserAB,
));
// After a single tick through the first task, it should have
// claimed ownership of all devices to avoid deadlocking others.
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserBA,
));
// The second task should not have changed the ownership count, as
// it's waiting on the first task.
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
// Track the tasks, but don't await them yet.
const promises = Promise.all([
task1,
task2,
]);
await new Promise((resolve) => {
setTimeout(resolve, 200);
});
// After .2s, the first task should have made an initial claim request.
expect(claimRequestCount).toBe(1);
await promises;
// After waiting for both tasks to complete, the first task should
// have failed, so the second task should have tried to create a
// new session and will have called claimOneTimeKeys
expect(claimRequestCount).toBe(2);
});
});
});
+26
View File
@@ -282,4 +282,30 @@ describe("utils", function() {
expect(target.nonenumerableProp).toBe(undefined);
});
});
describe("chunkPromises", function() {
it("should execute promises in chunks", async function() {
let promiseCount = 0;
function fn1() {
return new Promise(async function(resolve, reject) {
await utils.sleep(1);
expect(promiseCount).toEqual(0);
++promiseCount;
resolve();
});
}
function fn2() {
return new Promise(function(resolve, reject) {
expect(promiseCount).toEqual(1);
++promiseCount;
resolve();
});
}
await utils.chunkPromises([fn1, fn2], 1);
expect(promiseCount).toEqual(2);
});
});
});
+65 -1
View File
@@ -79,6 +79,7 @@ class MockRTCPeerConnection {
return Promise.resolve();
}
close() {}
getStats() { return []; }
}
describe('Call', function() {
@@ -122,6 +123,7 @@ describe('Call', function() {
// We just stub out sendEvent: we're not interested in testing the client's
// event sending code here
client.client.sendEvent = () => {};
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
call = new MatrixCall({
client: client.client,
roomId: '!foo:bar',
@@ -138,7 +140,9 @@ describe('Call', function() {
});
it('should ignore candidate events from non-matching party ID', async function() {
await call.placeVoiceCall();
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
await call.onAnswerReceived({
getContent: () => {
return {
@@ -190,4 +194,64 @@ describe('Call', function() {
// Hangup to stop timers
call.hangup(CallErrorCode.UserHangup, true);
});
it('should add candidates received before answer if party ID is correct', async function() {
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
call.peerConn.addIceCandidate = jest.fn();
call.onRemoteIceCandidatesReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'the_correct_party_id',
candidates: [
{
candidate: 'the_correct_candidate',
sdpMid: '',
},
],
};
},
});
call.onRemoteIceCandidatesReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'some_other_party_id',
candidates: [
{
candidate: 'the_wrong_candidate',
sdpMid: '',
},
],
};
},
});
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(0);
await call.onAnswerReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'the_correct_party_id',
answer: {
sdp: DUMMY_SDP,
},
};
},
});
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
expect(call.peerConn.addIceCandidate).toHaveBeenCalledWith({
candidate: 'the_correct_candidate',
sdpMid: '',
});
});
});
+10
View File
@@ -36,6 +36,10 @@ export enum EventType {
*/
RoomAliases = "m.room.aliases", // deprecated https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
// Spaces MSC1772
SpaceChild = "org.matrix.msc1772.space.child",
SpaceParent = "org.matrix.msc1772.space.parent",
// Room timeline events
RoomRedaction = "m.room.redaction",
RoomMessage = "m.room.message",
@@ -87,3 +91,9 @@ export enum MsgType {
Location = "m.location",
Video = "m.video",
}
export const RoomCreateTypeField = "org.matrix.msc1772.type"; // Spaces MSC1772
export enum RoomType {
Space = "org.matrix.msc1772.space", // Spaces MSC1772
}
+2 -2
View File
@@ -41,7 +41,7 @@ declare global {
getUserMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
}
export interface DesktopCapturerConstraints {
interface DesktopCapturerConstraints {
audio: boolean | {
mandatory: {
chromeMediaSource: string;
@@ -56,7 +56,7 @@ declare global {
};
}
export interface DesktopCapturerSource {
interface DesktopCapturerSource {
id: string;
name: string;
thumbnailURL: string;
+63 -1
View File
@@ -246,10 +246,25 @@ MatrixBaseApis.prototype.register = function(
/**
* Register a guest account.
* This method returns the auth info needed to create a new authenticated client,
* Remember to call `setGuest(true)` on the (guest-)authenticated client, e.g:
* ```javascript
* const tmpClient = await sdk.createClient(MATRIX_INSTANCE);
* const { user_id, device_id, access_token } = tmpClient.registerGuest();
* const client = createClient({
* baseUrl: MATRIX_INSTANCE,
* accessToken: access_token,
* userId: user_id,
* deviceId: device_id,
* })
* client.setGuest(true);
* ```
*
* @param {Object=} opts Registration options
* @param {Object} opts.body JSON HTTP body to provide.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {Promise} Resolves: JSON object that contains:
* { user_id, device_id, access_token, home_server }
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
@@ -1518,6 +1533,21 @@ MatrixBaseApis.prototype.getDevices = function() {
);
};
/**
* Gets specific device details for the logged-in user
* @param {string} device_id device to query
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getDevice = function(device_id) {
const path = utils.encodeUri("/devices/$device_id", {
$device_id: device_id,
});
return this._http.authedRequest(
undefined, 'GET', path, undefined, undefined,
);
};
/**
* Update the given device
*
@@ -2374,3 +2404,35 @@ MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, score, reason)
return this._http.authedRequest(undefined, "POST", path, null, {score, reason});
};
/**
* Fetches or paginates a summary of a space as defined by MSC2946
* @param {string} roomId The ID of the space-room to use as the root of the summary.
* @param {number?} maxRoomsPerSpace The maximum number of rooms to return per subspace.
* @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true.
* @param {boolean?} autoJoinOnly Whether to only return rooms with auto_join=true.
* @param {number?} limit The maximum number of rooms to return in total.
* @param {string?} batch The opaque token to paginate a previous summary request.
* @returns {Promise} the response, with next_batch, rooms, events fields.
*/
MatrixBaseApis.prototype.getSpaceSummary = function(
roomId,
maxRoomsPerSpace,
suggestedOnly,
autoJoinOnly,
limit,
batch,
) {
const path = utils.encodeUri("/rooms/$roomId/spaces", {
$roomId: roomId,
});
return this._http.authedRequest(undefined, "POST", path, null, {
max_rooms_per_space: maxRoomsPerSpace,
suggested_only: suggestedOnly,
auto_join_only: autoJoinOnly,
limit,
batch,
}, {
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
});
};
+92 -52
View File
@@ -61,6 +61,7 @@ import {DEHYDRATION_ALGORITHM} from "./crypto/dehydration";
const SCROLLBACK_DELAY_MS = 3000;
export const CRYPTO_ENABLED = isCryptoAvailable();
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
function keysFromRecoverySession(sessions, decryptionKey, roomId) {
const keys = [];
@@ -393,6 +394,10 @@ export function MatrixClient(opts) {
this._clientWellKnown = undefined;
this._clientWellKnownPromise = undefined;
this._turnServers = [];
this._turnServersExpiry = 0;
this._checkTurnServersIntervalID = null;
// The SDK doesn't really provide a clean way for events to recalculate the push
// actions for themselves, so we have to kinda help them out when they are encrypted.
// We do this so that push rules are correctly executed on events in their decrypted
@@ -495,19 +500,8 @@ MatrixClient.prototype.rehydrateDevice = async function() {
return;
}
let getDeviceResult;
try {
getDeviceResult = await this._http.authedRequest(
undefined,
"GET",
"/dehydrated_device",
undefined, undefined,
{
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
},
);
} catch (e) {
logger.info("could not get dehydrated device", e.toString());
const getDeviceResult = this.getDehydratedDevice();
if (!getDeviceResult) {
return;
}
@@ -569,6 +563,27 @@ MatrixClient.prototype.rehydrateDevice = async function() {
}
};
/**
* Get the current dehydrated device, if any
* @return {Promise} A promise of an object containing the dehydrated device
*/
MatrixClient.prototype.getDehydratedDevice = async function() {
try {
return await this._http.authedRequest(
undefined,
"GET",
"/dehydrated_device",
undefined, undefined,
{
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
},
);
} catch (e) {
logger.info("could not get dehydrated device", e.toString());
return;
}
};
/**
* Set the dehydration key. This will also periodically dehydrate devices to
* the server.
@@ -3447,11 +3462,12 @@ MatrixClient.prototype.getRoomUpgradeHistory = function(roomId, verifyLinks=fals
* @param {string} roomId
* @param {string} userId
* @param {module:client.callback} callback Optional.
* @param {string} reason Optional.
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.invite = function(roomId, userId, callback) {
return _membershipChange(this, roomId, userId, "invite", undefined,
MatrixClient.prototype.invite = function(roomId, userId, callback, reason) {
return _membershipChange(this, roomId, userId, "invite", reason,
callback);
};
@@ -4942,6 +4958,57 @@ MatrixClient.prototype.getTurnServers = function() {
return this._turnServers || [];
};
/**
* Get the unix timestamp (in seconds) at which the current
* TURN credentials (from getTurnServers) expire
* @return {number} The expiry timestamp, in seconds, or null if no credentials
*/
MatrixClient.prototype.getTurnServersExpiry = function() {
return this._turnServersExpiry;
};
MatrixClient.prototype._checkTurnServers = async function() {
if (!this._supportsVoip) {
return;
}
let credentialsGood = false;
const remainingTime = this._turnServersExpiry - Date.now();
if (remainingTime > TURN_CHECK_INTERVAL) {
logger.debug("TURN creds are valid for another " + remainingTime + " ms: not fetching new ones.");
credentialsGood = true;
} else {
logger.debug("Fetching new TURN credentials");
try {
const res = await this.turnServer();
if (res.uris) {
logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs");
// map the response to a format that can be fed to RTCPeerConnection
const servers = {
urls: res.uris,
username: res.username,
credential: res.password,
};
this._turnServers = [servers];
// The TTL is in seconds but we work in ms
this._turnServersExpiry = Date.now() + (res.ttl * 1000);
credentialsGood = true;
}
} catch (err) {
logger.error("Failed to get TURN URIs", err);
// If we get a 403, there's no point in looping forever.
if (err.httpStatus === 403) {
logger.info("TURN access unavailable for this account: stopping credentials checks");
if (this._checkTurnServersIntervalID !== null) global.clearInterval(this._checkTurnServersIntervalID);
this._checkTurnServersIntervalID = null;
}
}
// otherwise, if we failed for whatever reason, try again the next time we're called.
}
return credentialsGood;
};
/**
* Set whether to allow a fallback ICE server should be used for negotiating a
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to
@@ -5094,7 +5161,12 @@ MatrixClient.prototype.startClient = async function(opts) {
}
// periodically poll for turn servers if we support voip
checkTurnServers(this);
if (this._supportsVoip) {
this._checkTurnServersIntervalID = setInterval(() => {
this._checkTurnServers();
}, TURN_CHECK_INTERVAL);
this._checkTurnServers();
}
if (this._syncApi) {
// This shouldn't happen since we thought the client was not running
@@ -5206,7 +5278,7 @@ MatrixClient.prototype.stopClient = function() {
this._callEventHandler = null;
}
global.clearTimeout(this._checkTurnServersTimeoutID);
global.clearInterval(this._checkTurnServersIntervalID);
if (this._clientWellKnownIntervalID !== undefined) {
global.clearInterval(this._clientWellKnownIntervalID);
}
@@ -5413,6 +5485,9 @@ async function(roomId, eventId, relationType, eventType, opts = {}) {
}));
events = events.filter(e => e.getType() === eventType);
}
if (originalEvent && relationType === "m.replace") {
events = events.filter(e => e.getSender() === originalEvent.getSender());
}
return {
originalEvent,
events,
@@ -5420,41 +5495,6 @@ async function(roomId, eventId, relationType, eventType, opts = {}) {
};
};
function checkTurnServers(client) {
if (!client._supportsVoip) {
return;
}
client.turnServer().then(function(res) {
if (res.uris) {
logger.log("Got TURN URIs: " + res.uris + " refresh in " +
res.ttl + " secs");
// map the response to a format that can be fed to
// RTCPeerConnection
const servers = {
urls: res.uris,
username: res.username,
credential: res.password,
};
client._turnServers = [servers];
// re-fetch when we're about to reach the TTL
client._checkTurnServersTimeoutID = setTimeout(() => {
checkTurnServers(client);
}, (res.ttl || (60 * 60)) * 1000 * 0.9);
}
}, function(err) {
logger.error("Failed to get TURN URIs");
// If we get a 403, there's no point in looping forever.
if (err.httpStatus === 403) {
logger.info("TURN access unavailable for this account");
return;
}
client._checkTurnServersTimeoutID = setTimeout(function() {
checkTurnServers(client);
}, 60000);
});
}
function _reject(callback, reject, err) {
if (callback) {
callback(err);
+16 -9
View File
@@ -28,7 +28,7 @@ import {DeviceInfo} from './deviceinfo';
import {CrossSigningInfo} from './CrossSigning';
import * as olmlib from './olmlib';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
import {defer, sleep} from '../utils';
import {chunkPromises, defer, sleep} from '../utils';
/* State transition diagram for DeviceList._deviceTrackingStatus
@@ -62,7 +62,7 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
* @alias module:crypto/DeviceList
*/
export class DeviceList extends EventEmitter {
constructor(baseApis, cryptoStore, olmDevice) {
constructor(baseApis, cryptoStore, olmDevice, keyDownloadChunkSize = 250) {
super();
this._cryptoStore = cryptoStore;
@@ -98,6 +98,9 @@ export class DeviceList extends EventEmitter {
// userId -> promise
this._keyDownloadsInProgressByUser = {};
// Maximum number of user IDs per request to prevent server overload (#1619)
this._keyDownloadChunkSize = keyDownloadChunkSize;
// Set whenever changes are made other than setting the sync token
this._dirty = false;
@@ -780,13 +783,17 @@ class DeviceListUpdateSerialiser {
opts.token = this._syncToken;
}
this._baseApis.downloadKeysForUsers(
downloadUsers, opts,
).then(async (res) => {
const dk = res.device_keys || {};
const masterKeys = res.master_keys || {};
const ssks = res.self_signing_keys || {};
const usks = res.user_signing_keys || {};
const factories = [];
for (let i = 0; i < downloadUsers.length; i += this._deviceList._keyDownloadChunkSize) {
const userSlice = downloadUsers.slice(i, i + this._deviceList._keyDownloadChunkSize);
factories.push(() => this._baseApis.downloadKeysForUsers(userSlice, opts));
}
chunkPromises(factories, 3).then(async (responses) => {
const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {})));
const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {})));
const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
const usks = Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
// yield to other things that want to execute in between users, to
// avoid wedging the CPU
+24 -5
View File
@@ -545,6 +545,7 @@ OlmDevice.prototype.createOutboundSession = async function(
}
});
},
logger.withPrefix("[createOutboundSession]"),
);
return newSessionId;
};
@@ -605,6 +606,7 @@ OlmDevice.prototype.createInboundSession = async function(
}
});
},
logger.withPrefix("[createInboundSession]"),
);
return result;
@@ -619,8 +621,10 @@ OlmDevice.prototype.createInboundSession = async function(
* @return {Promise<string[]>} a list of known session ids for the device
*/
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
const log = logger.withPrefix("[getSessionIdsForDevice]");
if (this._sessionsInProgress[theirDeviceIdentityKey]) {
logger.log("waiting for olm session to be created");
log.debug(`Waiting for Olm session for ${theirDeviceIdentityKey} to be created`);
try {
await this._sessionsInProgress[theirDeviceIdentityKey];
} catch (e) {
@@ -638,6 +642,7 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
},
);
},
log,
);
return sessionIds;
@@ -651,13 +656,14 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
* @param {boolean} nowait Don't wait for an in-progress session to complete.
* This should only be set to true of the calling function is the function
* that marked the session as being in-progress.
* @param {Logger} [log] A possibly customised log
* @return {Promise<?string>} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = async function(
theirDeviceIdentityKey, nowait,
theirDeviceIdentityKey, nowait, log,
) {
const sessionInfos = await this.getSessionInfoForDevice(
theirDeviceIdentityKey, nowait,
theirDeviceIdentityKey, nowait, log,
);
if (sessionInfos.length === 0) {
@@ -697,11 +703,16 @@ OlmDevice.prototype.getSessionIdForDevice = async function(
* @param {boolean} nowait Don't wait for an in-progress session to complete.
* This should only be set to true of the calling function is the function
* that marked the session as being in-progress.
* @param {Logger} [log] A possibly customised log
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
*/
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey, nowait) {
OlmDevice.prototype.getSessionInfoForDevice = async function(
deviceIdentityKey, nowait, log = logger,
) {
log = log.withPrefix("[getSessionInfoForDevice]");
if (this._sessionsInProgress[deviceIdentityKey] && !nowait) {
logger.log("waiting for olm session to be created");
log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`);
try {
await this._sessionsInProgress[deviceIdentityKey];
} catch (e) {
@@ -727,6 +738,7 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey,
}
});
},
log,
);
return info;
@@ -761,6 +773,7 @@ OlmDevice.prototype.encryptMessage = async function(
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
},
logger.withPrefix("[encryptMessage]"),
);
return res;
};
@@ -794,6 +807,7 @@ OlmDevice.prototype.decryptMessage = async function(
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
},
logger.withPrefix("[decryptMessage]"),
);
return payloadString;
};
@@ -825,6 +839,7 @@ OlmDevice.prototype.matchesSession = async function(
matches = sessionInfo.session.matches_inbound(ciphertext);
});
},
logger.withPrefix("[matchesSession]"),
);
return matches;
};
@@ -1095,6 +1110,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
},
);
},
logger.withPrefix("[addInboundGroupSession]"),
);
};
@@ -1265,6 +1281,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
},
);
},
logger.withPrefix("[decryptGroupMessage]"),
);
if (error) {
@@ -1310,6 +1327,7 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
},
);
},
logger.withPrefix("[hasInboundSessionKeys]"),
);
return result;
@@ -1369,6 +1387,7 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
},
);
},
logger.withPrefix("[getInboundGroupSessionKey]"),
);
return result;
+2 -2
View File
@@ -22,7 +22,7 @@ limitations under the License.
* @module crypto/algorithms/megolm
*/
import {getPrefixedLogger, logger} from '../../logger';
import {logger} from '../../logger';
import * as utils from "../../utils";
import {polyfillSuper} from "../../utils";
import * as olmlib from "../olmlib";
@@ -736,7 +736,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(
logger.debug(`Ensuring Olm sessions for devices in ${this._roomId}`);
const devicemap = await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers,
getPrefixedLogger(`[${this._roomId}]`),
logger.withPrefix(`[${this._roomId}]`),
);
logger.debug(`Ensured Olm sessions for devices in ${this._roomId}`);
+43 -44
View File
@@ -183,7 +183,7 @@ export async function getExistingOlmSessions(
* @param {Array} [failedServers] An array to fill with remote servers that
* failed to respond to one-time-key requests.
*
* @param {Object} [log] A possibly customised log
* @param {Logger} [log] A possibly customised log
*
* @return {Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
@@ -208,6 +208,35 @@ export async function ensureOlmSessionsForDevices(
const result = {};
const resolveSession = {};
// Mark all sessions this task intends to update as in progress. It is
// important to do this for all devices this task cares about in a single
// synchronous operation, as otherwise it is possible to have deadlocks
// where multiple tasks wait indefinitely on another task to update some set
// of common devices.
for (const [, devices] of Object.entries(devicesByUser)) {
for (const deviceInfo of devices) {
const key = deviceInfo.getIdentityKey();
if (key === olmDevice.deviceCurve25519Key) {
// We don't start sessions with ourself, so there's no need to
// mark it in progress.
continue;
}
if (!olmDevice._sessionsInProgress[key]) {
// pre-emptively mark the session as in-progress to avoid race
// conditions. If we find that we already have a session, then
// we'll resolve
olmDevice._sessionsInProgress[key] = new Promise(resolve => {
resolveSession[key] = (...args) => {
delete olmDevice._sessionsInProgress[key];
resolve(...args);
};
});
}
}
}
for (const [userId, devices] of Object.entries(devicesByUser)) {
result[userId] = {};
for (const deviceInfo of devices) {
@@ -232,41 +261,21 @@ export async function ensureOlmSessionsForDevices(
continue;
}
if (!olmDevice._sessionsInProgress[key]) {
// pre-emptively mark the session as in-progress to avoid race
// conditions. If we find that we already have a session, then
// we'll resolve
olmDevice._sessionsInProgress[key] = new Promise(
(resolve, reject) => {
resolveSession[key] = {
resolve: (...args) => {
delete olmDevice._sessionsInProgress[key];
resolve(...args);
},
reject: (...args) => {
delete olmDevice._sessionsInProgress[key];
reject(...args);
},
};
},
);
}
const forWhom = `for ${key} (${userId}:${deviceId})`;
const sessionId = await olmDevice.getSessionIdForDevice(
key, resolveSession[key],
key, resolveSession[key], log,
);
if (sessionId !== null && resolveSession[key]) {
// we found a session, but we had marked the session as
// in-progress, so unmark it and unblock anything that was
// waiting
delete olmDevice._sessionsInProgress[key];
resolveSession[key].resolve();
delete resolveSession[key];
// in-progress, so resolve it now, which will unmark it and
// unblock anything that was waiting
resolveSession[key]();
}
if (sessionId === null || force) {
if (force) {
log.info(`Forcing new Olm session for ${userId}:${deviceId}`);
log.info(`Forcing new Olm session ${forWhom}`);
} else {
log.info(`Making new Olm session for ${userId}:${deviceId}`);
log.info(`Making new Olm session ${forWhom}`);
}
devicesWithoutSession.push([userId, deviceId]);
}
@@ -284,14 +293,6 @@ export async function ensureOlmSessionsForDevices(
const oneTimeKeyAlgorithm = "signed_curve25519";
let res;
let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
// If your homeserver takes a nap here and never replies, this process
// would hang indefinitely. While that's easily fixed by setting a
// timeout on this request, let's first log whether that's the root
// cause we're seeing in practice.
// See also https://github.com/vector-im/element-web/issues/16194
const otkTimeoutLogger = setTimeout(() => {
log.error(`Homeserver never replied while claiming ${taskDetail}`);
}, otkTimeout);
try {
log.debug(`Claiming ${taskDetail}`);
res = await baseApis.claimOneTimeKeys(
@@ -300,22 +301,20 @@ export async function ensureOlmSessionsForDevices(
log.debug(`Claimed ${taskDetail}`);
} catch (e) {
for (const resolver of Object.values(resolveSession)) {
resolver.resolve();
resolver();
}
log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
throw e;
} finally {
clearTimeout(otkTimeoutLogger);
}
if (failedServers && "failures" in res) {
failedServers.push(...Object.keys(res.failures));
}
const otk_res = res.one_time_keys || {};
const otkResult = res.one_time_keys || {};
const promises = [];
for (const [userId, devices] of Object.entries(devicesByUser)) {
const userRes = otk_res[userId] || {};
const userRes = otkResult[userId] || {};
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
@@ -347,7 +346,7 @@ export async function ensureOlmSessionsForDevices(
`for device ${userId}:${deviceId}`,
);
if (resolveSession[key]) {
resolveSession[key].resolve();
resolveSession[key]();
}
continue;
}
@@ -357,12 +356,12 @@ export async function ensureOlmSessionsForDevices(
olmDevice, oneTimeKey, userId, deviceInfo,
).then((sid) => {
if (resolveSession[key]) {
resolveSession[key].resolve(sid);
resolveSession[key](sid);
}
result[userId][deviceId].sessionId = sid;
}, (e) => {
if (resolveSession[key]) {
resolveSession[key].resolve();
resolveSession[key]();
}
throw e;
}),
@@ -20,6 +20,7 @@ import {logger} from '../../logger';
import * as utils from "../../utils";
export const VERSION = 9;
const PROFILE_TRANSACTIONS = false;
/**
* Implementation of a CryptoStore which is backed by an existing
@@ -34,6 +35,7 @@ export class Backend {
*/
constructor(db) {
this._db = db;
this._nextTxnId = 0;
// make sure we close the db on `onversionchange` - otherwise
// attempts to delete the database will block (and subsequent
@@ -757,10 +759,27 @@ export class Backend {
}));
}
doTxn(mode, stores, func) {
doTxn(mode, stores, func, log = logger) {
let startTime;
let description;
if (PROFILE_TRANSACTIONS) {
const txnId = this._nextTxnId++;
startTime = Date.now();
description = `${mode} crypto store transaction ${txnId} in ${stores}`;
log.debug(`Starting ${description}`);
}
const txn = this._db.transaction(stores, mode);
const promise = promiseifyTxn(txn);
const result = func(txn);
if (PROFILE_TRANSACTIONS) {
promise.then(() => {
const elapsedTime = Date.now() - startTime;
log.debug(`Finished ${description}, took ${elapsedTime} ms`);
}, () => {
const elapsedTime = Date.now() - startTime;
log.error(`Failed ${description}, took ${elapsedTime} ms`);
});
}
return promise.then(() => {
return result;
});
+3 -2
View File
@@ -596,6 +596,7 @@ export class IndexedDBCryptoStore {
* @param {function(*)} func Function called with the
* transaction object: an opaque object that should be passed
* to store functions.
* @param {Logger} [log] A possibly customised log
* @return {Promise} Promise that resolves with the result of the `func`
* when the transaction is complete. If the backend is
* async (ie. the indexeddb backend) any of the callback
@@ -603,8 +604,8 @@ export class IndexedDBCryptoStore {
* reject with that exception. On synchronous backends, the
* exception will propagate to the caller of the getFoo method.
*/
doTxn(mode, stores, func) {
return this._backend.doTxn(mode, stores, func);
doTxn(mode, stores, func, log) {
return this._backend.doTxn(mode, stores, func, log);
}
}
@@ -179,7 +179,9 @@ export class ToDeviceChannel {
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
// the request has picked a ready or start event, tell the other devices about it
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
const nonChosenDevices = this._devices.filter(d => d !== this._deviceId);
const nonChosenDevices = this._devices.filter(
d => d !== this._deviceId && d !== this._client.getDeviceId(),
);
if (nonChosenDevices.length) {
const message = this.completeContent({
code: "m.accepted",
+1 -1
View File
@@ -271,10 +271,10 @@ MatrixHttpApi.prototype = {
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
xhr.onreadystatechange = function() {
let resp;
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
callbacks.clearTimeout(xhr.timeout_timer);
var resp;
try {
if (xhr.status === 0) {
throw new AbortError();
+14 -3
View File
@@ -59,17 +59,28 @@ log.methodFactory = function(methodName, logLevel, loggerName) {
* Drop-in replacement for <code>console</code> using {@link https://www.npmjs.com/package/loglevel|loglevel}.
* Can be tailored down to specific use cases if needed.
*/
export const logger = log.getLogger(DEFAULT_NAMESPACE);
export const logger: PrefixedLogger = log.getLogger(DEFAULT_NAMESPACE);
logger.setLevel(log.levels.DEBUG);
interface PrefixedLogger extends Logger {
prefix?: any;
withPrefix?: (prefix: string) => PrefixedLogger;
prefix?: string;
}
export function getPrefixedLogger(prefix): PrefixedLogger {
function extendLogger(logger: PrefixedLogger) {
logger.withPrefix = function(prefix: string): PrefixedLogger {
const existingPrefix = this.prefix || "";
return getPrefixedLogger(existingPrefix + prefix);
};
}
extendLogger(logger);
function getPrefixedLogger(prefix): PrefixedLogger {
const prefixLogger: PrefixedLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`);
if (prefixLogger.prefix !== prefix) {
// Only do this setup work the first time through, as loggers are saved by name.
extendLogger(prefixLogger);
prefixLogger.prefix = prefix;
prefixLogger.setLevel(log.levels.DEBUG);
}
+1 -1
View File
@@ -52,7 +52,7 @@ export * from "./store/session/webstorage";
export * from "./crypto/store/memory-crypto-store";
export * from "./crypto/store/indexeddb-crypto-store";
export * from "./content-repo";
export const ContentHelpers = import("./content-helpers");
export * as ContentHelpers from "./content-helpers";
export {
createNewMatrixCall,
setAudioOutput as setMatrixCallAudioOutput,
+5 -2
View File
@@ -290,6 +290,9 @@ RoomMember.prototype.getMxcAvatarUrl = function() {
return null;
};
const MXID_PATTERN = /@.+:.+/;
const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
function calculateDisplayName(selfUserId, displayName, roomState) {
if (!displayName || displayName === selfUserId) {
return selfUserId;
@@ -308,13 +311,13 @@ function calculateDisplayName(selfUserId, displayName, roomState) {
// Next check if the name contains something that look like a mxid
// If it does, it may be someone trying to impersonate someone else
// Show full mxid in this case
let disambiguate = /@.+:.+/.test(displayName);
let disambiguate = MXID_PATTERN.test(displayName);
if (!disambiguate) {
// Also show mxid if the display name contains any LTR/RTL characters as these
// make it very difficult for us to find similar *looking* display names
// E.g "Mark" could be cloned by writing "kraM" but in RTL.
disambiguate = /[\u200E\u200F\u202A-\u202F]/.test(displayName);
disambiguate = LTR_RTL_PATTERN.test(displayName);
}
if (!disambiguate) {
+32 -2
View File
@@ -30,7 +30,7 @@ import {RoomMember} from "./room-member";
import {RoomSummary} from "./room-summary";
import {logger} from '../logger';
import {ReEmitter} from '../ReEmitter';
import {EventType} from "../@types/event";
import {EventType, RoomCreateTypeField, RoomType} from "../@types/event";
// These constants are used as sane defaults when the homeserver doesn't support
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
@@ -818,7 +818,7 @@ Room.prototype.getBlacklistUnverifiedDevices = function() {
*/
Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
allowDefault) {
const roomAvatarEvent = this.currentState.getStateEvents("m.room.avatar", "");
const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, "");
if (allowDefault === undefined) {
allowDefault = true;
}
@@ -836,6 +836,15 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
return null;
};
/**
* Get the mxc avatar url for the room, if one was set.
* @return {string} the mxc avatar url or falsy
*/
Room.prototype.getMxcAvatarUrl = function() {
const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, "");
return roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
};
/**
* Get the aliases this room has according to the room's state
* The aliases returned by this function may not necessarily
@@ -1856,6 +1865,27 @@ Room.prototype.getJoinRule = function() {
return this.currentState.getJoinRule();
};
/**
* Returns the type of the room from the `m.room.create` event content or undefined if none is set
* @returns {?string} the type of the room. Currently only RoomType.Space is known.
*/
Room.prototype.getType = function() {
const createEvent = this.currentState.getStateEvents("m.room.create", "");
if (!createEvent) {
logger.warn("Room " + this.roomId + " does not have an m.room.create event");
return undefined;
}
return createEvent.getContent()[RoomCreateTypeField];
};
/**
* Returns whether the room is a space-room as defined by MSC1772.
* @returns {boolean} true if the room's type is RoomType.Space
*/
Room.prototype.isSpaceRoom = function() {
return this.getType() === RoomType.Space;
};
/**
* This is an internal method. Calculates the name of the room from the current
* room state.
+1 -1
View File
@@ -435,7 +435,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise} Resolves if the data was persisted.
*/
_persistSyncData: function(nextBatch, roomsData, groupsData) {
logger.log("Persisting sync data up to ", nextBatch);
logger.log("Persisting sync data up to", nextBatch);
return utils.promiseTry(() => {
const txn = this.db.transaction(["sync"], "readwrite");
const store = txn.objectStore("sync");
+2 -2
View File
@@ -51,8 +51,8 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
* sync from the server is not required. This does not reduce memory usage as all
* the data is eagerly fetched when <code>startup()</code> is called.
* <pre>
* let opts = { localStorage: window.localStorage };
* let store = new IndexedDBStore();
* let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage };
* let store = new IndexedDBStore(opts);
* await store.startup(); // load from indexed db
* let client = sdk.createClient({
* store: store,
+9
View File
@@ -745,6 +745,15 @@ export function promiseTry<T>(fn: () => T): Promise<T> {
return new Promise((resolve) => resolve(fn()));
}
// Creates and awaits all promises, running no more than `chunkSize` at the same time
export async function chunkPromises<T>(fns: (() => Promise<T>)[], chunkSize: number): Promise<T[]> {
const results: T[] = [];
for (let i = 0; i < fns.length; i += chunkSize) {
results.push(...(await Promise.all(fns.slice(i, i + chunkSize).map(fn => fn()))));
}
return results;
}
// We need to be able to access the Node.js crypto library from within the
// Matrix SDK without needing to `require("crypto")`, which will fail in
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
+241 -143
View File
@@ -174,6 +174,11 @@ export enum CallErrorCode {
SignallingFailed = 'signalling_timeout',
}
enum ConstraintsType {
Audio = "audio",
Video = "video",
}
/**
* The version field that we set in m.call.* events
*/
@@ -251,8 +256,6 @@ export class MatrixCall extends EventEmitter {
private localAVStream: MediaStream;
private inviteOrAnswerSent: boolean;
private waitForLocalAVStream: boolean;
// XXX: This is either the invite or answer from remote...
private msg: any;
// XXX: I don't know why this is called 'config'.
private config: MediaStreamConstraints;
private successor: MatrixCall;
@@ -284,6 +287,11 @@ export class MatrixCall extends EventEmitter {
private makingOffer: boolean;
private ignoreOffer: boolean;
// If candidates arrive before we've picked an opponent (which, in particular,
// will happen if the opponent sends candidates eagerly before the user answers
// the call) we buffer them up here so we can then add the ones from the party we pick
private remoteCandidateBuffer = new Map<string, RTCIceCandidate[]>();
constructor(opts: CallOpts) {
super();
this.roomId = opts.roomId;
@@ -291,9 +299,6 @@ export class MatrixCall extends EventEmitter {
this.type = null;
this.forceTURN = opts.forceTURN;
this.ourPartyId = this.client.deviceId;
// We compare this to null to checks the presence of a party ID:
// make sure it's null, not undefined
this.opponentPartyId = null;
// Array of Objects with urls, username, credential keys
this.turnServers = opts.turnServers || [];
if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
@@ -328,10 +333,11 @@ export class MatrixCall extends EventEmitter {
* Place a voice call to this room.
* @throws If you have not specified a listener for 'error' events.
*/
placeVoiceCall() {
async placeVoiceCall() {
logger.debug("placeVoiceCall");
this.checkForErrorListener();
this.placeCallWithConstraints(getUserMediaVideoContraints(CallType.Voice));
const constraints = getUserMediaContraints(ConstraintsType.Audio);
await this.placeCallWithConstraints(constraints);
this.type = CallType.Voice;
}
@@ -343,12 +349,13 @@ export class MatrixCall extends EventEmitter {
* to render the local camera preview.
* @throws If you have not specified a listener for 'error' events.
*/
placeVideoCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
async placeVideoCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
logger.debug("placeVideoCall");
this.checkForErrorListener();
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
this.placeCallWithConstraints(getUserMediaVideoContraints(CallType.Video));
const constraints = getUserMediaContraints(ConstraintsType.Video);
await this.placeCallWithConstraints(constraints);
this.type = CallType.Video;
}
@@ -365,61 +372,37 @@ export class MatrixCall extends EventEmitter {
async placeScreenSharingCall(
remoteVideoElement: HTMLVideoElement,
localVideoElement: HTMLVideoElement,
selectDesktopCapturerSource: () => Promise<DesktopCapturerSource>,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
) {
logger.debug("placeScreenSharingCall");
this.checkForErrorListener();
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
if (window.electron?.getDesktopCapturerSources) {
// We have access to getDesktopCapturerSources()
logger.debug("Electron getDesktopCapturerSources() is available...");
try {
const selectedSource = await selectDesktopCapturerSource();
// If no source was selected cancel call
if (!selectedSource) return;
const getUserMediaOptions: MediaStreamConstraints | DesktopCapturerConstraints = {
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: selectedSource.id,
},
},
}
this.screenSharingStream = await window.navigator.mediaDevices.getUserMedia(getUserMediaOptions);
try {
const screenshareConstraints = await getScreenshareContraints(selectDesktopCapturerSource);
if (!screenshareConstraints) return;
if (window.electron?.getDesktopCapturerSources) {
// We are using Electron
logger.debug("Getting screen stream using getUserMedia()...");
this.screenSharingStream = await navigator.mediaDevices.getUserMedia(screenshareConstraints);
} else {
// We are not using Electron
logger.debug("Getting screen stream using getDisplayMedia()...");
this.screenSharingStream = await navigator.mediaDevices.getDisplayMedia(screenshareConstraints);
}
logger.debug("Got screen stream, requesting audio stream...");
const audioConstraints = getUserMediaVideoContraints(CallType.Voice);
this.placeCallWithConstraints(audioConstraints);
} catch (err) {
this.emit(CallEvent.Error,
new CallError(
CallErrorCode.NoUserMedia,
"Failed to get screen-sharing stream: ", err,
),
);
}
} else {
/* We do not have access to the Electron desktop capturer,
* therefore we can assume we are on the web */
logger.debug("Electron desktopCapturer is not available...");
try {
this.screenSharingStream = await navigator.mediaDevices.getDisplayMedia({'audio': false});
logger.debug("Got screen stream, requesting audio stream...");
const audioConstraints = getUserMediaVideoContraints(CallType.Voice);
this.placeCallWithConstraints(audioConstraints);
} catch (err) {
this.emit(CallEvent.Error,
new CallError(
CallErrorCode.NoUserMedia,
"Failed to get screen-sharing stream: ", err,
),
);
}
logger.debug("Got screen stream, requesting audio stream...");
const audioConstraints = getUserMediaContraints(ConstraintsType.Audio);
this.placeCallWithConstraints(audioConstraints);
} catch (err) {
this.emit(CallEvent.Error,
new CallError(
CallErrorCode.NoUserMedia,
"Failed to get screen-sharing stream: ", err,
),
);
}
this.type = CallType.Video;
}
@@ -541,11 +524,24 @@ export class MatrixCall extends EventEmitter {
* @param {MatrixEvent} event The m.call.invite event
*/
async initWithInvite(event: MatrixEvent) {
this.msg = event.getContent();
const invite = event.getContent();
this.direction = CallDirection.Inbound;
// make sure we have valid turn creds. Unless something's gone wrong, it should
// poll and keep the credentials valid so this should be instant.
const haveTurnCreds = await this.client._checkTurnServers();
if (!haveTurnCreds) {
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
}
this.peerConn = this.createPeerConnection();
// we must set the party ID before await-ing on anything: the call event
// handler will start giving us more call events (eg. candidates) so if
// we haven't set the party ID, we'll ignore them.
this.chooseOpponent(event);
try {
await this.peerConn.setRemoteDescription(this.msg.offer);
await this.peerConn.setRemoteDescription(invite.offer);
await this.addBufferedIceCandidates();
} catch (e) {
logger.debug("Failed to set remote description", e);
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
@@ -564,13 +560,6 @@ export class MatrixCall extends EventEmitter {
this.type = this.remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
this.setState(CallState.Ringing);
this.opponentVersion = this.msg.version;
if (this.opponentVersion !== 0) {
// ignore party ID in v0 calls: party ID isn't a thing until v1
this.opponentPartyId = this.msg.party_id || null;
}
this.opponentCaps = this.msg.capabilities || {};
this.opponentMember = event.sender;
if (event.getLocalAge()) {
setTimeout(() => {
@@ -584,7 +573,7 @@ export class MatrixCall extends EventEmitter {
}
this.emit(CallEvent.Hangup);
}
}, this.msg.lifetime - event.getLocalAge());
}, invite.lifetime - event.getLocalAge());
}
}
@@ -596,7 +585,6 @@ export class MatrixCall extends EventEmitter {
// perverse as it may seem, sometimes we want to instantiate a call with a
// hangup message (because when getting the state of the room on load, events
// come in reverse order and we want to remember that a call has been hung up)
this.msg = event.getContent();
this.setState(CallState.Ended);
}
@@ -611,7 +599,11 @@ export class MatrixCall extends EventEmitter {
logger.debug(`Answering call ${this.callId} of type ${this.type}`);
if (!this.localAVStream && !this.waitForLocalAVStream) {
const constraints = getUserMediaVideoContraints(this.type);
const constraints = getUserMediaContraints(
this.type == CallType.Video ?
ConstraintsType.Video:
ConstraintsType.Audio,
);
logger.log("Getting user media with constraints", constraints);
this.setState(CallState.WaitLocalMedia);
this.waitForLocalAVStream = true;
@@ -668,6 +660,8 @@ export class MatrixCall extends EventEmitter {
logger.debug("Ending call " + this.callId);
this.terminate(CallParty.Local, reason, !suppressEvent);
// We don't want to send hangup here if we didn't even get to sending an invite
if (this.state === CallState.WaitLocalMedia) return;
const content = {};
// Continue to send no reason for user hangups temporarily, until
// clients understand the user_hangup reason (voip v1)
@@ -841,8 +835,11 @@ export class MatrixCall extends EventEmitter {
return;
}
if (this.callHasEnded()) {
this.stopAllMedia();
return;
}
this.localAVStream = stream;
logger.info("Got local AV stream with id " + this.localAVStream.id);
this.setState(CallState.CreateOffer);
@@ -868,11 +865,8 @@ export class MatrixCall extends EventEmitter {
}
}
this.localAVStream = stream;
logger.info("Got local AV stream with id " + this.localAVStream.id);
// why do we enable audio (and only audio) tracks here? -- matthew
setTracksEnabled(stream.getAudioTracks(), true);
this.peerConn = this.createPeerConnection();
for (const audioTrack of stream.getAudioTracks()) {
logger.info("Adding audio track with id " + audioTrack.id);
@@ -886,7 +880,7 @@ export class MatrixCall extends EventEmitter {
// Now we wait for the negotiationneeded event
};
private sendAnswer() {
private async sendAnswer() {
const answerContent = {
answer: {
sdp: this.peerConn.localDescription.sdp,
@@ -908,12 +902,12 @@ export class MatrixCall extends EventEmitter {
logger.info(`Discarding ${this.candidateSendQueue.length} candidates that will be sent in answer`);
this.candidateSendQueue = [];
this.sendVoipEvent(EventType.CallAnswer, answerContent).then(() => {
try {
await this.sendVoipEvent(EventType.CallAnswer, answerContent);
// If this isn't the first time we've tried to send the answer,
// we may have candidates queued up, so send them now.
this.inviteOrAnswerSent = true;
this.sendCandidateQueue();
}).catch((error) => {
} catch (error) {
// We've failed to answer: back to the ringing state
this.setState(CallState.Ringing);
this.client.cancelPendingEvent(error.event);
@@ -926,7 +920,11 @@ export class MatrixCall extends EventEmitter {
}
this.emit(CallEvent.Error, new CallError(code, message, error));
throw error;
});
}
// error handler re-throws so this won't happen on error, but
// we don't want the same error handling on the candidate queue
this.sendCandidateQueue();
}
private gotUserMediaForAnswer = async (stream: MediaStream) => {
@@ -990,7 +988,7 @@ export class MatrixCall extends EventEmitter {
private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent) => {
if (event.candidate) {
logger.debug(
"Got local ICE " + event.candidate.sdpMid + " candidate: " +
"Call " + this.callId + " got local ICE " + event.candidate.sdpMid + " candidate: " +
event.candidate.candidate,
);
@@ -1024,43 +1022,39 @@ export class MatrixCall extends EventEmitter {
}
};
onRemoteIceCandidatesReceived(ev: MatrixEvent) {
async onRemoteIceCandidatesReceived(ev: MatrixEvent) {
if (this.callHasEnded()) {
//debuglog("Ignoring remote ICE candidate because call has ended");
return;
}
if (!this.partyIdMatches(ev.getContent())) {
logger.info(
`Ignoring candidates from party ID ${ev.getContent().party_id}: ` +
`we have chosen party ID ${this.opponentPartyId}`,
);
return;
}
const cands = ev.getContent().candidates;
if (!cands) {
logger.info("Ignoring candidates event with no candidates!");
return;
}
for (const cand of cands) {
if (
(cand.sdpMid === null || cand.sdpMid === undefined) &&
(cand.sdpMLineIndex === null || cand.sdpMLineIndex === undefined)
) {
logger.debug("Ignoring remote ICE candidate with no sdpMid or sdpMLineIndex");
return;
}
logger.debug("Got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
try {
this.peerConn.addIceCandidate(cand);
} catch (err) {
if (!this.ignoreOffer) {
logger.info("Failed to add remore ICE candidate", err);
}
}
const fromPartyId = ev.getContent().version === 0 ? null : ev.getContent().party_id || null;
if (this.opponentPartyId === undefined) {
// we haven't picked an opponent yet so save the candidates
logger.info(`Bufferring ${cands.length} candidates until we pick an opponent`);
const bufferedCands = this.remoteCandidateBuffer.get(fromPartyId) || [];
bufferedCands.push(...cands);
this.remoteCandidateBuffer.set(fromPartyId, bufferedCands);
return;
}
if (!this.partyIdMatches(ev.getContent())) {
logger.info(
`Ignoring candidates from party ID ${ev.getContent().party_id}: ` +
`we have chosen party ID ${this.opponentPartyId}`,
);
return;
}
await this.addIceCandidates(cands);
}
/**
@@ -1068,11 +1062,14 @@ export class MatrixCall extends EventEmitter {
* @param {Object} msg
*/
async onAnswerReceived(event: MatrixEvent) {
logger.debug(`Got answer for call ID ${this.callId} from party ID ${event.getContent().party_id}`);
if (this.callHasEnded()) {
logger.debug(`Ignoring answer because call ID ${this.callId} has ended`);
return;
}
if (this.opponentPartyId !== null) {
if (this.opponentPartyId !== undefined) {
logger.info(
`Ignoring answer from party ID ${event.getContent().party_id}: ` +
`we already have an answer/reject from ${this.opponentPartyId}`,
@@ -1080,12 +1077,8 @@ export class MatrixCall extends EventEmitter {
return;
}
this.opponentVersion = event.getContent().version;
if (this.opponentVersion !== 0) {
this.opponentPartyId = event.getContent().party_id || null;
}
this.opponentCaps = event.getContent().capabilities || {};
this.opponentMember = event.sender;
this.chooseOpponent(event);
await this.addBufferedIceCandidates();
this.setState(CallState.Connecting);
@@ -1260,19 +1253,9 @@ export class MatrixCall extends EventEmitter {
try {
await this.sendVoipEvent(eventType, content);
this.sendCandidateQueue();
if (this.state === CallState.CreateOffer) {
this.inviteOrAnswerSent = true;
this.setState(CallState.InviteSent);
this.inviteTimeout = setTimeout(() => {
this.inviteTimeout = null;
if (this.state === CallState.InviteSent) {
this.hangup(CallErrorCode.InviteTimeout, false);
}
}, CALL_TIMEOUT_MS);
}
} catch (error) {
this.client.cancelPendingEvent(error.event);
logger.error("Failed to send invite", error);
if (error.event) this.client.cancelPendingEvent(error.event);
let code = CallErrorCode.SignallingFailed;
let message = "Signalling failed";
@@ -1287,6 +1270,22 @@ export class MatrixCall extends EventEmitter {
this.emit(CallEvent.Error, new CallError(code, message, error));
this.terminate(CallParty.Local, code, false);
// no need to carry on & send the candidate queue, but we also
// don't want to rethrow the error
return;
}
this.sendCandidateQueue();
if (this.state === CallState.CreateOffer) {
this.inviteOrAnswerSent = true;
this.setState(CallState.InviteSent);
this.inviteTimeout = setTimeout(() => {
this.inviteTimeout = null;
if (this.state === CallState.InviteSent) {
this.hangup(CallErrorCode.InviteTimeout, false);
}
}, CALL_TIMEOUT_MS);
}
};
@@ -1623,7 +1622,7 @@ export class MatrixCall extends EventEmitter {
}
}
private sendCandidateQueue() {
private async sendCandidateQueue() {
if (this.candidateSendQueue.length === 0) {
return;
}
@@ -1635,20 +1634,28 @@ export class MatrixCall extends EventEmitter {
candidates: cands,
};
logger.debug("Attempting to send " + cands.length + " candidates");
this.sendVoipEvent(EventType.CallCandidates, content).then(() => {
this.candidateSendTries = 0;
this.sendCandidateQueue();
}, (error) => {
for (let i = 0; i < cands.length; i++) {
this.candidateSendQueue.push(cands[i]);
}
try {
await this.sendVoipEvent(EventType.CallCandidates, content);
} catch (error) {
// don't retry this event: we'll send another one later as we might
// have more candidates by then.
if (error.event) this.client.cancelPendingEvent(error.event);
// put all the candidates we failed to send back in the queue
this.candidateSendQueue.push(...cands);
if (this.candidateSendTries > 5) {
logger.debug(
"Failed to send candidates on attempt " + this.candidateSendTries +
". Giving up for now.", error,
". Giving up on this call.", error,
);
this.candidateSendTries = 0;
const code = CallErrorCode.SignallingFailed;
const message = "Signalling failed";
this.emit(CallEvent.Error, new CallError(code, message, error));
this.hangup(code, false);
return;
}
@@ -1658,7 +1665,7 @@ export class MatrixCall extends EventEmitter {
setTimeout(() => {
this.sendCandidateQueue();
}, delayMs);
});
}
}
private async placeCallWithConstraints(constraints: MediaStreamConstraints) {
@@ -1668,11 +1675,18 @@ export class MatrixCall extends EventEmitter {
this.setState(CallState.WaitLocalMedia);
this.direction = CallDirection.Outbound;
this.config = constraints;
// It would be really nice if we could start gathering candidates at this point
// so the ICE agent could be gathering while we open our media devices: we already
// know the type of the call and therefore what tracks we want to send.
// Perhaps we could do this by making fake tracks now and then using replaceTrack()
// once we have the actual tracks? (Can we make fake tracks?)
// make sure we have valid turn creds. Unless something's gone wrong, it should
// poll and keep the credentials valid so this should be instant.
const haveTurnCreds = await this.client._checkTurnServers();
if (!haveTurnCreds) {
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
}
// create the peer connection now so it can be gathering candidates while we get user
// media (assuming a candidate pool size is configured)
this.peerConn = this.createPeerConnection();
try {
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
this.gotUserMediaForInvite(mediaStream);
@@ -1702,9 +1716,64 @@ export class MatrixCall extends EventEmitter {
private partyIdMatches(msg): boolean {
// They must either match or both be absent (in which case opponentPartyId will be null)
const msgPartyId = msg.party_id || null;
// Also we ignore party IDs on the invite/offer if the version is 0, so we must do the same
// here and use null if the version is 0 (woe betide any opponent sending messages in the
// same call with different versions)
const msgPartyId = msg.version === 0 ? null : msg.party_id || null;
return msgPartyId === this.opponentPartyId;
}
// Commits to an opponent for the call
// ev: An invite or answer event
private chooseOpponent(ev: MatrixEvent) {
// I choo-choo-choose you
const msg = ev.getContent();
logger.debug(`Choosing party ID ${msg.party_id} for call ID ${this.callId}`);
this.opponentVersion = msg.version;
if (this.opponentVersion === 0) {
// set to null to indicate that we've chosen an opponent, but because
// they're v0 they have no party ID (even if they sent one, we're ignoring it)
this.opponentPartyId = null;
} else {
// set to their party ID, or if they're naughty and didn't send one despite
// not being v0, set it to null to indicate we picked an opponent with no
// party ID
this.opponentPartyId = msg.party_id || null;
}
this.opponentCaps = msg.capabilities || {};
this.opponentMember = ev.sender;
}
private async addBufferedIceCandidates() {
const bufferedCands = this.remoteCandidateBuffer.get(this.opponentPartyId);
if (bufferedCands) {
logger.info(`Adding ${bufferedCands.length} buffered candidates for opponent ${this.opponentPartyId}`);
await this.addIceCandidates(bufferedCands);
}
this.remoteCandidateBuffer = null;
}
private async addIceCandidates(cands: RTCIceCandidate[]) {
for (const cand of cands) {
if (
(cand.sdpMid === null || cand.sdpMid === undefined) &&
(cand.sdpMLineIndex === null || cand.sdpMLineIndex === undefined)
) {
logger.debug("Ignoring remote ICE candidate with no sdpMid or sdpMLineIndex");
continue;
}
logger.debug("Call " + this.callId + " got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
try {
await this.peerConn.addIceCandidate(cand);
} catch (err) {
if (!this.ignoreOffer) {
logger.info("Failed to add remote ICE candidate", err);
}
}
}
}
}
function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {
@@ -1713,17 +1782,19 @@ function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {
}
}
function getUserMediaVideoContraints(callType: CallType) {
function getUserMediaContraints(type: ConstraintsType) {
const isWebkit = !!navigator.webkitGetUserMedia;
switch (callType) {
case CallType.Voice:
switch (type) {
case ConstraintsType.Audio: {
return {
audio: {
deviceId: audioInput ? {ideal: audioInput} : undefined,
}, video: false,
},
video: false,
};
case CallType.Video:
}
case ConstraintsType.Video: {
return {
audio: {
deviceId: audioInput ? {ideal: audioInput} : undefined,
@@ -1738,6 +1809,33 @@ function getUserMediaVideoContraints(callType: CallType) {
height: isWebkit ? { exact: 360 } : { ideal: 360 },
},
};
}
}
}
async function getScreenshareContraints(selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>) {
if (window.electron?.getDesktopCapturerSources && selectDesktopCapturerSource) {
// We have access to getDesktopCapturerSources()
logger.debug("Electron getDesktopCapturerSources() is available...");
const selectedSource = await selectDesktopCapturerSource();
if (!selectedSource) return null;
return {
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: selectedSource.id,
},
},
};
} else {
// We do not have access to the Electron desktop capturer,
// therefore we can assume we are on the web
logger.debug("Electron desktopCapturer is not available...");
return {
audio: false,
video: true,
};
}
}
+2
View File
@@ -138,6 +138,8 @@ export class CallEventHandler {
);
}
const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now();
logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
call = createNewMatrixCall(this.client, event.getRoomId(), {
forceTURN: this.client._forceTURN,
});
+25 -20
View File
@@ -1876,10 +1876,10 @@ bluebird@^3.5.0, bluebird@^3.7.2:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
version "4.11.9"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
bn.js@^5.0.0, bn.js@^5.1.1:
version "5.1.3"
@@ -1922,7 +1922,7 @@ braces@^3.0.1, braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
brorand@^1.0.1:
brorand@^1.0.1, brorand@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
@@ -2796,17 +2796,17 @@ electron-to-chromium@^1.3.634:
integrity sha512-cev+jOrz/Zm1i+Yh334Hed6lQVOkkemk2wRozfMF4MtTR7pxf3r3L5Rbd7uX1zMcEqVJ7alJBnJL7+JffkC6FQ==
elliptic@^6.5.3:
version "6.5.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
dependencies:
bn.js "^4.4.0"
brorand "^1.0.1"
bn.js "^4.11.9"
brorand "^1.1.0"
hash.js "^1.0.0"
hmac-drbg "^1.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
hmac-drbg "^1.0.1"
inherits "^2.0.4"
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
emittery@^0.7.1:
version "0.7.2"
@@ -3820,7 +3820,7 @@ he@^1.1.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hmac-drbg@^1.0.0:
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
@@ -5069,11 +5069,16 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4:
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
lodash@^4.17.4:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loglevel@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
@@ -5255,7 +5260,7 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
@@ -5917,9 +5922,9 @@ pug-attrs@^2.0.4:
pug-runtime "^2.0.5"
pug-code-gen@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-2.0.2.tgz#ad0967162aea077dcf787838d94ed14acb0217c2"
integrity sha512-kROFWv/AHx/9CRgoGJeRSm+4mLWchbgpRzTEn8XCiwwOy6Vh0gAClS8Vh5TEJ9DBjaP8wCjS3J6HKsEsYdvaCw==
version "2.0.3"
resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-2.0.3.tgz#122eb9ada9b5bf601705fe15aaa0a7d26bc134ab"
integrity sha512-r9sezXdDuZJfW9J91TN/2LFbiqDhmltTFmGpHTsGdrNGp3p4SxAjjXEfnuK2e4ywYsRIVP0NeLbSAMHUcaX1EA==
dependencies:
constantinople "^3.1.2"
doctypes "^1.1.0"