Compare commits

..

4 Commits

Author SHA1 Message Date
David Baker a600432946 v0.14.3 2019-01-22 10:41:09 +00:00
David Baker cde77cfc92 Prepare changelog for v0.14.3 2019-01-22 10:41:08 +00:00
David Baker d1f8cbaade v0.14.3-rc.1 2019-01-17 10:44:15 +00:00
David Baker 5f29cd2c1c Prepare changelog for v0.14.3-rc.1 2019-01-17 10:44:14 +00:00
105 changed files with 8176 additions and 12428 deletions
-2
View File
@@ -1,8 +1,6 @@
{
"presets": ["es2015"],
"plugins": [
"transform-class-properties",
// this transforms async functions into generator functions, which
// are then made to use the regenerator module by babel's
// transform-regnerator plugin (which is enabled by es2015).
-34
View File
@@ -1,34 +0,0 @@
steps:
- label: ":eslint: Lint"
command:
- "yarn install"
- "yarn lint"
plugins:
- docker#v3.0.1:
image: "node:10"
- label: ":karma: Tests"
command:
- "yarn install"
- "yarn test"
plugins:
- docker#v3.0.1:
image: "node:10"
- label: "📃 Docs"
command:
- "yarn install"
- "yarn gendoc"
plugins:
- docker#v3.0.1:
image: "node:10"
- wait
- label: "🐴 Trigger matrix-react-sdk"
trigger: "matrix-react-sdk"
branches: "develop"
build:
branch: "develop"
message: "[js-sdk] ${BUILDKITE_MESSAGE}"
async: true
+1 -11
View File
@@ -1,6 +1,6 @@
module.exports = {
parser: "babel-eslint", // now needed for class properties
parserOptions: {
ecmaVersion: 2017,
sourceType: "module",
ecmaFeatures: {
}
@@ -14,9 +14,6 @@ module.exports = {
es6: true,
},
extends: ["eslint:recommended", "google"],
plugins: [
"babel",
],
rules: {
// rules we've always adhered to or now do
"max-len": ["error", {
@@ -53,7 +50,6 @@ module.exports = {
// rules we do not want from the google styleguide
"object-curly-spacing": ["off"],
"spaced-comment": ["off"],
"guard-for-in": ["off"],
// in principle we prefer single quotes, but life is too short
quotes: ["off"],
@@ -76,11 +72,5 @@ module.exports = {
"named": "never",
"asyncArrow": "always",
}],
"arrow-parens": "off",
// eslint's built in no-invalid-this rule breaks with class properties
"no-invalid-this": "off",
// so we replace it with a version that is class property aware
"babel/no-invalid-this": "error",
}
}
-2
View File
@@ -1,2 +0,0 @@
patreon: matrixdotorg
liberapay: matrixdotorg
+1 -4
View File
@@ -2,9 +2,6 @@
/.jsdoc
node_modules
/.npmrc
/*.log
package-lock.json
.lock-wscript
build/Release
coverage
@@ -15,6 +12,6 @@ reports
/lib
/specbuild
# version file and tarball created by `npm pack` / `yarn pack`
# version file and tarball created by 'npm pack'
/git-revision.txt
/matrix-js-sdk-*.tgz
+5
View File
@@ -0,0 +1,5 @@
language: node_js
node_js:
- "10.11.0"
script:
- ./travis.sh
+1 -541
View File
@@ -1,543 +1,3 @@
Changes in [2.4.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.2-rc.1) (2019-10-09)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.1...v2.4.2-rc.1)
* Log state of Olm sessions
[\#1047](https://github.com/matrix-org/matrix-js-sdk/pull/1047)
* Add method to get access to all timelines
[\#1048](https://github.com/matrix-org/matrix-js-sdk/pull/1048)
Changes in [2.4.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.1) (2019-10-01)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.0...v2.4.1)
* Upgrade deps
[\#1046](https://github.com/matrix-org/matrix-js-sdk/pull/1046)
* Ignore crypto events with no content
[\#1043](https://github.com/matrix-org/matrix-js-sdk/pull/1043)
Changes in [2.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.0) (2019-09-27)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.0-rc.1...v2.4.0)
* Clean Yarn cache during release
[\#1045](https://github.com/matrix-org/matrix-js-sdk/pull/1045)
Changes in [2.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.0-rc.1) (2019-09-25)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.2...v2.4.0-rc.1)
* Remove id_server from creds for interactive auth
[\#1044](https://github.com/matrix-org/matrix-js-sdk/pull/1044)
* Remove IS details from requestToken to HS
[\#1041](https://github.com/matrix-org/matrix-js-sdk/pull/1041)
* Add support for sending MSISDN tokens to alternate URLs
[\#1040](https://github.com/matrix-org/matrix-js-sdk/pull/1040)
* Add separate 3PID add and bind APIs
[\#1038](https://github.com/matrix-org/matrix-js-sdk/pull/1038)
* Bump eslint-utils from 1.4.0 to 1.4.2
[\#1037](https://github.com/matrix-org/matrix-js-sdk/pull/1037)
* Handle WebRTC security errors as non-fatal
[\#1036](https://github.com/matrix-org/matrix-js-sdk/pull/1036)
* Check for r0.6.0 support in addition to unstable feature flags
[\#1035](https://github.com/matrix-org/matrix-js-sdk/pull/1035)
* Update room members on member event redaction
[\#1030](https://github.com/matrix-org/matrix-js-sdk/pull/1030)
* Support hidden read receipts
[\#1028](https://github.com/matrix-org/matrix-js-sdk/pull/1028)
* Do 3pid lookups in lowercase
[\#1029](https://github.com/matrix-org/matrix-js-sdk/pull/1029)
* Add Synapse admin functions for deactivating a user
[\#1027](https://github.com/matrix-org/matrix-js-sdk/pull/1027)
* Fix addPendingEvent with pending event order == chronological
[\#1026](https://github.com/matrix-org/matrix-js-sdk/pull/1026)
* Add AutoDiscovery.getRawClientConfig() for easy .well-known lookups
[\#1024](https://github.com/matrix-org/matrix-js-sdk/pull/1024)
* Don't convert errors to JSON if they are JSON already
[\#1025](https://github.com/matrix-org/matrix-js-sdk/pull/1025)
* Send id_access_token to HS for use in proxied IS requests
[\#1022](https://github.com/matrix-org/matrix-js-sdk/pull/1022)
* Clean up JSON handling in identity server requests
[\#1023](https://github.com/matrix-org/matrix-js-sdk/pull/1023)
* Use the v2 (hashed) lookup for identity server queries
[\#1021](https://github.com/matrix-org/matrix-js-sdk/pull/1021)
* Add getIdServer() & doesServerRequireIdServerParam()
[\#1018](https://github.com/matrix-org/matrix-js-sdk/pull/1018)
* Make requestToken endpoints work without ID Server
[\#1019](https://github.com/matrix-org/matrix-js-sdk/pull/1019)
* Fix setIdentityServer
[\#1016](https://github.com/matrix-org/matrix-js-sdk/pull/1016)
* Change ICE fallback server and make fallback opt-in
[\#1015](https://github.com/matrix-org/matrix-js-sdk/pull/1015)
* Throw an exception if trying to do an ID server request with no ID server
[\#1014](https://github.com/matrix-org/matrix-js-sdk/pull/1014)
* Add setIdentityServerUrl
[\#1013](https://github.com/matrix-org/matrix-js-sdk/pull/1013)
* Add matrix base API to report an event
[\#1011](https://github.com/matrix-org/matrix-js-sdk/pull/1011)
* Fix POST body for v2 IS requests
[\#1010](https://github.com/matrix-org/matrix-js-sdk/pull/1010)
* Add API for bulk lookup on the Identity Server
[\#1009](https://github.com/matrix-org/matrix-js-sdk/pull/1009)
* Remove deprecated authedRequestWithPrefix and requestWithPrefix
[\#1000](https://github.com/matrix-org/matrix-js-sdk/pull/1000)
* Add API for checking IS account info
[\#1007](https://github.com/matrix-org/matrix-js-sdk/pull/1007)
* Support rewriting push rules when our internal defaults change
[\#1006](https://github.com/matrix-org/matrix-js-sdk/pull/1006)
* Upgrade dependencies
[\#1005](https://github.com/matrix-org/matrix-js-sdk/pull/1005)
Changes in [2.3.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.2) (2019-09-16)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.2-rc.1...v2.3.2)
* [Release] Fix addPendingEvent with pending event order == chronological
[\#1034](https://github.com/matrix-org/matrix-js-sdk/pull/1034)
Changes in [2.3.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.2-rc.1) (2019-09-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.1...v2.3.2-rc.1)
* Synapse admin functions to release
[\#1033](https://github.com/matrix-org/matrix-js-sdk/pull/1033)
* [To Release] Add matrix base API to report an event
[\#1032](https://github.com/matrix-org/matrix-js-sdk/pull/1032)
Changes in [2.3.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.1) (2019-09-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.1-rc.1...v2.3.1)
* No changes since rc.1
Changes in [2.3.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.1-rc.1) (2019-09-11)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.0...v2.3.1-rc.1)
* Update room members on member event redaction
[\#1031](https://github.com/matrix-org/matrix-js-sdk/pull/1031)
Changes in [2.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.0) (2019-08-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.0-rc.1...v2.3.0)
* [release] Support rewriting push rules when our internal defaults change
[\#1008](https://github.com/matrix-org/matrix-js-sdk/pull/1008)
Changes in [2.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.0-rc.1) (2019-07-31)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.2.0...v2.3.0-rc.1)
* Add support for IS v2 API with authentication
[\#1002](https://github.com/matrix-org/matrix-js-sdk/pull/1002)
* Tombstone bugfixes
[\#1001](https://github.com/matrix-org/matrix-js-sdk/pull/1001)
* Support for MSC2140 (terms of service for IS/IM)
[\#988](https://github.com/matrix-org/matrix-js-sdk/pull/988)
* Add a request method to /devices
[\#994](https://github.com/matrix-org/matrix-js-sdk/pull/994)
Changes in [2.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.2.0) (2019-07-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.2.0-rc.2...v2.2.0)
* Upgrade lodash dependencies
Changes in [2.2.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.2.0-rc.2) (2019-07-12)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.2.0-rc.1...v2.2.0-rc.2)
* Fix regression from 2.2.0-rc.1 in request to /devices
[\#995](https://github.com/matrix-org/matrix-js-sdk/pull/995)
Changes in [2.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.2.0-rc.1) (2019-07-12)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.1.1...v2.2.0-rc.1)
* End the verification timer when verification is done
[\#993](https://github.com/matrix-org/matrix-js-sdk/pull/993)
* Stabilize usage of stably stable APIs (in a stable way)
[\#990](https://github.com/matrix-org/matrix-js-sdk/pull/990)
* Expose original_event for /relations
[\#987](https://github.com/matrix-org/matrix-js-sdk/pull/987)
* Process ephemeral events outside timeline handling
[\#989](https://github.com/matrix-org/matrix-js-sdk/pull/989)
* Don't accept any locally known edits earlier than the last known server-side
aggregated edit
[\#986](https://github.com/matrix-org/matrix-js-sdk/pull/986)
* Get edit date transparently from server aggregations or local echo
[\#984](https://github.com/matrix-org/matrix-js-sdk/pull/984)
* Add a function to flag keys for backup without scheduling a backup
[\#982](https://github.com/matrix-org/matrix-js-sdk/pull/982)
* Block read marker and read receipt from advancing into pending events
[\#981](https://github.com/matrix-org/matrix-js-sdk/pull/981)
* Upgrade dependencies
[\#977](https://github.com/matrix-org/matrix-js-sdk/pull/977)
* Add default push rule to ignore reactions
[\#976](https://github.com/matrix-org/matrix-js-sdk/pull/976)
* Fix exception whilst syncing
[\#979](https://github.com/matrix-org/matrix-js-sdk/pull/979)
* Include the error object when raising Session.logged_out
[\#975](https://github.com/matrix-org/matrix-js-sdk/pull/975)
Changes in [2.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.1.1) (2019-07-11)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.1.0...v2.1.1)
* Process emphemeral events outside timeline handling
[\#989](https://github.com/matrix-org/matrix-js-sdk/pull/989)
Changes in [2.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.1.0) (2019-07-08)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.1.0-rc.1...v2.1.0)
* Fix exception whilst syncing
[\#979](https://github.com/matrix-org/matrix-js-sdk/pull/979)
Changes in [2.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.1.0-rc.1) (2019-07-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.0.1...v2.1.0-rc.1)
* Handle self read receipts for fixing e2e notification counts
[\#974](https://github.com/matrix-org/matrix-js-sdk/pull/974)
* Add redacts field to event.toJSON
[\#973](https://github.com/matrix-org/matrix-js-sdk/pull/973)
* Handle associated event send failures
[\#972](https://github.com/matrix-org/matrix-js-sdk/pull/972)
* Remove irrelevant debug line from timeline handling
[\#971](https://github.com/matrix-org/matrix-js-sdk/pull/971)
* Handle relations in encrypted rooms
[\#969](https://github.com/matrix-org/matrix-js-sdk/pull/969)
* Relations endpoint support
[\#967](https://github.com/matrix-org/matrix-js-sdk/pull/967)
* Disable event encryption for reactions
[\#968](https://github.com/matrix-org/matrix-js-sdk/pull/968)
* Change the known safe room version to version 4
[\#966](https://github.com/matrix-org/matrix-js-sdk/pull/966)
* Check for lazy-loading support in the spec versions instead
[\#965](https://github.com/matrix-org/matrix-js-sdk/pull/965)
* Use camelCase instead of underscore
[\#963](https://github.com/matrix-org/matrix-js-sdk/pull/963)
* Time out verification attempts after 10 minutes of inactivity
[\#961](https://github.com/matrix-org/matrix-js-sdk/pull/961)
* Don't handle key verification requests which are immediately cancelled
[\#962](https://github.com/matrix-org/matrix-js-sdk/pull/962)
Changes in [2.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.1) (2019-06-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.0.1-rc.2...v2.0.1)
No changes since rc.2
Changes in [2.0.1-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.1-rc.2) (2019-06-18)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.0.1-rc.1...v2.0.1-rc.2)
* return 'sending' status for an event that is only locally redacted
[\#960](https://github.com/matrix-org/matrix-js-sdk/pull/960)
* Key verification request fixes
[\#954](https://github.com/matrix-org/matrix-js-sdk/pull/954)
* Add flag to force saving sync store
[\#956](https://github.com/matrix-org/matrix-js-sdk/pull/956)
* Expose the inhibit_login flag to register
[\#953](https://github.com/matrix-org/matrix-js-sdk/pull/953)
* Support redactions and relations of/with unsent events.
[\#947](https://github.com/matrix-org/matrix-js-sdk/pull/947)
Changes in [2.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.1-rc.1) (2019-06-12)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.0.0...v2.0.1-rc.1)
* Fix content uploads for modern browsers
[\#952](https://github.com/matrix-org/matrix-js-sdk/pull/952)
* Don't overlap auth submissions with polls
[\#951](https://github.com/matrix-org/matrix-js-sdk/pull/951)
* Add funding details for GitHub sponsor button
[\#945](https://github.com/matrix-org/matrix-js-sdk/pull/945)
* Fix backup sig validation with multiple sigs
[\#944](https://github.com/matrix-org/matrix-js-sdk/pull/944)
* Don't send another token request while one's in flight
[\#943](https://github.com/matrix-org/matrix-js-sdk/pull/943)
* Don't poll UI auth again until current poll finishes
[\#942](https://github.com/matrix-org/matrix-js-sdk/pull/942)
* Provide the discovered URLs when a liveliness error occurs
[\#938](https://github.com/matrix-org/matrix-js-sdk/pull/938)
* Encode event IDs when redacting events
[\#941](https://github.com/matrix-org/matrix-js-sdk/pull/941)
* add missing logger
[\#940](https://github.com/matrix-org/matrix-js-sdk/pull/940)
* verification: don't error if we don't know about some keys
[\#939](https://github.com/matrix-org/matrix-js-sdk/pull/939)
* Local echo for redactions
[\#937](https://github.com/matrix-org/matrix-js-sdk/pull/937)
* Refresh safe room versions when the server looks more modern than us
[\#934](https://github.com/matrix-org/matrix-js-sdk/pull/934)
* Add v4 as a safe room version
[\#935](https://github.com/matrix-org/matrix-js-sdk/pull/935)
* Disable guard-for-in rule
[\#933](https://github.com/matrix-org/matrix-js-sdk/pull/933)
* Extend loglevel logging for the whole project
[\#924](https://github.com/matrix-org/matrix-js-sdk/pull/924)
* fix(login): saves access_token and user_id after login for all login types
[\#930](https://github.com/matrix-org/matrix-js-sdk/pull/930)
* Do not try to request thumbnails with non-integer sizes
[\#929](https://github.com/matrix-org/matrix-js-sdk/pull/929)
* Revert "Add a bunch of debugging to .well-known IS validation"
[\#928](https://github.com/matrix-org/matrix-js-sdk/pull/928)
* Add a bunch of debugging to .well-known IS validation
[\#927](https://github.com/matrix-org/matrix-js-sdk/pull/927)
Changes in [2.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.0) (2019-05-31)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.2.0...v2.0.0)
BREAKING CHANGES
----------------
* This package now publishes in ES6 / ES2015 syntax to NPM
* Saves access_token and user_id after login for all login types
[\#932](https://github.com/matrix-org/matrix-js-sdk/pull/932)
* Fix recovery key encoding for base-x 3.0.5
[\#931](https://github.com/matrix-org/matrix-js-sdk/pull/931)
Changes in [1.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.2.0) (2019-05-29)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.2.0-rc.1...v1.2.0)
Changes in [1.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.2.0-rc.1) (2019-05-23)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.1.0...v1.2.0-rc.1)
* interactive-auth now handles requesting email tokens
[\#926](https://github.com/matrix-org/matrix-js-sdk/pull/926)
* allow access to unreplaced message content
[\#923](https://github.com/matrix-org/matrix-js-sdk/pull/923)
* Add method to retrieve replacing event
[\#922](https://github.com/matrix-org/matrix-js-sdk/pull/922)
* More logging when signature verification fails
[\#921](https://github.com/matrix-org/matrix-js-sdk/pull/921)
* Local echo for m.replace relations
[\#920](https://github.com/matrix-org/matrix-js-sdk/pull/920)
* Track relations as pending and remove when cancelled
[\#919](https://github.com/matrix-org/matrix-js-sdk/pull/919)
* Add stringify helper to summarise events when debugging
[\#916](https://github.com/matrix-org/matrix-js-sdk/pull/916)
* Message editing: filter out replacements for senders that are not the
original sender
[\#918](https://github.com/matrix-org/matrix-js-sdk/pull/918)
* Wait until decrypt before aggregating
[\#917](https://github.com/matrix-org/matrix-js-sdk/pull/917)
* Message editing: mark original event as replaced instead of replacing the
event object
[\#914](https://github.com/matrix-org/matrix-js-sdk/pull/914)
* Support for replacing message through m.replace relationship.
[\#913](https://github.com/matrix-org/matrix-js-sdk/pull/913)
* Use a short timeout for .well-known requests
[\#912](https://github.com/matrix-org/matrix-js-sdk/pull/912)
* Redaction and change events for relations
[\#911](https://github.com/matrix-org/matrix-js-sdk/pull/911)
* Add basic read path for relations
[\#910](https://github.com/matrix-org/matrix-js-sdk/pull/910)
* Add a concept of default push rules, using it for tombstone notifications
[\#860](https://github.com/matrix-org/matrix-js-sdk/pull/860)
* yarn upgrade
[\#907](https://github.com/matrix-org/matrix-js-sdk/pull/907)
Changes in [1.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.1.0) (2019-05-07)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.1.0-rc.1...v1.1.0)
* No Changes since rc.1
Changes in [1.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.1.0-rc.1) (2019-04-30)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.4...v1.1.0-rc.1)
* use the release version of olm 3.1.0
[\#903](https://github.com/matrix-org/matrix-js-sdk/pull/903)
* Use new Olm repo link in README
[\#901](https://github.com/matrix-org/matrix-js-sdk/pull/901)
* Support being fed a .well-known config object for validation
[\#897](https://github.com/matrix-org/matrix-js-sdk/pull/897)
* emit self-membership event at end of handling sync update
[\#900](https://github.com/matrix-org/matrix-js-sdk/pull/900)
* Use packages.matrix.org for Olm
[\#898](https://github.com/matrix-org/matrix-js-sdk/pull/898)
* Fix tests on develop
[\#899](https://github.com/matrix-org/matrix-js-sdk/pull/899)
* Stop syncing when the token is invalid
[\#895](https://github.com/matrix-org/matrix-js-sdk/pull/895)
* change event redact, POST request to PUT request
[\#887](https://github.com/matrix-org/matrix-js-sdk/pull/887)
* Expose better autodiscovery error messages
[\#894](https://github.com/matrix-org/matrix-js-sdk/pull/894)
* Explicitly guard store usage during sync startup
[\#892](https://github.com/matrix-org/matrix-js-sdk/pull/892)
* Flag v3 rooms as safe
[\#893](https://github.com/matrix-org/matrix-js-sdk/pull/893)
* Cache failed capabilities lookups for shorter amounts of time
[\#890](https://github.com/matrix-org/matrix-js-sdk/pull/890)
* Fix highlight notifications for unencrypted rooms
[\#891](https://github.com/matrix-org/matrix-js-sdk/pull/891)
* Document checking crypto state before using `hasUnverifiedDevices`
[\#889](https://github.com/matrix-org/matrix-js-sdk/pull/889)
* Add logging to sync startup path
[\#888](https://github.com/matrix-org/matrix-js-sdk/pull/888)
* Track e2e highlights better, particularly in 'Mentions Only' rooms
[\#886](https://github.com/matrix-org/matrix-js-sdk/pull/886)
* support both the incorrect and correct MAC methods
[\#882](https://github.com/matrix-org/matrix-js-sdk/pull/882)
* Refuse to set forwards pagination token on live timeline
[\#885](https://github.com/matrix-org/matrix-js-sdk/pull/885)
* Degrade `IndexedDBStore` back to memory only on failure
[\#884](https://github.com/matrix-org/matrix-js-sdk/pull/884)
* Refuse to link live timelines into the forwards/backwards position when
either is invalid
[\#877](https://github.com/matrix-org/matrix-js-sdk/pull/877)
* Key backup logging improvements
[\#883](https://github.com/matrix-org/matrix-js-sdk/pull/883)
* Don't assume aborts are always from txn.abort()
[\#880](https://github.com/matrix-org/matrix-js-sdk/pull/880)
* Add a bunch of logging
[\#878](https://github.com/matrix-org/matrix-js-sdk/pull/878)
* Refuse splicing the live timeline into a broken position
[\#873](https://github.com/matrix-org/matrix-js-sdk/pull/873)
* Add existence check to local storage based crypto store
[\#872](https://github.com/matrix-org/matrix-js-sdk/pull/872)
Changes in [1.0.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.4) (2019-04-08)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.3...v1.0.4)
* Hotfix: more logging and potential fixes for timeline corruption issue, see ticket https://github.com/vector-im/riot-web/issues/8593.
Changes in [1.0.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.3) (2019-04-01)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.3-rc.1...v1.0.3)
* Add existence check to local storage based crypto store
[\#874](https://github.com/matrix-org/matrix-js-sdk/pull/874)
Changes in [1.0.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.3-rc.1) (2019-03-27)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.2...v1.0.3-rc.1)
* Add IndexedDB existence checks
[\#871](https://github.com/matrix-org/matrix-js-sdk/pull/871)
* Emit sync errors for capturing by clients
[\#869](https://github.com/matrix-org/matrix-js-sdk/pull/869)
* Add functions for getting room upgrade history and leaving those rooms
[\#868](https://github.com/matrix-org/matrix-js-sdk/pull/868)
* Clarify the meaning of 'real name' for contribution
[\#867](https://github.com/matrix-org/matrix-js-sdk/pull/867)
* Remove `sessionStore` to `cryptoStore` migration path
[\#865](https://github.com/matrix-org/matrix-js-sdk/pull/865)
* Add debugging for spurious room version warnings
[\#866](https://github.com/matrix-org/matrix-js-sdk/pull/866)
* Add investigation notes for browser storage
[\#864](https://github.com/matrix-org/matrix-js-sdk/pull/864)
* make sure resolve object is defined before calling it
[\#862](https://github.com/matrix-org/matrix-js-sdk/pull/862)
* Rename `MatrixInMemoryStore` to `MemoryStore`
[\#861](https://github.com/matrix-org/matrix-js-sdk/pull/861)
* Use Buildkite for CI
[\#859](https://github.com/matrix-org/matrix-js-sdk/pull/859)
* only create one session at a time per device
[\#857](https://github.com/matrix-org/matrix-js-sdk/pull/857)
Changes in [1.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.2) (2019-03-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.2-rc.1...v1.0.2)
* No changes since rc.1
Changes in [1.0.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.2-rc.1) (2019-03-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.1...v1.0.2-rc.1)
* Use modern Yarn version on Travis CI
[\#858](https://github.com/matrix-org/matrix-js-sdk/pull/858)
* Switch to `yarn` for dependency management
[\#856](https://github.com/matrix-org/matrix-js-sdk/pull/856)
* More key request fixes
[\#855](https://github.com/matrix-org/matrix-js-sdk/pull/855)
* Calculate encrypted notification counts
[\#851](https://github.com/matrix-org/matrix-js-sdk/pull/851)
* Update dependencies
[\#854](https://github.com/matrix-org/matrix-js-sdk/pull/854)
* make sure key requests get sent
[\#850](https://github.com/matrix-org/matrix-js-sdk/pull/850)
* Use 'ideal' rather than 'exact' for deviceid
[\#852](https://github.com/matrix-org/matrix-js-sdk/pull/852)
* handle partially-shared sessions better
[\#848](https://github.com/matrix-org/matrix-js-sdk/pull/848)
Changes in [1.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.1) (2019-03-06)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.1-rc.2...v1.0.1)
* No changes since rc.2
Changes in [1.0.1-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.1-rc.2) (2019-03-05)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.1-rc.1...v1.0.1-rc.2)
* dont swallow txn errors in crypto store
[\#853](https://github.com/matrix-org/matrix-js-sdk/pull/853)
* Don't swallow txn errors in crypto store
[\#849](https://github.com/matrix-org/matrix-js-sdk/pull/849)
Changes in [1.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.1-rc.1) (2019-02-28)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.0...v1.0.1-rc.1)
* Fix "e is undefined" masking the original error in MegolmDecryption
[\#847](https://github.com/matrix-org/matrix-js-sdk/pull/847)
Changes in [1.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.0) (2019-02-14)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.0-rc.2...v1.0.0)
* Try again to commit package-lock.json
[\#841](https://github.com/matrix-org/matrix-js-sdk/pull/841)
Changes in [1.0.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.0-rc.2) (2019-02-14)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.0-rc.1...v1.0.0-rc.2)
* Release script: commit package-lock.json
[\#839](https://github.com/matrix-org/matrix-js-sdk/pull/839)
* Add method to force re-check of key backup
[\#840](https://github.com/matrix-org/matrix-js-sdk/pull/840)
* Fix: dont check for unverified devices in left members
[\#838](https://github.com/matrix-org/matrix-js-sdk/pull/838)
Changes in [1.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.0-rc.1) (2019-02-08)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.3...v1.0.0-rc.1)
* change hex SAS verification to decimal and emoji
[\#837](https://github.com/matrix-org/matrix-js-sdk/pull/837)
* Trust on decrypt
[\#836](https://github.com/matrix-org/matrix-js-sdk/pull/836)
* Always track our own devices
[\#835](https://github.com/matrix-org/matrix-js-sdk/pull/835)
* Make linting rules more consistent
[\#834](https://github.com/matrix-org/matrix-js-sdk/pull/834)
* add method to room to check for unverified devices
[\#833](https://github.com/matrix-org/matrix-js-sdk/pull/833)
* Merge redesign into develop
[\#831](https://github.com/matrix-org/matrix-js-sdk/pull/831)
* Supporting infrastructure for educated decisions on when to upgrade rooms
[\#830](https://github.com/matrix-org/matrix-js-sdk/pull/830)
* Include signature info for unknown devices
[\#826](https://github.com/matrix-org/matrix-js-sdk/pull/826)
* Flag v2 rooms as "safe"
[\#828](https://github.com/matrix-org/matrix-js-sdk/pull/828)
* Update ESLint
[\#821](https://github.com/matrix-org/matrix-js-sdk/pull/821)
Changes in [0.14.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.3) (2019-01-22)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.3-rc.1...v0.14.3)
@@ -625,7 +85,7 @@ Changes in [0.14.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
BREAKING CHANGE
----------------
* js-sdk now uses Olm 3.0. Apps using Olm must update to 3.0 to
continue using Olm with the js-sdk. The js-sdk will call Olm's
init() method when the client is started.
+9 -13
View File
@@ -24,7 +24,7 @@ works. Develop is the unstable branch where all the development actually
happens: the workflow is that contributors should fork the develop branch to
make a 'feature' branch for a particular contribution, and then make a pull
request to merge this back into the matrix.org 'official' develop branch. We
use GitHub's pull request workflow to review the contribution, and either ask
use github's pull request workflow to review the contribution, and either ask
you to make any refinements needed or merge it and make them ourselves. The
changes will then land on master when we next do a release.
@@ -60,8 +60,8 @@ Sign off
~~~~~~~~
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
and you agree to license it under the same terms as the project's license, we've adopted the
same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
@@ -109,16 +109,12 @@ include the line in your commit or pull request comment::
Signed-off-by: Your Name <your@email.example.org>
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
...using your real name; unfortunately pseudonyms and anonymous contributions
can't be accepted. Git makes this trivial - just use the -s flag when you do
``git commit``, having first set ``user.name`` and ``user.email`` git configs
(which you should have done anyway :)
Git allows you to add this signoff automatically when using the ``-s`` flag to
``git commit``, which uses the name and email set in your ``user.name`` and
``user.email`` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase::
If you forgot to sign off your commits before making your pull request and are on git 2.17+
you can mass signoff using rebase::
git rebase --signoff origin/develop
+19 -22
View File
@@ -1,7 +1,8 @@
Matrix Javascript SDK
=====================
[![Build Status](http://matrix.org/jenkins/buildStatus/icon?job=JavascriptSDK)](http://matrix.org/jenkins/job/JavascriptSDK/)
This is the [Matrix](https://matrix.org) Client-Server r0 SDK for
This is the [Matrix](https://matrix.org) Client-Server v1/v2 alpha SDK for
JavaScript. This SDK can be run in a browser or in Node.js.
Quickstart
@@ -20,11 +21,7 @@ Please check [the working browser example](examples/browser) for more informatio
In Node.js
----------
Ensure you have the latest LTS version of Node.js installed.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://yarnpkg.com/docs/install/) if you do not have it already.
``yarn add matrix-js-sdk``
``npm install matrix-js-sdk``
```javascript
var sdk = require("matrix-js-sdk");
@@ -79,7 +76,7 @@ client.on("Room.timeline", function(event, room, toStartOfTimeline) {
});
```
By default, the `matrix-js-sdk` client uses the `MemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room:
By default, the `matrix-js-sdk` client uses the `MatrixInMemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room:
```javascript
Object.keys(client.store.rooms).forEach((roomId) => {
@@ -286,7 +283,7 @@ This SDK uses JSDoc3 style comments. You can manually build and
host the API reference from the source files like this:
```
$ yarn gendoc
$ npm run gendoc
$ cd .jsdoc
$ python -m SimpleHTTPServer 8005
```
@@ -297,8 +294,8 @@ End-to-end encryption support
=============================
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
application to make libolm available, via the ``Olm`` global.
[libolm](http://matrix.org/git/olm). It is left up to the application to make
libolm available, via the ``Olm`` global.
It is also necessry to call ``matrixClient.initCrypto()`` after creating a new
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
@@ -317,20 +314,20 @@ specification.
To provide the Olm library in a browser application:
* download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
* download the transpiled libolm (from https://matrix.org/packages/npm/olm/).
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
To provide the Olm library in a node.js application:
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``
* ``npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz``
(replace the URL with the latest version you want to use from
https://packages.matrix.org/npm/olm/)
https://matrix.org/packages/npm/olm/)
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
If you want to package Olm as dependency for your node.js application, you can
use ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``. If your
application also works without e2e crypto enabled, add ``--optional`` to mark it
as an optional dependency.
If you want to package Olm as dependency for your node.js application, you
can use ``npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz
--save-optional`` (if your application also works without e2e crypto enabled)
or ``--save`` (if it doesn't) to do so.
Contributing
@@ -340,7 +337,7 @@ want to use this SDK, skip this section.*
First, you need to pull in the right build tools:
```
$ yarn install
$ npm install
```
Building
@@ -348,20 +345,20 @@ Building
To build a browser version from scratch when developing::
```
$ yarn build
$ npm run build
```
To constantly do builds when files are modified (using ``watchify``)::
```
$ yarn watch
$ npm run watch
```
To run tests (Jasmine)::
```
$ yarn test
$ npm test
```
To run linting:
```
$ yarn lint
$ npm run lint
```
-70
View File
@@ -1,70 +0,0 @@
# Browser Storage Notes
## Overview
Browsers examined: Firefox 67, Chrome 75
The examination below applies to the default, non-persistent storage policy.
## Quota Measurement
Browsers appear to enforce and measure the quota in terms of space on disk, not
data stored, so you may be able to store more data than the simple sum of all
input data depending on how compressible your data is.
## Quota Limit
Specs and documentation suggest we should consistently receive
`QuotaExceededError` when we're near space limits, but the reality is a bit
blurrier.
When we are low on disk space overall or near the group limit / origin quota:
* Chrome
* Log database may fail to start with AbortError
* IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker
* When near the quota, QuotaExceededError is used more consistently
* Firefox
* The first error will be QuotaExceededError
* Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations."
* Once you start getting errors, the DB is effectively wedged in read-only
mode
* Can revive access if you reopen the DB
## Cache Eviction
While the Storage Standard says all storage for an origin group should be
limited by a single quota, in practice, browsers appear to handle `localStorage`
separately from the others, so it has a separate quota limit and isn't evicted
when low on space.
* Chrome, Firefox
* IndexedDB for origin deleted
* Local Storage remains in place
## Persistent Storage
Storage Standard offers a `navigator.storage.persist` API that can be used to
request persistent storage that won't be deleted by the browser because of low
space.
* Chrome
* Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
* Firefox
* Firefox 67 shows a prompt to grant
* Reverting persistent seems to require revoking permission _and_ clearing
site data
## Storage Estimation
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
how much space remains.
* Chrome, Firefox
* Can run this at any time to request an estimate of space remaining
* Firefox
* Returns `0` for `usage` if a site is persisted
+1 -1
View File
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
# now run our checks
cd "$tmpdir"
yarn lint
npm run lint
+6 -8
View File
@@ -6,7 +6,7 @@ export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 10 || exit $?
yarn install || exit $?
npm install || exit $?
RC=0
@@ -18,19 +18,17 @@ function fail {
# don't use last time's test reports
rm -rf reports coverage || exit $?
yarn test || fail "yarn test finished with return code $?"
npm test || fail "npm test finished with return code $?"
yarn -s lint -f checkstyle > eslint.xml ||
npm run -s lint -- -f checkstyle > eslint.xml ||
fail "eslint finished with return code $?"
# delete the old tarball, if it exists
rm -f matrix-js-sdk-*.tgz
# `yarn pack` doesn't seem to run scripts, however that seems okay here as we
# just built as part of `install` above.
yarn pack ||
fail "yarn pack finished with return code $?"
npm pack ||
fail "npm pack finished with return code $?"
yarn gendoc || fail "JSDoc failed with code $?"
npm run gendoc || fail "JSDoc failed with code $?"
exit $RC
+6772
View File
File diff suppressed because it is too large Load Diff
+13 -16
View File
@@ -1,24 +1,24 @@
{
"name": "matrix-js-sdk",
"version": "2.4.2-rc.1",
"version": "0.14.3",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test:build": "babel -s -d specbuild spec",
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" node_modules/mocha/bin/_mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
"test:watch": "mocha --watch --compilers js:babel-core/register --recursive spec --colors",
"test": "yarn test:build && yarn test:run",
"check": "yarn test:build && _mocha --recursive specbuild --colors",
"gendoc": "babel --no-babelrc --plugins transform-class-properties -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"start": "yarn start:init && yarn start:watch",
"test": "npm run test:build && npm run test:run",
"check": "npm run test:build && _mocha --recursive specbuild --colors",
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"start": "npm run start:init && npm run start:watch",
"start:watch": "babel -s -w --skip-initial-build -d lib src",
"start:init": "babel -s -d lib src",
"clean": "rimraf lib dist",
"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 && terser -c -m -o dist/browser-matrix.min.js --source-map \"content='dist/browser-matrix.js.map'\" dist/browser-matrix.js",
"dist": "yarn build",
"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 93 src spec",
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt"
"lint": "eslint --max-warnings 101 src spec",
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
"type": "git",
@@ -54,6 +54,7 @@
"dependencies": {
"another-json": "^0.2.0",
"babel-runtime": "^6.26.0",
"base-x": "3.0.4",
"bluebird": "^3.5.0",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
@@ -65,30 +66,26 @@
},
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-eslint": "^10.0.1",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.18.0",
"browserify": "^16.2.3",
"browserify-shim": "^3.8.13",
"eslint": "^5.12.0",
"eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^5.3.0",
"exorcist": "^0.4.0",
"expect": "^1.20.2",
"istanbul": "^0.4.5",
"jsdoc": "^3.5.5",
"lolex": "^1.5.2",
"matrix-mock-request": "^1.2.3",
"matrix-mock-request": "^1.2.2",
"mocha": "^5.2.0",
"mocha-jenkins-reporter": "^0.4.0",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
"rimraf": "^2.5.4",
"source-map-support": "^0.4.11",
"sourceify": "^0.1.0",
"terser": "^4.0.0",
"watchify": "^3.11.1"
"uglify-js": "^2.8.26",
"watchify": "^3.11.0"
},
"browserify": {
"transform": [
+11 -31
View File
@@ -6,9 +6,7 @@
# github-changelog-generator; install via:
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
# npm; typically installed by Node.js
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
# hub; install via brew (OSX) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
set -e
@@ -24,8 +22,6 @@ else
echo "hub is required: please install it"
exit
fi
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
@@ -92,8 +88,6 @@ if [ -z "$skip_changelog" ]; then
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
fi
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
@@ -153,22 +147,14 @@ cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_cha
set -x
# Bump package.json and build the dist
echo "yarn version"
# yarn version will automatically commit its modification
echo "npm version"
# npm version will automatically commit its modification
# and make a release tag. We don't want it to create the tag
# because it can only sign with the default key, but we can
# only turn off both of these behaviours, so we have to
# manually commit the result.
yarn version --no-git-tag-version --new-version "$release"
# commit yarn.lock if it exists, is versioned, and is modified
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
then
pkglock='yarn.lock'
else
pkglock=''
fi
git commit package.json $pkglock -m "$tag"
npm version --no-git-tag-version "$release"
git commit package.json -m "$tag"
# figure out if we should be signing this release
@@ -184,7 +170,7 @@ fi
# assets.
# We make a completely separate checkout to be sure
# we're using released versions of the dependencies
# (rather than whatever we're pulling in from yarn link)
# (rather than whatever we're pulling in from npm link)
assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
@@ -195,15 +181,10 @@ if [ $dodist -eq 0 ]; then
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
# We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
# global cache here to ensure we get the right thing.
yarn cache clean
yarn install
npm install
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" yarn dist
DIST_VERSION="$tag" npm run dist
popd
@@ -292,13 +273,12 @@ fi
rm "${release_text}"
rm "${latest_changes}"
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
# publish to npmjs
npm publish
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
yarn gendoc
npm run gendoc
echo "copying jsdocs to gh-pages branch"
git checkout gh-pages
@@ -323,7 +303,7 @@ git checkout master
git pull
git merge "$rel_branch"
# push master and docs (if generated) to github
# push master and docs (if generated) to github
git push origin master
if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
+15 -24
View File
@@ -1,7 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018-2019 New Vector Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -27,7 +27,6 @@ import MockHttpBackend from 'matrix-mock-request';
import expect from 'expect';
import Promise from 'bluebird';
import LocalStorageCryptoStore from '../lib/crypto/store/localStorage-crypto-store';
import logger from '../src/logger';
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
@@ -39,10 +38,9 @@ import logger from '../src/logger';
*
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
* session store. If undefined, we will create a MockStorageApi.
* @param {object} options additional options to pass to the client
*/
export default function TestClient(
userId, deviceId, accessToken, sessionStoreBackend, options,
userId, deviceId, accessToken, sessionStoreBackend,
) {
this.userId = userId;
this.deviceId = deviceId;
@@ -52,22 +50,19 @@ export default function TestClient(
}
const sessionStore = new sdk.WebStorageSessionStore(sessionStoreBackend);
this.httpBackend = new MockHttpBackend();
// expose this so the tests can get to it
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
options = Object.assign({
this.httpBackend = new MockHttpBackend();
this.client = sdk.createClient({
baseUrl: "http://" + userId + ".test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
sessionStore: sessionStore,
cryptoStore: this.cryptoStore,
request: this.httpBackend.requestFn,
}, options);
if (!options.cryptoStore) {
// expose this so the tests can get to it
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
options.cryptoStore = this.cryptoStore;
}
this.client = sdk.createClient(options);
});
this.deviceKeys = null;
this.oneTimeKeys = {};
@@ -83,7 +78,7 @@ TestClient.prototype.toString = function() {
* @return {Promise}
*/
TestClient.prototype.start = function() {
logger.log(this + ': starting');
console.log(this + ': starting');
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
@@ -101,7 +96,7 @@ TestClient.prototype.start = function() {
this.httpBackend.flushAllExpected(),
testUtils.syncPromise(this.client),
]).then(() => {
logger.log(this + ': started');
console.log(this + ': started');
});
};
@@ -123,7 +118,7 @@ TestClient.prototype.expectDeviceKeyUpload = function() {
expect(content.one_time_keys).toBe(undefined);
expect(content.device_keys).toBeTruthy();
logger.log(self + ': received device keys');
console.log(self + ': received device keys');
// we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
@@ -160,7 +155,7 @@ TestClient.prototype.awaitOneTimeKeyUpload = function() {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).toNotEqual({});
logger.log('%s: received %i one-time keys', this,
console.log('%s: received %i one-time keys', this,
Object.keys(content.one_time_keys).length);
this.oneTimeKeys = content.one_time_keys;
return {one_time_key_counts: {
@@ -186,11 +181,7 @@ TestClient.prototype.expectKeyQuery = function(response) {
this.httpBackend.when('POST', '/keys/query').respond(
200, (path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual(
{},
"Expected key query for " + userId + ", got " +
Object.keys(content.device_keys),
);
expect(content.device_keys[userId]).toEqual({});
});
return response;
});
@@ -224,11 +215,11 @@ TestClient.prototype.getSigningKey = function() {
* @returns {Promise} promise which completes once the sync has been flushed
*/
TestClient.prototype.flushSync = function() {
logger.log(`${this}: flushSync`);
console.log(`${this}: flushSync`);
return Promise.all([
this.httpBackend.flush('/sync', 1),
testUtils.syncPromise(this.client),
]).then(() => {
logger.log(`${this}: flushSync completed`);
console.log(`${this}: flushSync completed`);
});
};
+4 -7
View File
@@ -20,7 +20,6 @@ import Promise from 'bluebird';
import TestClient from '../TestClient';
import testUtils from '../test-utils';
import logger from '../../src/logger';
const ROOM_ID = "!room:id";
@@ -72,7 +71,7 @@ function getSyncResponse(roomMembers) {
describe("DeviceList management:", function() {
if (!global.Olm) {
logger.warn('not running deviceList tests: Olm not present');
console.warn('not running deviceList tests: Olm not present');
return;
}
@@ -88,7 +87,7 @@ describe("DeviceList management:", function() {
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
// we create our own sessionStoreBackend so that we can use it for
// another TestClient.
@@ -102,14 +101,13 @@ describe("DeviceList management:", function() {
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
logger.log("Forcing alice to download our device keys");
console.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
@@ -122,7 +120,7 @@ describe("DeviceList management:", function() {
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
logger.log("Telling alice to send a megolm message");
console.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/',
@@ -145,7 +143,6 @@ describe("DeviceList management:", function() {
it("We should not get confused by out-of-order device query responses",
() => {
// https://github.com/vector-im/riot-web/issues/3126
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
+11 -27
View File
@@ -36,7 +36,6 @@ import Promise from 'bluebird';
const utils = require("../../lib/utils");
const testUtils = require("../test-utils");
const TestClient = require('../TestClient').default;
import logger from '../../src/logger';
let aliTestClient;
const roomId = "!room:localhost";
@@ -73,11 +72,7 @@ function expectAliQueryKeys() {
bobKeys[bobDeviceId] = bobTestClient.deviceKeys;
aliTestClient.httpBackend.when("POST", "/keys/query")
.respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual(
{},
"Expected Alice to key query for " + bobUserId + ", got " +
Object.keys(content.device_keys),
);
expect(content.device_keys[bobUserId]).toEqual({});
const result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
@@ -96,16 +91,12 @@ function expectBobQueryKeys() {
const aliKeys = {};
aliKeys[aliDeviceId] = aliTestClient.deviceKeys;
logger.log("query result will be", aliKeys);
console.log("query result will be", aliKeys);
bobTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual(
{},
"Expected Bob to key query for " + aliUserId + ", got " +
Object.keys(content.device_keys),
);
expect(content.device_keys[aliUserId]).toEqual({});
const result = {};
result[aliUserId] = aliKeys;
return {device_keys: result};
@@ -335,7 +326,7 @@ function recvMessage(httpBackend, client, sender, message) {
if (event.getType() == "m.room.member") {
return;
}
logger.log(client.credentials.userId + " received event",
console.log(client.credentials.userId + " received event",
event);
client.removeListener("event", onEvent);
@@ -406,7 +397,7 @@ describe("MatrixClient crypto", function() {
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
await aliTestClient.client.initCrypto();
@@ -553,7 +544,6 @@ describe("MatrixClient crypto", function() {
});
it("Ali sends a message", function(done) {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -564,7 +554,6 @@ describe("MatrixClient crypto", function() {
});
it("Bob receives a message", function() {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -575,7 +564,6 @@ describe("MatrixClient crypto", function() {
});
it("Bob receives a message with a bogus sender", function() {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -608,7 +596,7 @@ describe("MatrixClient crypto", function() {
const eventPromise = new Promise((resolve, reject) => {
const onEvent = function(event) {
logger.log(bobUserId + " received event",
console.log(bobUserId + " received event",
event);
resolve(event);
};
@@ -629,7 +617,6 @@ describe("MatrixClient crypto", function() {
});
it("Ali blocks Bob's device", function(done) {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -649,7 +636,6 @@ describe("MatrixClient crypto", function() {
});
it("Bob receives two pre-key messages", function(done) {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -663,8 +649,6 @@ describe("MatrixClient crypto", function() {
});
it("Bob replies to the message", function() {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
bobTestClient.expectKeyQuery({device_keys: {[bobUserId]: {}}});
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -675,14 +659,13 @@ describe("MatrixClient crypto", function() {
.then(bobRecvMessage)
.then(bobEnablesEncryption)
.then(bobSendsReplyMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1, "Unexpected cipghertext type.");
expect(ciphertext.type).toEqual(1);
}).then(aliRecvMessage);
});
it("Ali does a key query when encryption is enabled", function() {
// enabling encryption in the room should make alice download devices
// for both members.
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => firstSync(aliTestClient))
@@ -713,6 +696,7 @@ describe("MatrixClient crypto", function() {
}).then(() => {
aliTestClient.expectKeyQuery({
device_keys: {
[aliUserId]: {},
[bobUserId]: {},
},
});
@@ -735,7 +719,7 @@ describe("MatrixClient crypto", function() {
return Promise.resolve()
.then(() => {
logger.log(aliTestClient + ': starting');
console.log(aliTestClient + ': starting');
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
aliTestClient.expectDeviceKeyUpload();
@@ -747,7 +731,7 @@ describe("MatrixClient crypto", function() {
aliTestClient.client.startClient({});
return httpBackend.flushAllExpected().then(() => {
logger.log(aliTestClient + ': started');
console.log(aliTestClient + ': started');
});
})
.then(() => httpBackend.when("POST", "/keys/upload")
@@ -756,7 +740,7 @@ describe("MatrixClient crypto", function() {
expect(content.one_time_keys).toNotEqual({});
expect(Object.keys(content.one_time_keys).length)
.toBeGreaterThanOrEqualTo(1);
logger.log('received %i one-time keys',
console.log('received %i one-time keys',
Object.keys(content.one_time_keys).length);
// cancel futher calls by telling the client
// we have more than we need
+4 -25
View File
@@ -15,7 +15,7 @@ describe("MatrixClient events", function() {
const selfAccessToken = "aseukfgwef";
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
@@ -157,7 +157,7 @@ describe("MatrixClient events", function() {
return;
}
expect(event.event).toMatch(SYNC_DATA.presence.events[0]);
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
SYNC_DATA.presence.events[0].content.presence,
);
@@ -302,32 +302,11 @@ describe("MatrixClient events", function() {
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN' };
httpBackend.when("GET", "/sync").respond(401, error);
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) {
client.on("Session.logged_out", function(event, member) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
httpBackend.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
@@ -5,7 +5,6 @@ const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const EventTimeline = sdk.EventTimeline;
import logger from '../../src/logger';
const baseUrl = "http://localhost.or.something";
const userId = "@alice:localhost";
@@ -85,7 +84,7 @@ function startClient(httpBackend, client) {
// set up a promise which will resolve once the client is initialised
const deferred = Promise.defer();
client.on("sync", function(state) {
logger.log("sync", state);
console.log("sync", state);
if (state != "SYNCING") {
return;
}
@@ -103,7 +102,7 @@ describe("getEventTimeline support", function() {
let client;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
});
@@ -228,7 +227,7 @@ describe("MatrixClient event timelines", function() {
let httpBackend = null;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
@@ -670,11 +669,11 @@ describe("MatrixClient event timelines", function() {
// initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
logger.log("sendTextMessage completed");
console.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
logger.log("getEventTimeline completed (2)");
console.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
}),
@@ -685,7 +684,7 @@ describe("MatrixClient event timelines", function() {
]).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
logger.log("getEventTimeline completed (1)");
console.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
+10 -10
View File
@@ -4,7 +4,7 @@ const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const publicGlobals = require("../../lib/matrix");
const Room = publicGlobals.Room;
const MemoryStore = publicGlobals.MemoryStore;
const MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
const Filter = publicGlobals.Filter;
const utils = require("../test-utils");
const MockStorageApi = require("../MockStorageApi");
@@ -21,9 +21,9 @@ describe("MatrixClient", function() {
const accessToken = "aseukfgwef";
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
store = new MemoryStore();
store = new MatrixInMemoryStore();
const mockStorage = new MockStorageApi();
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
@@ -48,7 +48,7 @@ describe("MatrixClient", function() {
const buf = new Buffer('hello world');
it("should upload the file", function(done) {
httpBackend.when(
"POST", "/_matrix/media/r0/upload",
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.rawData).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt");
@@ -87,7 +87,7 @@ describe("MatrixClient", function() {
it("should parse the response if rawResponse=false", function(done) {
httpBackend.when(
"POST", "/_matrix/media/r0/upload",
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.opts.json).toBeFalsy();
}).respond(200, { "content_uri": "uri" });
@@ -107,7 +107,7 @@ describe("MatrixClient", function() {
it("should parse errors into a MatrixError", function(done) {
httpBackend.when(
"POST", "/_matrix/media/r0/upload",
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.rawData).toEqual(buf);
expect(req.opts.json).toBeFalsy();
@@ -355,9 +355,9 @@ describe("MatrixClient", function() {
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
};
logger.log("Ed25519: " + ed25519key);
logger.log("boris:", sign(borisKeys.dev1));
logger.log("chaz:", sign(chazKeys.dev2));
console.log("Ed25519: " + ed25519key);
console.log("boris:", sign(borisKeys.dev1));
console.log("chaz:", sign(chazKeys.dev2));
*/
httpBackend.when("POST", "/keys/query").check(function(req) {
@@ -396,7 +396,7 @@ describe("MatrixClient", function() {
const auth = {a: 1};
it("should pass through an auth dict", function(done) {
httpBackend.when(
"DELETE", "/_matrix/client/r0/devices/my_device",
"DELETE", "/_matrix/client/unstable/devices/my_device",
).check(function(req) {
expect(req.data).toEqual({auth: auth});
}).respond(200);
+2 -2
View File
@@ -58,7 +58,7 @@ describe("MatrixClient opts", function() {
};
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
});
@@ -128,7 +128,7 @@ describe("MatrixClient opts", function() {
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn,
store: new sdk.MemoryStore(),
store: new sdk.MatrixInMemoryStore(),
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
+1 -1
View File
@@ -20,7 +20,7 @@ describe("MatrixClient retrying", function() {
let room;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
scheduler = new sdk.MatrixScheduler();
@@ -104,7 +104,7 @@ describe("MatrixClient room timelines", function() {
}
beforeEach(function(done) {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
+1 -1
View File
@@ -23,7 +23,7 @@ describe("MatrixClient syncing", function() {
const roomTwo = "!bar:localhost";
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
+18 -24
View File
@@ -23,7 +23,6 @@ import expect from 'expect';
const utils = require('../../lib/utils');
const testUtils = require('../test-utils');
const TestClient = require('../TestClient').default;
import logger from '../../src/logger';
const ROOM_ID = "!room:id";
@@ -204,7 +203,7 @@ function getSyncResponse(roomMembers) {
describe("megolm", function() {
if (!global.Olm) {
logger.warn('not running megolm tests: Olm not present');
console.warn('not running megolm tests: Olm not present');
return;
}
const Olm = global.Olm;
@@ -283,7 +282,7 @@ describe("megolm", function() {
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",
@@ -417,7 +416,7 @@ describe("megolm", function() {
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
console.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
@@ -500,7 +499,6 @@ describe("megolm", function() {
it('Alice sends a megolm message', function() {
let p2pSession;
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -556,7 +554,7 @@ describe("megolm", function() {
).respond(200, function(path, content) {
const ct = content.ciphertext;
const r = inboundGroupSession.decrypt(ct);
logger.log('Decrypted received megolm message', r);
console.log('Decrypted received megolm message', r);
expect(r.message_index).toEqual(0);
const decrypted = JSON.parse(r.plaintext);
@@ -583,7 +581,6 @@ describe("megolm", function() {
});
it("We shouldn't attempt to send to blocked devices", function() {
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -601,7 +598,7 @@ describe("megolm", function() {
return aliceTestClient.flushSync();
}).then(function() {
logger.log('Forcing alice to download our device keys');
console.log('Forcing alice to download our device keys');
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
@@ -612,10 +609,10 @@ describe("megolm", function() {
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
logger.log('Telling alice to block our device');
console.log('Telling alice to block our device');
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
logger.log('Telling alice to send a megolm message');
console.log('Telling alice to send a megolm message');
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
@@ -637,7 +634,6 @@ describe("megolm", function() {
let p2pSession;
let megolmSessionId;
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -657,7 +653,7 @@ describe("megolm", function() {
return aliceTestClient.flushSync();
}).then(function() {
logger.log("Fetching bob's devices and marking known");
console.log("Fetching bob's devices and marking known");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
@@ -670,17 +666,17 @@ describe("megolm", function() {
aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
});
}).then(function() {
logger.log('Telling alice to send a megolm message');
console.log('Telling alice to send a megolm message');
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(path, content) {
logger.log('sendToDevice: ', content);
console.log('sendToDevice: ', content);
const m = content.messages['@bob:xyz'].DEVICE_ID;
const ct = m.ciphertext[testSenderKey];
expect(ct.type).toEqual(1); // normal message
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
logger.log('decrypted sendToDevice:', decrypted);
console.log('decrypted sendToDevice:', decrypted);
expect(decrypted.type).toEqual('m.room_key');
megolmSessionId = decrypted.content.session_id;
return {};
@@ -689,7 +685,7 @@ describe("megolm", function() {
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, function(path, content) {
logger.log('/send:', content);
console.log('/send:', content);
expect(content.session_id).toEqual(megolmSessionId);
return {
event_id: '$event_id',
@@ -705,14 +701,14 @@ describe("megolm", function() {
}),
]);
}).then(function() {
logger.log('Telling alice to block our device');
console.log('Telling alice to block our device');
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
logger.log('Telling alice to send another megolm message');
console.log('Telling alice to send another megolm message');
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, function(path, content) {
logger.log('/send:', content);
console.log('/send:', content);
expect(content.session_id).toNotEqual(megolmSessionId);
return {
event_id: '$event_id',
@@ -793,7 +789,7 @@ describe("megolm", function() {
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(path, content) {
logger.log("sendToDevice: ", content);
console.log("sendToDevice: ", content);
const m = content.messages[aliceTestClient.userId].DEVICE_ID;
const ct = m.ciphertext[testSenderKey];
expect(ct.type).toEqual(0); // pre-key message
@@ -813,7 +809,7 @@ describe("megolm", function() {
).respond(200, function(path, content) {
const ct = content.ciphertext;
const r = inboundGroupSession.decrypt(ct);
logger.log('Decrypted received megolm message', r);
console.log('Decrypted received megolm message', r);
decrypted = JSON.parse(r.plaintext);
return {
@@ -847,7 +843,6 @@ describe("megolm", function() {
let downloadPromise;
let sendPromise;
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -866,7 +861,7 @@ describe("megolm", function() {
return aliceTestClient.flushSync();
}).then(function() {
// this will block
logger.log('Forcing alice to download our device keys');
console.log('Forcing alice to download our device keys');
downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
// so will this.
@@ -891,7 +886,6 @@ describe("megolm", function() {
it("Alice exports megolm keys and imports them to a new device", function() {
let messageEncrypted;
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
+3 -4
View File
@@ -14,12 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../src/logger';
// try to load the olm library.
try {
global.Olm = require('olm');
logger.log('loaded libolm');
console.log('loaded libolm');
} catch (e) {
logger.warn("unable to run crypto tests: libolm not available");
console.warn("unable to run crypto tests: libolm not available");
}
+5 -6
View File
@@ -5,7 +5,6 @@ import Promise from 'bluebird';
// load olm before the sdk if possible
import './olm-loader';
import logger from '../src/logger';
import sdk from '..';
const MatrixEvent = sdk.MatrixEvent;
@@ -26,7 +25,7 @@ module.exports.syncPromise = function(client, count) {
const p = new Promise((resolve, reject) => {
const cb = (state) => {
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
console.log(`${Date.now()} syncPromise(${count}): ${state}`);
if (state == 'SYNCING') {
resolve();
} else {
@@ -49,8 +48,8 @@ module.exports.syncPromise = function(client, count) {
module.exports.beforeEach = function(context) {
const desc = context.currentTest.fullTitle();
logger.log(desc);
logger.log(new Array(1 + desc.length).join("="));
console.log(desc);
console.log(new Array(1 + desc.length).join("="));
};
/**
@@ -233,11 +232,11 @@ module.exports.awaitDecryption = function(event) {
return Promise.resolve(event);
}
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
console.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
console.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
+24 -24
View File
@@ -30,7 +30,7 @@ describe("AutoDiscovery", function() {
let httpBackend = null;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new MockHttpBackend();
sdk.request(httpBackend.requestFn);
});
@@ -94,7 +94,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
@@ -117,7 +117,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
@@ -140,7 +140,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
@@ -163,7 +163,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
@@ -191,7 +191,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
@@ -217,7 +217,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
@@ -245,7 +245,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
@@ -274,8 +274,8 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
@@ -303,8 +303,8 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
@@ -334,8 +334,8 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
@@ -439,14 +439,14 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_IS,
error: "Invalid identity server discovery response",
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
error: "Invalid identity server discovery response",
base_url: null,
},
};
@@ -479,14 +479,14 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_IS,
error: "Invalid identity server discovery response",
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
error: "Invalid identity server discovery response",
base_url: null,
},
};
@@ -520,15 +520,15 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_IS,
error: "Invalid identity server discovery response",
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
base_url: "https://identity.example.org",
error: "Invalid identity server discovery response",
base_url: null,
},
};
@@ -561,15 +561,15 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_IS,
error: "Invalid identity server discovery response",
// We still expect the base_url to be here for debugging purposes
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
base_url: "https://identity.example.org",
error: "Invalid identity server discovery response",
base_url: null,
},
};
+8 -8
View File
@@ -9,7 +9,7 @@ describe("ContentRepo", function() {
const baseUrl = "https://my.home.server";
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("getHttpUriForMxc", function() {
@@ -31,7 +31,7 @@ describe("ContentRepo", function() {
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
baseUrl + "/_matrix/media/v1/download/server.name/resourceid",
);
});
@@ -43,7 +43,7 @@ describe("ContentRepo", function() {
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop",
);
});
@@ -52,7 +52,7 @@ describe("ContentRepo", function() {
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
"?width=32#automade",
);
});
@@ -61,7 +61,7 @@ describe("ContentRepo", function() {
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade",
);
});
});
@@ -73,21 +73,21 @@ describe("ContentRepo", function() {
it("should set w/h by default to 96", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
baseUrl + "/_matrix/media/v1/identicon/foobar" +
"?width=96&height=96",
);
});
it("should be able to set custom w/h", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
baseUrl + "/_matrix/media/v1/identicon/foobar" +
"?width=32&height=64",
);
});
it("should URL encode the identicon string", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
baseUrl + "/_matrix/media/v1/identicon/foo%23bar" +
"?width=32&height=64",
);
});
-244
View File
@@ -8,11 +8,6 @@ import expect from 'expect';
import WebStorageSessionStore from '../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../MockStorageApi';
import TestClient from '../TestClient';
import {MatrixEvent} from '../../lib/models/event';
import Room from '../../lib/models/room';
import olmlib from '../../lib/crypto/olmlib';
import lolex from 'lolex';
const EventEmitter = require("events").EventEmitter;
@@ -124,243 +119,4 @@ describe("Crypto", function() {
await prom;
});
});
describe('Key requests', function() {
let aliceClient;
let bobClient;
beforeEach(async function() {
aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await aliceClient.initCrypto();
await bobClient.initCrypto();
});
afterEach(async function() {
aliceClient.stopClient();
bobClient.stopClient();
});
it(
"does not cancel keyshare requests if some messages are not decrypted",
async function() {
function awaitEvent(emitter, event) {
return new Promise((resolve, reject) => {
emitter.once(event, (result) => {
resolve(result);
});
});
}
async function keyshareEventForEvent(event, index) {
const eventContent = event.getWireContent();
const key = await aliceClient._crypto._olmDevice
.getInboundGroupSessionKey(
roomId, eventContent.sender_key, eventContent.session_id,
index,
);
const ksEvent = new MatrixEvent({
type: "m.forwarded_room_key",
sender: "@alice:example.com",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: eventContent.sender_key,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
session_id: eventContent.session_id,
session_key: key.key,
chain_index: key.chain_index,
forwarding_curve25519_key_chain:
key.forwarding_curve_key_chain,
},
});
// make onRoomKeyEvent think this was an encrypted event
ksEvent._senderCurve25519Key = "akey";
return ksEvent;
}
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient._crypto.encryptEvent(event, aliceRoom);
event._clearEvent = {};
event._senderCurve25519Key = null;
event._claimedEd25519Key = null;
try {
await bobClient._crypto.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const bobDecryptor = bobClient._crypto._getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
let eventPromise = Promise.all(events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}));
// keyshare the session key starting at the second message, so
// the first message can't be decrypted yet, but the second one
// can
let ksEvent = await keyshareEventForEvent(events[1], 1);
await bobDecryptor.onRoomKeyEvent(ksEvent);
await eventPromise;
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).toNotBe("m.bad.encrypted");
const cryptoStore = bobClient._cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: senderKey,
session_id: sessionId,
};
// the room key request should still be there, since we haven't
// decrypted everything
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toExist();
// keyshare the session key starting at the first message, so
// that it can now be decrypted
eventPromise = awaitEvent(events[0], "Event.decrypted");
ksEvent = await keyshareEventForEvent(events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
await eventPromise;
expect(events[0].getContent().msgtype).toNotBe("m.bad.encrypted");
// the room key request should be gone since we've now decypted everything
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toNotExist();
},
);
it("creates a new keyshare request if we request a keyshare", async function() {
// make sure that cancelAndResend... creates a new keyshare request
// if there wasn't an already-existing one
const event = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
session_id: "sessionid",
sender_key: "senderkey",
},
});
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
const cryptoStore = aliceClient._cryptoStore;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: "!someroom",
session_id: "sessionid",
sender_key: "senderkey",
};
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toExist();
});
it("uses a new txnid for re-requesting keys", async function() {
const event = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
session_id: "sessionid",
sender_key: "senderkey",
},
});
/* return a promise and a function. When the function is called,
* the promise will be resolved.
*/
function awaitFunctionCall() {
let func;
const promise = new Promise((resolve, reject) => {
func = function(...args) {
resolve(args);
return new Promise((resolve, reject) => {
// give us some time to process the result before
// continuing
global.setTimeout(resolve, 1);
});
};
});
return {func, promise};
}
aliceClient.startClient();
const clock = lolex.install();
try {
let promise;
// make a room key request, and record the transaction ID for the
// sendToDevice call
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
clock.runToLast();
let args = await promise;
const txnId = args[2];
clock.runToLast();
// give the room key request manager time to update the state
// of the request
await Promise.resolve();
// cancel and resend the room key request
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
clock.runToLast();
// the first call to sendToDevice will be the cancellation
args = await promise;
// the second call to sendToDevice will be the key request
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
clock.runToLast();
args = await promise;
clock.runToLast();
expect(args[2]).toNotBe(txnId);
} finally {
clock.uninstall();
}
});
});
});
+9 -5
View File
@@ -1,6 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,10 +16,11 @@ limitations under the License.
*/
import DeviceList from '../../../lib/crypto/DeviceList';
import MockStorageApi from '../../MockStorageApi';
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
import testUtils from '../../test-utils';
import utils from '../../../lib/utils';
import logger from '../../../src/logger';
import expect from 'expect';
import Promise from 'bluebird';
@@ -56,15 +57,18 @@ const signedDeviceList = {
describe('DeviceList', function() {
let downloadSpy;
let sessionStore;
let cryptoStore;
let deviceLists = [];
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
deviceLists = [];
downloadSpy = expect.createSpy();
const mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
cryptoStore = new MemoryCryptoStore();
});
@@ -81,7 +85,7 @@ describe('DeviceList', function() {
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
const dl = new DeviceList(baseApis, cryptoStore, sessionStore, mockOlm);
deviceLists.push(dl);
return dl;
}
@@ -135,7 +139,7 @@ describe('DeviceList', function() {
}).then(() => {
// uh-oh; user restarts before second request completes. The new instance
// should know we never got a complete device list.
logger.log("Creating new devicelist to simulate app reload");
console.log("Creating new devicelist to simulate app reload");
downloadSpy.reset();
const dl2 = createTestDeviceList();
const queryDefer3 = Promise.defer();
+7 -5
View File
@@ -5,12 +5,12 @@ import Promise from 'bluebird';
import sdk from '../../../..';
import algorithms from '../../../../lib/crypto/algorithms';
import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils';
import OlmDevice from '../../../../lib/crypto/OlmDevice';
import Crypto from '../../../../lib/crypto';
import logger from '../../../../src/logger';
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
@@ -22,7 +22,7 @@ const Olm = global.Olm;
describe("MegolmDecryption", function() {
if (!global.Olm) {
logger.warn('Not running megolm unit tests: libolm not present');
console.warn('Not running megolm unit tests: libolm not present');
return;
}
@@ -32,7 +32,7 @@ describe("MegolmDecryption", function() {
let mockBaseApis;
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
await Olm.init();
@@ -40,9 +40,10 @@ describe("MegolmDecryption", function() {
mockBaseApis = {};
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(cryptoStore);
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
@@ -263,9 +264,10 @@ describe("MegolmDecryption", function() {
it("re-uses sessions for sequential messages", async function() {
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(cryptoStore);
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
olmDevice.verifySignature = expect.createSpy();
await olmDevice.init();
+6 -63
View File
@@ -1,5 +1,5 @@
/*
Copyright 2018,2019 New Vector Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,19 +17,18 @@ limitations under the License.
import '../../../olm-loader';
import expect from 'expect';
import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils';
import logger from '../../../../src/logger';
import OlmDevice from '../../../../lib/crypto/OlmDevice';
import olmlib from '../../../../lib/crypto/olmlib';
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
function makeOlmDevice() {
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(cryptoStore);
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
return olmDevice;
}
@@ -46,7 +45,7 @@ async function setupSession(initiator, opponent) {
describe("OlmDecryption", function() {
if (!global.Olm) {
logger.warn('Not running megolm unit tests: libolm not present');
console.warn('Not running megolm unit tests: libolm not present');
return;
}
@@ -54,7 +53,7 @@ describe("OlmDecryption", function() {
let bobOlmDevice;
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
await global.Olm.init();
@@ -83,61 +82,5 @@ describe("OlmDecryption", function() {
"The olm or proteus is an aquatic salamander in the family Proteidae",
);
});
it("creates only one session at a time", async function() {
// if we call ensureOlmSessionsForDevices multiple times, it should
// only try to create one session at a time, even if the server is
// slow
let count = 0;
const baseApis = {
claimOneTimeKeys: () => {
// simulate a very slow server (.5 seconds to respond)
count++;
return new Promise((resolve, reject) => {
setTimeout(reject, 500);
});
},
};
const devicesByUser = {
"@bob:example.com": [
DeviceInfo.fromStorage({
keys: {
"curve25519:ABCDEFG": "akey",
},
}, "ABCDEFG"),
],
};
function alwaysSucceed(promise) {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
// start two tasks that try to ensure that there's an olm session
const promises = Promise.all([
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUser,
)),
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUser,
)),
]);
await new Promise((resolve) => {
setTimeout(resolve, 200);
});
// after .2s, both tasks should have started, but one should be
// waiting on the other before trying to create a session, so
// claimOneTimeKeys should have only been called once
expect(count).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(count).toBe(2);
});
});
});
+10 -15
View File
@@ -28,7 +28,6 @@ import testUtils from '../../test-utils';
import OlmDevice from '../../../lib/crypto/OlmDevice';
import Crypto from '../../../lib/crypto';
import logger from '../../../src/logger';
const Olm = global.Olm;
@@ -75,14 +74,6 @@ const KEY_BACKUP_DATA = {
},
};
const BACKUP_INFO = {
algorithm: "m.megolm_backup.v1",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
function makeTestClient(sessionStore, cryptoStore) {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
@@ -113,7 +104,7 @@ function makeTestClient(sessionStore, cryptoStore) {
describe("MegolmBackup", function() {
if (!global.Olm) {
logger.warn('Not running megolm backup unit tests: libolm not present');
console.warn('Not running megolm backup unit tests: libolm not present');
return;
}
@@ -126,20 +117,26 @@ describe("MegolmBackup", function() {
let megolmDecryption;
beforeEach(async function() {
await Olm.init();
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockCrypto.backupKey = new Olm.PkEncryption();
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
);
mockCrypto.backupInfo = BACKUP_INFO;
mockCrypto.backupInfo = {
algorithm: "m.megolm_backup.v1",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
cryptoStore = new MemoryCryptoStore(mockStorage);
olmDevice = new OlmDevice(cryptoStore);
olmDevice = new OlmDevice(sessionStore, cryptoStore);
// we stub out the olm encryption bits
mockOlmLib = {};
@@ -439,7 +436,6 @@ describe("MegolmBackup", function() {
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
@@ -461,7 +457,6 @@ describe("MegolmBackup", function() {
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null, null, BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
@@ -1,146 +0,0 @@
/*
Copyright 2018-2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../../../../src/logger';
try {
global.Olm = require('olm');
} catch (e) {
logger.warn("unable to run device verification tests: libolm not available");
}
import expect from 'expect';
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
import {ShowQRCode, ScanQRCode} from '../../../../lib/crypto/verification/QRCode';
const Olm = global.Olm;
describe("QR code verification", function() {
if (!global.Olm) {
logger.warn('Not running device verification tests: libolm not present');
return;
}
beforeEach(async function() {
await Olm.init();
});
describe("showing", function() {
it("should emit an event to show a QR code", async function() {
const qrCode = new ShowQRCode({
getUserId: () => "@alice:example.com",
deviceId: "ABCDEFG",
getDeviceEd25519Key: function() {
return "device+ed25519+key";
},
});
const spy = expect.createSpy().andCall((e) => {
qrCode.done();
});
qrCode.on("show_qr_code", spy);
await qrCode.verify();
expect(spy).toHaveBeenCalledWith({
url: "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey",
});
});
});
describe("scanning", function() {
const QR_CODE_URL = "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey";
it("should verify when a QR code is sent", async function() {
const device = DeviceInfo.fromStorage(
{
algorithms: [],
keys: {
"curve25519:ABCDEFG": "device+curve25519+key",
"ed25519:ABCDEFG": "device+ed25519+key",
},
verified: false,
known: false,
unsigned: {},
},
"ABCDEFG",
);
const client = {
getStoredDevice: expect.createSpy().andReturn(device),
setDeviceVerified: expect.createSpy(),
};
const qrCode = new ScanQRCode(client);
qrCode.on("confirm_user_id", ({userId, confirm}) => {
if (userId === "@alice:example.com") {
confirm();
} else {
qrCode.cancel(new Error("Incorrect user"));
}
});
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
await qrCode.verify();
expect(client.getStoredDevice)
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
expect(client.setDeviceVerified)
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
});
it("should error when the user ID doesn't match", async function() {
const client = {
getStoredDevice: expect.createSpy(),
setDeviceVerified: expect.createSpy(),
};
const qrCode = new ScanQRCode(client, "@bob:example.com", "ABCDEFG");
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
const spy = expect.createSpy();
await qrCode.verify().catch(spy);
expect(spy).toHaveBeenCalled();
expect(client.getStoredDevice).toNotHaveBeenCalled();
expect(client.setDeviceVerified).toNotHaveBeenCalled();
});
it("should error if the key doesn't match", async function() {
const device = DeviceInfo.fromStorage(
{
algorithms: [],
keys: {
"curve25519:ABCDEFG": "device+curve25519+key",
"ed25519:ABCDEFG": "a+different+device+ed25519+key",
},
verified: false,
known: false,
unsigned: {},
},
"ABCDEFG",
);
const client = {
getStoredDevice: expect.createSpy().andReturn(device),
setDeviceVerified: expect.createSpy(),
};
const qrCode = new ScanQRCode(client, "@alice:example.com", "ABCDEFG");
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
const spy = expect.createSpy();
await qrCode.verify().catch(spy);
expect(spy).toHaveBeenCalled();
expect(client.getStoredDevice).toHaveBeenCalled();
expect(client.setDeviceVerified).toNotHaveBeenCalled();
});
});
});
@@ -1,82 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../../../../src/logger';
try {
global.Olm = require('olm');
} catch (e) {
logger.warn("unable to run device verification tests: libolm not available");
}
import expect from 'expect';
import {verificationMethods} from '../../../../lib/crypto';
import SAS from '../../../../lib/crypto/verification/SAS';
const Olm = global.Olm;
import {makeTestClients} from './util';
describe("verification request", function() {
if (!global.Olm) {
logger.warn('Not running device verification unit tests: libolm not present');
return;
}
beforeEach(async function() {
await Olm.init();
});
it("should request and accept a verification", async function() {
const [alice, bob] = await makeTestClients(
[
{userId: "@alice:example.com", deviceId: "Osborne2"},
{userId: "@bob:example.com", deviceId: "Dynabook"},
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice._crypto._deviceList.getRawStoredDevicesForUser = function() {
return {
Dynabook: {
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
};
};
alice.downloadKeys = () => {
return Promise.resolve();
};
bob.downloadKeys = () => {
return Promise.resolve();
};
bob.on("crypto.verification.request", (request) => {
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
bobVerifier.verify();
// XXX: Private function access (but it's a test, so we're okay)
bobVerifier._endTimer();
});
const aliceVerifier = await alice.requestVerification("@bob:example.com");
expect(aliceVerifier).toBeAn(SAS);
// XXX: Private function access (but it's a test, so we're okay)
aliceVerifier._endTimer();
});
});
-276
View File
@@ -1,276 +0,0 @@
/*
Copyright 2018-2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../../../../src/logger';
try {
global.Olm = require('olm');
} catch (e) {
logger.warn("unable to run device verification tests: libolm not available");
}
import expect from 'expect';
import sdk from '../../../..';
import {verificationMethods} from '../../../../lib/crypto';
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
import SAS from '../../../../lib/crypto/verification/SAS';
const Olm = global.Olm;
const MatrixEvent = sdk.MatrixEvent;
import {makeTestClients} from './util';
describe("SAS verification", function() {
if (!global.Olm) {
logger.warn('Not running device verification unit tests: libolm not present');
return;
}
beforeEach(async function() {
await Olm.init();
});
it("should error on an unexpected event", async function() {
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
sas.handleEvent(new MatrixEvent({
sender: "@alice:example.com",
type: "es.inquisition",
content: {},
}));
const spy = expect.createSpy();
await sas.verify()
.catch(spy);
expect(spy).toHaveBeenCalled();
// Cancel the SAS for cleanup (we started a verification, so abort)
sas.cancel();
});
describe("verification", function() {
let alice;
let bob;
let aliceSasEvent;
let bobSasEvent;
let aliceVerifier;
let bobPromise;
beforeEach(async function() {
[alice, bob] = await makeTestClients(
[
{userId: "@alice:example.com", deviceId: "Osborne2"},
{userId: "@bob:example.com", deviceId: "Dynabook"},
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice.setDeviceVerified = expect.createSpy();
alice.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key";
};
alice.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
"Dynabook",
);
};
alice.downloadKeys = () => {
return Promise.resolve();
};
bob.setDeviceVerified = expect.createSpy();
bob.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Osborne2": "alice+base64+ed25519+key",
},
},
"Osborne2",
);
};
bob.getDeviceEd25519Key = () => {
return "bob+base64+ed25519+key";
};
bob.downloadKeys = () => {
return Promise.resolve();
};
aliceSasEvent = null;
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bob.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
bobSasEvent = e;
} else {
try {
expect(e.sas).toEqual(aliceSasEvent.sas);
e.confirm();
aliceSasEvent.confirm();
} catch (error) {
e.mismatch();
aliceSasEvent.mismatch();
}
}
});
resolve(verifier);
});
});
aliceVerifier = alice.beginKeyVerification(
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
);
aliceVerifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!bobSasEvent) {
aliceSasEvent = e;
} else {
try {
expect(e.sas).toEqual(bobSasEvent.sas);
e.confirm();
bobSasEvent.confirm();
} catch (error) {
e.mismatch();
bobSasEvent.mismatch();
}
}
});
});
it("should verify a key", async function() {
let macMethod;
const origSendToDevice = alice.sendToDevice;
bob.sendToDevice = function(type, map) {
if (type === "m.key.verification.accept") {
macMethod = map[alice.getUserId()][alice.deviceId]
.message_authentication_code;
}
return origSendToDevice.call(this, type, map);
};
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
]);
// make sure that it uses the preferred method
expect(macMethod).toBe("hkdf-hmac-sha256");
// make sure Alice and Bob verified each other
expect(alice.setDeviceVerified)
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
expect(bob.setDeviceVerified)
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
});
it("should be able to verify using the old MAC", async function() {
// pretend that Alice can only understand the old (incorrect) MAC,
// and make sure that she can still verify with Bob
let macMethod;
const origSendToDevice = alice.sendToDevice;
alice.sendToDevice = function(type, map) {
if (type === "m.key.verification.start") {
// Note: this modifies not only the message that Bob
// receives, but also the copy of the message that Alice
// has, since it is the same object. If this does not
// happen, the verification will fail due to a hash
// commitment mismatch.
map[bob.getUserId()][bob.deviceId]
.message_authentication_codes = ['hmac-sha256'];
}
return origSendToDevice.call(this, type, map);
};
bob.sendToDevice = function(type, map) {
if (type === "m.key.verification.accept") {
macMethod = map[alice.getUserId()][alice.deviceId]
.message_authentication_code;
}
return origSendToDevice.call(this, type, map);
};
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
]);
expect(macMethod).toBe("hmac-sha256");
expect(alice.setDeviceVerified)
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
expect(bob.setDeviceVerified)
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
});
});
it("should send a cancellation message on error", async function() {
const [alice, bob] = await makeTestClients(
[
{userId: "@alice:example.com", deviceId: "Osborne2"},
{userId: "@bob:example.com", deviceId: "Dynabook"},
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice.setDeviceVerified = expect.createSpy();
alice.downloadKeys = () => {
return Promise.resolve();
};
bob.setDeviceVerified = expect.createSpy();
bob.downloadKeys = () => {
return Promise.resolve();
};
const bobPromise = new Promise((resolve, reject) => {
bob.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
e.mismatch();
});
resolve(verifier);
});
});
const aliceVerifier = alice.beginKeyVerification(
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
);
const aliceSpy = expect.createSpy();
const bobSpy = expect.createSpy();
await Promise.all([
aliceVerifier.verify().catch(aliceSpy),
bobPromise.then((verifier) => verifier.verify()).catch(bobSpy),
]);
expect(aliceSpy).toHaveBeenCalled();
expect(bobSpy).toHaveBeenCalled();
expect(alice.setDeviceVerified)
.toNotHaveBeenCalled();
expect(bob.setDeviceVerified)
.toNotHaveBeenCalled();
});
});
-63
View File
@@ -1,63 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import TestClient from '../../../TestClient';
import sdk from '../../../..';
const MatrixEvent = sdk.MatrixEvent;
export async function makeTestClients(userInfos, options) {
const clients = [];
const clientMap = {};
const sendToDevice = function(type, map) {
// console.log(this.getUserId(), "sends", type, map);
for (const [userId, devMap] of Object.entries(map)) {
if (userId in clientMap) {
for (const [deviceId, msg] of Object.entries(devMap)) {
if (deviceId in clientMap[userId]) {
const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
type: type,
content: msg,
});
setTimeout(
() => clientMap[userId][deviceId]
.emit("toDeviceEvent", event),
0,
);
}
}
}
}
};
for (const userInfo of userInfos) {
const client = (new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined,
options,
)).client;
if (!(userInfo.userId in clientMap)) {
clientMap[userInfo.userId] = {};
}
clientMap[userInfo.userId][userInfo.deviceId] = client;
client.sendToDevice = sendToDevice;
clients.push(client);
}
await Promise.all(clients.map((client) => client.initCrypto()));
return clients;
}
+1 -1
View File
@@ -18,7 +18,7 @@ describe("EventTimeline", function() {
let timeline;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
// XXX: this is a horrid hack; should use sinon or something instead to mock
const timelineSet = { room: { roomId: roomId }};
+2 -3
View File
@@ -21,11 +21,10 @@ import testUtils from '../test-utils';
import expect from 'expect';
import Promise from 'bluebird';
import logger from '../../src/logger';
describe("MatrixEvent", () => {
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe(".attemptDecryption", () => {
@@ -49,7 +48,7 @@ describe("MatrixEvent", () => {
const crypto = {
decryptEvent: function() {
++callCount;
logger.log(`decrypt: ${callCount}`);
console.log(`decrypt: ${callCount}`);
if (callCount == 1) {
// schedule a second decryption attempt while
// the first one is still running.
+1 -1
View File
@@ -12,7 +12,7 @@ describe("Filter", function() {
let filter;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
filter = new Filter(userId);
});
+5 -6
View File
@@ -24,7 +24,6 @@ const InteractiveAuth = sdk.InteractiveAuth;
const MatrixError = sdk.MatrixError;
import expect from 'expect';
import logger from '../../src/logger';
// Trivial client object to test interactive auth
// (we do not need TestClient here)
@@ -36,7 +35,7 @@ class FakeClient {
describe("InteractiveAuth", function() {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
it("should start an auth stage and complete it", function(done) {
@@ -65,7 +64,7 @@ describe("InteractiveAuth", function() {
// first we expect a call here
stateUpdated.andCall(function(stage) {
logger.log('aaaa');
console.log('aaaa');
expect(stage).toEqual("logintype");
ia.submitAuthDict({
type: "logintype",
@@ -76,7 +75,7 @@ describe("InteractiveAuth", function() {
// .. which should trigger a call here
const requestRes = {"a": "b"};
doRequest.andCall(function(authData) {
logger.log('cccc');
console.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
@@ -107,7 +106,7 @@ describe("InteractiveAuth", function() {
// first we expect a call to doRequest
doRequest.andCall(function(authData) {
logger.log("request1", authData);
console.log("request1", authData);
expect(authData).toEqual({});
const err = new MatrixError({
session: "sessionId",
@@ -133,7 +132,7 @@ describe("InteractiveAuth", function() {
// submitAuthDict should trigger another call to doRequest
doRequest.andCall(function(authData) {
logger.log("request2", authData);
console.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
-25
View File
@@ -1,25 +0,0 @@
import expect from 'expect';
import TestClient from '../TestClient';
describe('Login request', function() {
let client;
beforeEach(function() {
client = new TestClient();
});
afterEach(function() {
client.stop();
});
it('should store "access_token" and "user_id" if in response', async function() {
const response = { user_id: 1, access_token: Date.now().toString(16) };
client.httpBackend.when('POST', '/login').respond(200, response);
client.httpBackend.flush('/login', 1, 100);
await client.client.login('m.login.any', { user: 'test', password: '12312za' });
expect(client.client.getAccessToken()).toBe(response.access_token);
expect(client.client.getUserId()).toBe(response.user_id);
});
});
+11 -6
View File
@@ -7,7 +7,6 @@ const utils = require("../test-utils");
import expect from 'expect';
import lolex from 'lolex';
import logger from '../../src/logger';
describe("MatrixClient", function() {
const userId = "@alice:bar";
@@ -70,7 +69,7 @@ describe("MatrixClient", function() {
"MatrixClient[UT] RECV " + method + " " + path + " " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
);
logger.log(logLine);
console.log(logLine);
if (!next) { // no more things to return
if (pendingLookup) {
@@ -92,7 +91,7 @@ describe("MatrixClient", function() {
return pendingLookup.promise;
}
if (next.path === path && next.method === method) {
logger.log(
console.log(
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response",
);
@@ -125,7 +124,7 @@ describe("MatrixClient", function() {
}
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
clock = lolex.install();
scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
@@ -154,9 +153,12 @@ describe("MatrixClient", function() {
});
// FIXME: We shouldn't be yanking _http like this.
client._http = [
"authedRequest", "getContentUri", "request", "uploadContent",
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
client._http.authedRequest.andCall(httpReq);
client._http.authedRequestWithPrefix.andCall(httpReq);
client._http.requestWithPrefix.andCall(httpReq);
client._http.request.andCall(httpReq);
// set reasonable working defaults
@@ -178,6 +180,9 @@ describe("MatrixClient", function() {
client._http.authedRequest.andCall(function() {
return Promise.defer().promise;
});
client._http.authedRequestWithPrefix.andCall(function() {
return Promise.defer().promise;
});
});
it("should not POST /filter if a matching filter already exists", async function() {
@@ -348,7 +353,7 @@ describe("MatrixClient", function() {
function syncChecker(expectedStates, done) {
return function syncListener(state, old) {
const expected = expectedStates.shift();
logger.log(
console.log(
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected,
);
if (!expected) {
+3 -3
View File
@@ -15,7 +15,7 @@ describe("realtime-callbacks", function() {
}
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
clock = lolex.install();
const fakeDate = clock.Date;
callbacks.setNow(fakeDate.now.bind(fakeDate));
@@ -56,8 +56,8 @@ describe("realtime-callbacks", function() {
it("should set 'this' to the global object", function() {
let passed = false;
const callback = function() {
expect(this).toBe(global); // eslint-disable-line babel/no-invalid-this
expect(this.console).toBeTruthy(); // eslint-disable-line babel/no-invalid-this
expect(this).toBe(global); // eslint-disable-line no-invalid-this
expect(this.console).toBeTruthy(); // eslint-disable-line no-invalid-this
passed = true;
};
callbacks.setTimeout(callback);
+1 -1
View File
@@ -14,7 +14,7 @@ describe("RoomMember", function() {
let member;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
member = new RoomMember(roomId, userA);
});
+1 -1
View File
@@ -17,7 +17,7 @@ describe("RoomState", function() {
let state;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
state = new RoomState(roomId);
state.setStateEvents([
utils.mkMembership({ // userA joined
+2 -5
View File
@@ -19,7 +19,7 @@ describe("Room", function() {
let room;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
room = new Room(roomId);
// mock RoomStates
room.oldState = room.getLiveTimeline()._startState =
@@ -104,7 +104,7 @@ describe("Room", function() {
user_ids: [userA],
},
});
room.addEphemeralEvents([typing]);
room.addLiveEvents([typing]);
expect(room.currentState.setTypingEvent).toHaveBeenCalledWith(typing);
});
@@ -1318,9 +1318,6 @@ describe("Room", function() {
// events should already be MatrixEvents
return function(event) {return event;};
},
isCryptoEnabled() {
return true;
},
isRoomEncrypted: function() {
return false;
},
+28 -40
View File
@@ -26,7 +26,7 @@ describe("MatrixScheduler", function() {
});
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
clock = lolex.install();
scheduler = new MatrixScheduler(function(ev, attempts, err) {
if (retryFn) {
@@ -48,7 +48,7 @@ describe("MatrixScheduler", function() {
clock.uninstall();
});
it("should process events in a queue in a FIFO manner", async function() {
it("should process events in a queue in a FIFO manner", function(done) {
retryFn = function() {
return 0;
};
@@ -57,30 +57,28 @@ describe("MatrixScheduler", function() {
};
const deferA = Promise.defer();
const deferB = Promise.defer();
let yieldedA = false;
let resolvedA = false;
scheduler.setProcessFunction(function(event) {
if (yieldedA) {
if (resolvedA) {
expect(event).toEqual(eventB);
return deferB.promise;
} else {
yieldedA = true;
expect(event).toEqual(eventA);
return deferA.promise;
}
});
const abPromise = Promise.all([
scheduler.queueEvent(eventA),
scheduler.queueEvent(eventB),
]);
deferB.resolve({b: true});
deferA.resolve({a: true});
const [a, b] = await abPromise;
expect(a.a).toEqual(true);
expect(b.b).toEqual(true);
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB).done(function() {
expect(resolvedA).toBe(true);
done();
});
deferA.resolve({});
resolvedA = true;
deferB.resolve({});
});
it("should invoke the retryFn on failure and wait the amount of time specified",
async function() {
function(done) {
const waitTimeMs = 1500;
const retryDefer = Promise.defer();
retryFn = function() {
@@ -99,26 +97,24 @@ describe("MatrixScheduler", function() {
return defer.promise;
} else if (procCount === 2) {
// don't care about this defer
return new Promise();
return Promise.defer().promise;
}
expect(procCount).toBeLessThan(3);
});
scheduler.queueEvent(eventA);
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
// wait just long enough before it does
await Promise.resolve();
expect(procCount).toEqual(1);
defer.reject({});
await retryDefer.promise;
expect(procCount).toEqual(1);
clock.tick(waitTimeMs);
await Promise.resolve();
expect(procCount).toEqual(2);
retryDefer.promise.done(function() {
expect(procCount).toEqual(1);
clock.tick(waitTimeMs);
expect(procCount).toEqual(2);
done();
});
});
it("should give up if the retryFn on failure returns -1 and try the next event",
async function() {
function(done) {
// Queue A & B.
// Reject A and return -1 on retry.
// Expect B to be tried next and the promise for A to be rejected.
@@ -126,8 +122,8 @@ describe("MatrixScheduler", function() {
return -1;
};
queueFn = function() {
return "yep";
};
return "yep";
};
const deferA = Promise.defer();
const deferB = Promise.defer();
@@ -146,17 +142,13 @@ describe("MatrixScheduler", function() {
const globalA = scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
// wait just long enough before it does
await Promise.resolve();
expect(procCount).toEqual(1);
deferA.reject({});
try {
await globalA;
} catch(err) {
await Promise.resolve();
globalA.catch(function() {
expect(procCount).toEqual(2);
}
done();
});
});
it("should treat each queue separately", function(done) {
@@ -308,11 +300,7 @@ describe("MatrixScheduler", function() {
expect(ev).toEqual(eventA);
return defer.promise;
});
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
// wait just long enough before it does
Promise.resolve().then(() => {
expect(procCount).toEqual(1);
});
expect(procCount).toEqual(1);
});
it("should not call the processFn if there are no queued events", function() {
+1 -1
View File
@@ -26,7 +26,7 @@ describe("SyncAccumulator", function() {
let sa;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
sa = new SyncAccumulator({
maxTimelineEntries: 10,
});
+2 -2
View File
@@ -68,7 +68,7 @@ function createLinkedTimelines() {
describe("TimelineIndex", function() {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("minIndex", function() {
@@ -164,7 +164,7 @@ describe("TimelineWindow", function() {
}
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("load", function() {
+1 -1
View File
@@ -11,7 +11,7 @@ describe("User", function() {
let user;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
utils.beforeEach(this); // eslint-disable-line no-invalid-this
user = new User(userId);
});
+1 -1
View File
@@ -7,7 +7,7 @@ import expect from 'expect';
describe("utils", function() {
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("encodeParams", function() {
+1 -7
View File
@@ -34,18 +34,12 @@ export default class Reemitter {
}
reEmit(source, eventNames) {
// We include the source as the last argument for event handlers which may need it,
// such as read receipt listeners on the client class which won't have the context
// of the room.
const forSource = (handler, ...args) => {
handler(...args, source);
};
for (const eventName of eventNames) {
if (this.boundHandlers[eventName] === undefined) {
this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName);
}
const boundHandler = this.boundHandlers[eventName];
const boundHandler = forSource.bind(this, this.boundHandlers[eventName]);
source.on(eventName, boundHandler);
}
}
+97 -252
View File
@@ -17,7 +17,7 @@ limitations under the License.
/** @module auto-discovery */
import Promise from 'bluebird';
import logger from './logger';
const logger = require("./logger");
import { URL as NodeURL } from "url";
// Dev note: Auto discovery is part of the spec.
@@ -102,56 +102,6 @@ export class AutoDiscovery {
// translate the meaning of the states in the spec, but also
// support our own if needed.
static get ERROR_INVALID() {
return "Invalid homeserver discovery response";
}
static get ERROR_GENERIC_FAILURE() {
return "Failed to get autodiscovery configuration from server";
}
static get ERROR_INVALID_HS_BASE_URL() {
return "Invalid base_url for m.homeserver";
}
static get ERROR_INVALID_HOMESERVER() {
return "Homeserver URL does not appear to be a valid Matrix homeserver";
}
static get ERROR_INVALID_IS_BASE_URL() {
return "Invalid base_url for m.identity_server";
}
static get ERROR_INVALID_IDENTITY_SERVER() {
return "Identity server URL does not appear to be a valid identity server";
}
static get ERROR_INVALID_IS() {
return "Invalid identity server discovery response";
}
static get ERROR_MISSING_WELLKNOWN() {
return "No .well-known JSON file found";
}
static get ERROR_INVALID_JSON() {
return "Invalid JSON";
}
static get ALL_ERRORS() {
return [
AutoDiscovery.ERROR_INVALID,
AutoDiscovery.ERROR_GENERIC_FAILURE,
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
AutoDiscovery.ERROR_INVALID_IS,
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
AutoDiscovery.ERROR_INVALID_JSON,
];
}
/**
* The auto discovery failed. The client is expected to communicate
* the error to the user and refuse logging in.
@@ -187,176 +137,6 @@ export class AutoDiscovery {
*/
static get SUCCESS() { return "SUCCESS"; }
/**
* Validates and verifies client configuration information for purposes
* of logging in. Such information includes the homeserver URL
* and identity server URL the client would want. Additional details
* may also be included, and will be transparently brought into the
* response object unaltered.
* @param {string} wellknown The configuration object itself, as returned
* by the .well-known auto-discovery endpoint.
* @return {Promise<DiscoveredClientConfig>} Resolves to the verified
* configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails.
*/
static async fromDiscoveryConfig(wellknown) {
// Step 1 is to get the config, which is provided to us here.
// We default to an error state to make the first few checks easier to
// write. We'll update the properties of this object over the duration
// of this function.
const clientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
// Technically, we don't have a problem with the identity server
// config at this point.
state: AutoDiscovery.PROMPT,
error: null,
base_url: null,
},
};
if (!wellknown || !wellknown["m.homeserver"]) {
logger.error("No m.homeserver key in config");
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
return Promise.resolve(clientConfig);
}
if (!wellknown["m.homeserver"]["base_url"]) {
logger.error("No m.homeserver base_url in config");
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
return Promise.resolve(clientConfig);
}
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
// sure it points to a homeserver in Step 3.
const hsUrl = this._sanitizeWellKnownUrl(
wellknown["m.homeserver"]["base_url"],
);
if (!hsUrl) {
logger.error("Invalid base_url for m.homeserver");
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
return Promise.resolve(clientConfig);
}
// Step 3: Make sure the homeserver URL points to a homeserver.
const hsVersions = await this._fetchWellKnownObject(
`${hsUrl}/_matrix/client/versions`,
);
if (!hsVersions || !hsVersions.raw["versions"]) {
logger.error("Invalid /versions response");
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
// Supply the base_url to the caller because they may be ignoring liveliness
// errors, like this one.
clientConfig["m.homeserver"].base_url = hsUrl;
return Promise.resolve(clientConfig);
}
// Step 4: Now that the homeserver looks valid, update our client config.
clientConfig["m.homeserver"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: hsUrl,
};
// Step 5: Try to pull out the identity server configuration
let isUrl = "";
if (wellknown["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch. Note that we also fail the homeserver check in the
// object because according to the spec we're supposed to FAIL_ERROR
// if *anything* goes wrong with the IS validation, including invalid
// format. This means we're supposed to stop discovery completely.
const failingClientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID_IS,
// We'll provide the base_url that was previously valid for
// debugging purposes.
base_url: clientConfig["m.homeserver"].base_url,
},
"m.identity_server": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID_IS,
base_url: null,
},
};
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b.
isUrl = this._sanitizeWellKnownUrl(
wellknown["m.identity_server"]["base_url"],
);
if (!isUrl) {
logger.error("Invalid base_url for m.identity_server");
failingClientConfig["m.identity_server"].error =
AutoDiscovery.ERROR_INVALID_IS_BASE_URL;
return Promise.resolve(failingClientConfig);
}
// Step 5b: Verify there is an identity server listening on the provided
// URL.
const isResponse = await this._fetchWellKnownObject(
`${isUrl}/_matrix/identity/api/v1`,
);
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
logger.error("Invalid /api/v1 response");
failingClientConfig["m.identity_server"].error =
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
// Supply the base_url to the caller because they may be ignoring
// liveliness errors, like this one.
failingClientConfig["m.identity_server"].base_url = isUrl;
return Promise.resolve(failingClientConfig);
}
}
// Step 6: Now that the identity server is valid, or never existed,
// populate the IS section.
if (isUrl && isUrl.length > 0) {
clientConfig["m.identity_server"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: isUrl,
};
}
// Step 7: Copy any other keys directly into the clientConfig. This is for
// things like custom configuration of services.
Object.keys(wellknown)
.map((k) => {
if (k === "m.homeserver" || k === "m.identity_server") {
// Only copy selected parts of the config to avoid overwriting
// properties computed by the validation logic above.
const notProps = ["error", "state", "base_url"];
for (const prop of Object.keys(wellknown[k])) {
if (notProps.includes(prop)) continue;
clientConfig[k][prop] = wellknown[k][prop];
}
} else {
// Just copy the whole thing over otherwise
clientConfig[k] = wellknown[k];
}
});
// Step 8: Give the config to the caller (finally)
return Promise.resolve(clientConfig);
}
/**
* Attempts to automatically discover client configuration information
* prior to logging in. Such information includes the homeserver URL
@@ -391,7 +171,7 @@ export class AutoDiscovery {
const clientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID,
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
@@ -408,8 +188,10 @@ export class AutoDiscovery {
const wellknown = await this._fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
if (!wellknown || wellknown.action !== "SUCCESS") {
logger.error("No response or error when parsing .well-known");
if (!wellknown || wellknown.action !== "SUCCESS"
|| !wellknown.raw["m.homeserver"]
|| !wellknown.raw["m.homeserver"]["base_url"]) {
logger.error("No m.homeserver key in well-known response");
if (wellknown.reason) logger.error(wellknown.reason);
if (wellknown.action === "IGNORE") {
clientConfig["m.homeserver"] = {
@@ -420,33 +202,99 @@ export class AutoDiscovery {
} else {
// this can only ever be FAIL_PROMPT at this point.
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
}
return Promise.resolve(clientConfig);
}
// Step 2: Validate and parse the config
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw);
}
/**
* Gets the raw discovery client configuration for the given domain name.
* Should only be used if there's no validation to be done on the resulting
* object, otherwise use findClientConfig().
* @param {string} domain The domain to get the client config for.
* @returns {Promise<object>} Resolves to the domain's client config. Can
* be an empty object.
*/
static async getRawClientConfig(domain) {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
// sure it points to a homeserver in Step 3.
const hsUrl = this._sanitizeWellKnownUrl(
wellknown.raw["m.homeserver"]["base_url"],
);
if (!hsUrl) {
logger.error("Invalid base_url for m.homeserver");
return Promise.resolve(clientConfig);
}
const response = await this._fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
// Step 3: Make sure the homeserver URL points to a homeserver.
const hsVersions = await this._fetchWellKnownObject(
`${hsUrl}/_matrix/client/versions`,
);
if (!response) return {};
return response.raw || {};
if (!hsVersions || !hsVersions.raw["versions"]) {
logger.error("Invalid /versions response");
return Promise.resolve(clientConfig);
}
// Step 4: Now that the homeserver looks valid, update our client config.
clientConfig["m.homeserver"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: hsUrl,
};
// Step 5: Try to pull out the identity server configuration
let isUrl = "";
if (wellknown.raw["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch. Note that we also fail the homeserver check in the
// object because according to the spec we're supposed to FAIL_ERROR
// if *anything* goes wrong with the IS validation, including invalid
// format. This means we're supposed to stop discovery completely.
const failingClientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: "Invalid identity server discovery response",
// We'll provide the base_url that was previously valid for
// debugging purposes.
base_url: clientConfig["m.homeserver"].base_url,
},
"m.identity_server": {
state: AutoDiscovery.FAIL_ERROR,
error: "Invalid identity server discovery response",
base_url: null,
},
};
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b.
isUrl = this._sanitizeWellKnownUrl(
wellknown.raw["m.identity_server"]["base_url"],
);
if (!isUrl) {
logger.error("Invalid base_url for m.identity_server");
return Promise.resolve(failingClientConfig);
}
// Step 5b: Verify there is an identity server listening on the provided
// URL.
const isResponse = await this._fetchWellKnownObject(
`${isUrl}/_matrix/identity/api/v1`,
);
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
logger.error("Invalid /api/v1 response");
return Promise.resolve(failingClientConfig);
}
}
// Step 6: Now that the identity server is valid, or never existed,
// populate the IS section.
if (isUrl && isUrl.length > 0) {
clientConfig["m.identity_server"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: isUrl,
};
}
// Step 7: Copy any other keys directly into the clientConfig. This is for
// things like custom configuration of services.
Object.keys(wellknown.raw)
.filter((k) => k !== "m.homeserver" && k !== "m.identity_server")
.map((k) => clientConfig[k] = wellknown.raw[k]);
// Step 8: Give the config to the caller (finally)
return Promise.resolve(clientConfig);
}
/**
@@ -510,14 +358,14 @@ export class AutoDiscovery {
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request(
{ method: "GET", uri: url, timeout: 5000 },
{ method: "GET", uri: url },
(err, response, body) => {
if (err || response.statusCode < 200 || response.statusCode >= 300) {
let action = "FAIL_PROMPT";
let reason = (err ? err.message : null) || "General failure";
if (response.statusCode === 404) {
action = "IGNORE";
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
reason = "No .well-known JSON file found";
}
resolve({raw: {}, action: action, reason: reason, error: err});
return;
@@ -526,15 +374,12 @@ export class AutoDiscovery {
try {
resolve({raw: JSON.parse(body), action: "SUCCESS"});
} catch (e) {
let reason = AutoDiscovery.ERROR_INVALID;
if (e.name === "SyntaxError") {
reason = AutoDiscovery.ERROR_INVALID_JSON;
}
let reason = "General failure";
if (e.name === "SyntaxError") reason = "Invalid JSON";
resolve({
raw: {},
action: "FAIL_PROMPT",
reason: reason,
error: e,
reason: reason, error: e,
});
}
},
+95 -637
View File
@@ -1,8 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -25,23 +23,8 @@ limitations under the License.
* @module base-apis
*/
import { SERVICE_TYPES } from './service-types';
import logger from './logger';
const httpApi = require("./http-api");
const utils = require("./utils");
const PushProcessor = require("./pushprocessor");
function termsUrlForService(serviceType, baseUrl) {
switch (serviceType) {
case SERVICE_TYPES.IS:
return baseUrl + httpApi.PREFIX_IDENTITY_V2 + '/terms';
case SERVICE_TYPES.IM:
return baseUrl + '/_matrix/integrations/v1/terms';
default:
throw new Error('Unsupported service type');
}
}
/**
* Low-level wrappers for the Matrix APIs
@@ -63,15 +46,6 @@ function termsUrlForService(serviceType, baseUrl) {
*
* @param {string} opts.accessToken The access_token for this user.
*
* @param {IdentityServerProvider} [opts.identityServer]
* Optional. A provider object with one function `getAccessToken`, which is a
* callback that returns a Promise<String> of an identity access token to supply
* with identity requests. If the object is unset, no access token will be
* supplied.
* See also https://github.com/vector-im/riot-web/issues/10615 which seeks to
* replace the previous approach of manual access tokens params with this
* callback throughout the SDK.
*
* @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of
* time to wait before timing out HTTP requests. If not specified, there is no
* timeout.
@@ -88,7 +62,6 @@ function MatrixBaseApis(opts) {
this.baseUrl = opts.baseUrl;
this.idBaseUrl = opts.idBaseUrl;
this.identityServer = opts.identityServer;
const httpOpts = {
baseUrl: opts.baseUrl,
@@ -127,15 +100,6 @@ MatrixBaseApis.prototype.getIdentityServerUrl = function(stripProto=false) {
return this.idBaseUrl;
};
/**
* Set the Identity Server URL of this client
* @param {string} url New Identity Server URL
*/
MatrixBaseApis.prototype.setIdentityServerUrl = function(url) {
this.idBaseUrl = utils.ensureNoTrailingSlash(url);
this._http.setIdBaseUrl(this.idBaseUrl);
};
/**
* Get the access token associated with this account.
* @return {?String} The access_token or null
@@ -187,14 +151,13 @@ MatrixBaseApis.prototype.isUsernameAvailable = function(username) {
* threepid uses during registration in the ID server. Set 'msisdn' to
* true to bind msisdn.
* @param {string} guestAccessToken
* @param {string} inhibitLogin
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.register = function(
username, password,
sessionId, auth, bindThreepids, guestAccessToken, inhibitLogin,
sessionId, auth, bindThreepids, guestAccessToken,
callback,
) {
// backwards compat
@@ -203,10 +166,6 @@ MatrixBaseApis.prototype.register = function(
} else if (bindThreepids === null || bindThreepids === undefined) {
bindThreepids = {};
}
if (typeof inhibitLogin === 'function') {
callback = inhibitLogin;
inhibitLogin = undefined;
}
if (auth === undefined || auth === null) {
auth = {};
@@ -233,9 +192,6 @@ MatrixBaseApis.prototype.register = function(
if (guestAccessToken !== undefined && guestAccessToken !== null) {
params.guest_access_token = guestAccessToken;
}
if (inhibitLogin !== undefined && inhibitLogin !== null) {
params.inhibit_login = inhibitLogin;
}
// Temporary parameter added to make the register endpoint advertise
// msisdn flows. This exists because there are clients that break
// when given stages they don't recognise. This parameter will cease
@@ -307,7 +263,8 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) {
return this._http.authedRequest(
(error, response) => {
if (response && response.access_token && response.user_id) {
if (loginType === "m.login.password" && response &&
response.access_token && response.user_id) {
this._http.opts.accessToken = response.access_token;
this.credentials = {
userId: response.user_id,
@@ -427,8 +384,9 @@ MatrixBaseApis.prototype.deactivateAccount = function(auth, erase) {
body.erase = erase;
}
return this._http.authedRequest(
return this._http.authedRequestWithPrefix(
undefined, "POST", '/account/deactivate', undefined, body,
httpApi.PREFIX_R0,
);
};
@@ -473,37 +431,6 @@ MatrixBaseApis.prototype.createRoom = function(options, callback) {
callback, "POST", "/createRoom", undefined, options,
);
};
/**
* Fetches relations for a given event
* @param {string} roomId the room of the event
* @param {string} eventId the id of the event
* @param {string} relationType the rel_type of the relations requested
* @param {string} eventType the event type of the relations requested
* @param {Object} opts options with optional values for the request.
* @param {Object} opts.from the pagination token returned from a previous request as `next_batch` to return following relations.
* @return {Object} the response, with chunk and next_batch.
*/
MatrixBaseApis.prototype.fetchRelations =
async function(roomId, eventId, relationType, eventType, opts) {
const queryParams = {};
if (opts.from) {
queryParams.from = opts.from;
}
const queryString = utils.encodeParams(queryParams);
const path = utils.encodeUri(
"/rooms/$roomId/relations/$eventId/$relationType/$eventType?" + queryString, {
$roomId: roomId,
$eventId: eventId,
$relationType: relationType,
$eventType: eventType,
});
const response = await this._http.authedRequest(
undefined, "GET", path, null, null, {
prefix: httpApi.PREFIX_UNSTABLE,
},
);
return response;
};
/**
* @param {string} roomId
@@ -961,6 +888,21 @@ MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, s
);
};
/**
* @param {string} roomId
* @param {string} eventId
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.redactEvent = function(roomId, eventId, callback) {
const path = utils.encodeUri("/rooms/$roomId/redact/$eventId", {
$roomId: roomId,
$eventId: eventId,
});
return this._http.authedRequest(callback, "POST", path, undefined, {});
};
/**
* @param {string} roomId
* @param {Number} limit
@@ -993,13 +935,10 @@ MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
* @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.
* @param {object} opts Options for the read markers.
* @param {object} opts.hidden True to hide the read receipt from other users. <b>This
* property is currently unstable and may change in the future.</b>
* @return {module:client.Promise} Resolves: the empty object, {}.
*/
MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
function(roomId, rmEventId, rrEventId, opts) {
function(roomId, rmEventId, rrEventId) {
const path = utils.encodeUri("/rooms/$roomId/read_markers", {
$roomId: roomId,
});
@@ -1007,7 +946,6 @@ MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
const content = {
"m.fully_read": rmEventId,
"m.read": rrEventId,
"m.hidden": Boolean(opts ? opts.hidden : false),
};
return this._http.authedRequest(
@@ -1339,16 +1277,10 @@ MatrixBaseApis.prototype.getThreePids = function(callback) {
};
/**
* Add a 3PID to your homeserver account and optionally bind it to an identity
* server as well. An identity server is required as part of the `creds` object.
*
* This API is deprecated, and you should instead use `addThreePidOnly`
* for homeservers that support it.
*
* @param {Object} creds
* @param {boolean} bind
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: on success
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
@@ -1362,75 +1294,6 @@ MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
);
};
/**
* Add a 3PID to your homeserver account. This API does not use an identity
* server, as the homeserver is expected to handle 3PID ownership validation.
*
* You can check whether a homeserver supports this API via
* `doesServerSupportSeparateAddAndBind`.
*
* @param {Object} data A object with 3PID validation data from having called
* `account/3pid/<medium>/requestToken` on the homeserver.
* @return {module:client.Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addThreePidOnly = function(data) {
const path = "/account/3pid/add";
return this._http.authedRequest(
undefined, "POST", path, null, data, {
prefix: httpApi.PREFIX_UNSTABLE,
},
);
};
/**
* Bind a 3PID for discovery onto an identity server via the homeserver. The
* identity server handles 3PID ownership validation and the homeserver records
* the new binding to track where all 3PIDs for the account are bound.
*
* You can check whether a homeserver supports this API via
* `doesServerSupportSeparateAddAndBind`.
*
* @param {Object} data A object with 3PID validation data from having called
* `validate/<medium>/requestToken` on the identity server. It should also
* contain `id_server` and `id_access_token` fields as well.
* @return {module:client.Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.bindThreePid = function(data) {
const path = "/account/3pid/bind";
return this._http.authedRequest(
undefined, "POST", path, null, data, {
prefix: httpApi.PREFIX_UNSTABLE,
},
);
};
/**
* Unbind a 3PID for discovery on an identity server via the homeserver. The
* homeserver removes its record of the binding to keep an updated record of
* where all 3PIDs for the account are bound.
*
* @param {string} medium The threepid medium (eg. 'email')
* @param {string} address The threepid address (eg. 'bob@example.com')
* this must be as returned by getThreePids.
* @return {module:client.Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.unbindThreePid = function(medium, address) {
const path = "/account/3pid/unbind";
const data = {
medium,
address,
id_server: this.getIdentityServerUrl(true),
};
return this._http.authedRequest(
undefined, "POST", path, null, data, {
prefix: httpApi.PREFIX_UNSTABLE,
},
);
};
/**
* @param {string} medium The threepid medium (eg. 'email')
* @param {string} address The threepid address (eg. 'bob@example.com')
@@ -1445,7 +1308,9 @@ MatrixBaseApis.prototype.deleteThreePid = function(medium, address) {
'medium': medium,
'address': address,
};
return this._http.authedRequest(undefined, "POST", path, null, data);
return this._http.authedRequestWithPrefix(
undefined, "POST", path, null, data, httpApi.PREFIX_UNSTABLE,
);
};
/**
@@ -1478,8 +1343,10 @@ MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback)
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getDevices = function() {
return this._http.authedRequest(
undefined, 'GET', "/devices", undefined, undefined,
const path = "/devices";
return this._http.authedRequestWithPrefix(
undefined, "GET", path, undefined, undefined,
httpApi.PREFIX_UNSTABLE,
);
};
@@ -1496,7 +1363,11 @@ MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
$device_id: device_id,
});
return this._http.authedRequest(undefined, "PUT", path, undefined, body);
return this._http.authedRequestWithPrefix(
undefined, "PUT", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
};
/**
@@ -1518,7 +1389,10 @@ MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
body.auth = auth;
}
return this._http.authedRequest(undefined, "DELETE", path, undefined, body);
return this._http.authedRequestWithPrefix(
undefined, "DELETE", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
};
/**
@@ -1536,8 +1410,10 @@ MatrixBaseApis.prototype.deleteMultipleDevices = function(devices, auth) {
body.auth = auth;
}
const path = "/delete_devices";
return this._http.authedRequest(undefined, "POST", path, undefined, body);
return this._http.authedRequestWithPrefix(
undefined, "POST", "/delete_devices", undefined, body,
httpApi.PREFIX_UNSTABLE,
);
};
@@ -1579,9 +1455,7 @@ MatrixBaseApis.prototype.setPusher = function(pusher, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getPushRules = function(callback) {
return this._http.authedRequest(callback, "GET", "/pushrules/").then(rules => {
return PushProcessor.rewriteDefaultRules(rules);
});
return this._http.authedRequest(callback, "GET", "/pushrules/");
};
/**
@@ -1715,7 +1589,9 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
} else {
path = "/keys/upload";
}
return this._http.authedRequest(callback, "POST", path, undefined, content);
return this._http.authedRequestWithPrefix(
callback, "POST", path, undefined, content, httpApi.PREFIX_UNSTABLE,
);
};
/**
@@ -1750,7 +1626,10 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
content.device_keys[u] = {};
});
return this._http.authedRequest(undefined, "POST", "/keys/query", undefined, content);
return this._http.authedRequestWithPrefix(
undefined, "POST", "/keys/query", undefined, content,
httpApi.PREFIX_UNSTABLE,
);
};
/**
@@ -1778,8 +1657,10 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
query[deviceId] = key_algorithm;
}
const content = {one_time_keys: queries};
const path = "/keys/claim";
return this._http.authedRequest(undefined, "POST", path, undefined, content);
return this._http.authedRequestWithPrefix(
undefined, "POST", "/keys/claim", undefined, content,
httpApi.PREFIX_UNSTABLE,
);
};
/**
@@ -1798,8 +1679,10 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
to: newToken,
};
const path = "/keys/changes";
return this._http.authedRequest(undefined, "GET", path, qps, undefined);
return this._http.authedRequestWithPrefix(
undefined, "GET", "/keys/changes", qps, undefined,
httpApi.PREFIX_UNSTABLE,
);
};
@@ -1807,36 +1690,10 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
// ==========================
/**
* Register with an Identity Server using the OpenID token from the user's
* Homeserver, which can be retrieved via
* {@link module:client~MatrixClient#getOpenIdToken}.
* Requests an email verification token directly from an Identity Server.
*
* Note that the `/account/register` endpoint (as well as IS authentication in
* general) was added as part of the v2 API version.
*
* @param {object} hsOpenIdToken
* @return {module:client.Promise} Resolves: with object containing an Identity
* Server access token.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.registerWithIdentityServer = function(hsOpenIdToken) {
if (!this.idBaseUrl) {
throw new Error("No Identity Server base URL set");
}
const uri = this.idBaseUrl + httpApi.PREFIX_IDENTITY_V2 + "/account/register";
return this._http.requestOtherUrl(
undefined, "POST", uri,
null, hsOpenIdToken,
);
};
/**
* Requests an email verification token directly from an identity server.
*
* This API is used as part of binding an email for discovery on an identity
* server. The validation data that results should be passed to the
* `bindThreePid` method to complete the binding process.
* Note that the Home Server offers APIs to proxy this API for specific
* situations, allowing for better feedback to the user.
*
* @param {string} email The email address to request a token for
* @param {string} clientSecret A secret binary string generated by the client.
@@ -1848,122 +1705,26 @@ MatrixBaseApis.prototype.registerWithIdentityServer = function(hsOpenIdToken) {
* @param {string} nextLink Optional If specified, the client will be redirected
* to this link after validation.
* @param {module:client.callback} callback Optional.
* @param {string} identityAccessToken The `access_token` field of the identity
* server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if no identity server is set
* @throws Error if No ID server is set
*/
MatrixBaseApis.prototype.requestEmailToken = async function(
email,
clientSecret,
sendAttempt,
nextLink,
callback,
identityAccessToken,
) {
MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret,
sendAttempt, nextLink, callback) {
const params = {
client_secret: clientSecret,
email: email,
send_attempt: sendAttempt,
next_link: nextLink,
};
try {
const response = await this._http.idServerRequest(
undefined, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, response);
return response;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
throw err;
}
return this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
};
/**
* Requests a MSISDN verification token directly from an identity server.
*
* This API is used as part of binding a MSISDN for discovery on an identity
* server. The validation data that results should be passed to the
* `bindThreePid` method to complete the binding process.
*
* @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in
* which phoneNumber should be parsed relative to.
* @param {string} phoneNumber The phone number, in national or international
* format
* @param {string} clientSecret A secret binary string generated by the client.
* It is recommended this be around 16 ASCII characters.
* @param {number} sendAttempt If an identity server sees a duplicate request
* with the same sendAttempt, it will not send another SMS.
* To request another SMS to be sent, use a larger value for
* the sendAttempt param as was used in the previous request.
* @param {string} nextLink Optional If specified, the client will be redirected
* to this link after validation.
* @param {module:client.callback} callback Optional.
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if no identity server is set
*/
MatrixBaseApis.prototype.requestMsisdnToken = async function(
phoneCountry,
phoneNumber,
clientSecret,
sendAttempt,
nextLink,
callback,
identityAccessToken,
) {
const params = {
client_secret: clientSecret,
country: phoneCountry,
phone_number: phoneNumber,
send_attempt: sendAttempt,
next_link: nextLink,
};
try {
const response = await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/requestToken",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, response);
return response;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "POST", "/validate/msisdn/requestToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
throw err;
}
};
/**
* Submits a MSISDN token to the identity server
* Submits an MSISDN token to the identity server
*
* This is used when submitting the code sent by SMS to a phone number.
* The ID server has an equivalent API for email but the js-sdk does
@@ -1973,324 +1734,47 @@ MatrixBaseApis.prototype.requestMsisdnToken = async function(
* @param {string} sid The sid given in the response to requestToken
* @param {string} clientSecret A secret binary string generated by the client.
* This must be the same value submitted in the requestToken call.
* @param {string} msisdnToken The MSISDN token, as enetered by the user.
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
* @param {string} token The token, as enetered by the user.
*
* @return {module:client.Promise} Resolves: Object, currently with no parameters.
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if No ID server is set
*/
MatrixBaseApis.prototype.submitMsisdnToken = async function(
sid,
clientSecret,
msisdnToken,
identityAccessToken,
) {
MatrixBaseApis.prototype.submitMsisdnToken = function(sid, clientSecret, token) {
const params = {
sid: sid,
client_secret: clientSecret,
token: msisdnToken,
token: token,
};
try {
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
}
throw err;
}
};
/**
* Submits a MSISDN token to an arbitrary URL.
*
* This is used when submitting the code sent by SMS to a phone number in the
* newer 3PID flow where the homeserver validates 3PID ownership (as part of
* `requestAdd3pidMsisdnToken`). The homeserver response may include a
* `submit_url` to specify where the token should be sent, and this helper can
* be used to pass the token to this URL.
*
* @param {string} url The URL to submit the token to
* @param {string} sid The sid given in the response to requestToken
* @param {string} clientSecret A secret binary string generated by the client.
* This must be the same value submitted in the requestToken call.
* @param {string} msisdnToken The MSISDN token, as enetered by the user.
*
* @return {module:client.Promise} Resolves: Object, currently with no parameters.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.submitMsisdnTokenOtherUrl = function(
url,
sid,
clientSecret,
msisdnToken,
) {
const params = {
sid: sid,
client_secret: clientSecret,
token: msisdnToken,
};
return this._http.requestOtherUrl(
undefined, "POST", url, undefined, params,
);
};
/**
* Gets the V2 hashing information from the identity server. Primarily useful for
* lookups.
* @param {string} identityAccessToken The access token for the identity server.
* @returns {Promise<object>} The hashing information for the identity server.
*/
MatrixBaseApis.prototype.getIdentityHashDetails = function(identityAccessToken) {
return this._http.idServerRequest(
undefined, "GET", "/hash_details",
null, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
};
/**
* Performs a hashed lookup of addresses against the identity server. This is
* only supported on identity servers which have at least the version 2 API.
* @param {Array<Array<string,string>>} addressPairs An array of 2 element arrays.
* The first element of each pair is the address, the second is the 3PID medium.
* Eg: ["email@example.org", "email"]
* @param {string} identityAccessToken The access token for the identity server.
* @returns {Promise<Array<{address, mxid}>>} A collection of address mappings to
* found MXIDs. Results where no user could be found will not be listed.
*/
MatrixBaseApis.prototype.identityHashedLookup = async function(
addressPairs, // [["email@example.org", "email"], ["10005550000", "msisdn"]]
identityAccessToken,
) {
const params = {
// addresses: ["email@example.org", "10005550000"],
// algorithm: "sha256",
// pepper: "abc123"
};
// Get hash information first before trying to do a lookup
const hashes = await this.getIdentityHashDetails(identityAccessToken);
if (!hashes || !hashes['lookup_pepper'] || !hashes['algorithms']) {
throw new Error("Unsupported identity server: bad response");
}
params['pepper'] = hashes['lookup_pepper'];
const localMapping = {
// hashed identifier => plain text address
// For use in this function's return format
};
// When picking an algorithm, we pick the hashed over no hashes
if (hashes['algorithms'].includes('sha256')) {
// Abuse the olm hashing
const olmutil = new global.Olm.Utility();
params["addresses"] = addressPairs.map(p => {
const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
const med = p[1].toLowerCase();
const hashed = olmutil.sha256(`${addr} ${med} ${params['pepper']}`)
.replace(/\+/g, '-').replace(/\//g, '_'); // URL-safe base64
// Map the hash to a known (case-sensitive) address. We use the case
// sensitive version because the caller might be expecting that.
localMapping[hashed] = p[0];
return hashed;
});
params["algorithm"] = "sha256";
} else if (hashes['algorithms'].includes('none')) {
params["addresses"] = addressPairs.map(p => {
const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
const med = p[1].toLowerCase();
const unhashed = `${addr} ${med}`;
// Map the unhashed values to a known (case-sensitive) address. We use
// the case sensitive version because the caller might be expecting that.
localMapping[unhashed] = p[0];
return unhashed;
});
params["algorithm"] = "none";
} else {
throw new Error("Unsupported identity server: unknown hash algorithm");
}
const response = await this._http.idServerRequest(
undefined, "POST", "/lookup",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
if (!response || !response['mappings']) return []; // no results
const foundAddresses = [/* {address: "plain@example.org", mxid} */];
for (const hashed of Object.keys(response['mappings'])) {
const mxid = response['mappings'][hashed];
const plainAddress = localMapping[hashed];
if (!plainAddress) {
throw new Error("Identity server returned more results than expected");
}
foundAddresses.push({address: plainAddress, mxid});
}
return foundAddresses;
};
/**
* Looks up the public Matrix ID mapping for a given 3rd party
* identifier from the Identity Server
*
* @param {string} medium The medium of the threepid, eg. 'email'
* @param {string} address The textual address of the threepid
* @param {module:client.callback} callback Optional.
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: A threepid mapping
* object or the empty object if no mapping
* exists
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.lookupThreePid = async function(
medium,
address,
callback,
identityAccessToken,
) {
try {
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
const response = await this.identityHashedLookup(
[[address, medium]], identityAccessToken,
);
const result = response.find(p => p.address === address);
if (!result) {
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, {});
return {};
}
const mapping = {
address,
medium,
mxid: result.mxid,
// We can't reasonably fill these parameters:
// not_before
// not_after
// ts
// signatures
};
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, mapping);
return mapping;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
const params = {
medium: medium,
address: address,
};
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "GET", "/lookup",
params, httpApi.PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err, undefined);
throw err;
}
};
/**
* Looks up the public Matrix ID mappings for multiple 3PIDs.
*
* @param {Array.<Array.<string>>} query Array of arrays containing
* [medium, address]
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: Lookup results from IS.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.bulkLookupThreePids = async function(
query,
identityAccessToken,
) {
try {
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
const response = await this.identityHashedLookup(
// We have to reverse the query order to get [address, medium] pairs
query.map(p => [p[1], p[0]]), identityAccessToken,
);
const v1results = [];
for (const mapping of response) {
const originalQuery = query.find(p => p[1] === mapping.address);
if (!originalQuery) {
throw new Error("Identity sever returned unexpected results");
}
v1results.push([
originalQuery[0], // medium
mapping.address,
mapping.mxid,
]);
}
return {threepids: v1results};
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
const params = {
threepids: query,
};
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
undefined, "POST", "/bulk_lookup", params,
httpApi.PREFIX_IDENTITY_V1, identityAccessToken,
);
}
throw err;
}
};
/**
* Get account info from the Identity Server. This is useful as a neutral check
* to verify that other APIs are likely to approve access by testing that the
* token is valid, terms have been agreed, etc.
*
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: an object with account info.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getIdentityAccount = function(
identityAccessToken,
) {
MatrixBaseApis.prototype.lookupThreePid = function(medium, address, callback) {
const params = {
medium: medium,
address: address,
};
return this._http.idServerRequest(
undefined, "GET", "/account",
undefined, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
callback, "GET", "/lookup",
params, httpApi.PREFIX_IDENTITY_V1,
);
};
// Direct-to-device messaging
// ==========================
@@ -2316,7 +1800,10 @@ MatrixBaseApis.prototype.sendToDevice = function(
messages: contentMap,
};
return this._http.authedRequest(undefined, "PUT", path, undefined, body);
return this._http.authedRequestWithPrefix(
undefined, "PUT", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
};
// Third party Lookup API
@@ -2328,8 +1815,9 @@ MatrixBaseApis.prototype.sendToDevice = function(
* @return {module:client.Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
return this._http.authedRequest(
return this._http.authedRequestWithPrefix(
undefined, "GET", "/thirdparty/protocols", undefined, undefined,
httpApi.PREFIX_UNSTABLE,
).then((response) => {
// sanity check
if (!response || typeof(response) !== 'object') {
@@ -2354,7 +1842,10 @@ MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
$protocol: protocol,
});
return this._http.authedRequest(undefined, "GET", path, params, undefined);
return this._http.authedRequestWithPrefix(
undefined, "GET", path, params, undefined,
httpApi.PREFIX_UNSTABLE,
);
};
/**
@@ -2370,45 +1861,12 @@ MatrixBaseApis.prototype.getThirdpartyUser = function(protocol, params) {
$protocol: protocol,
});
return this._http.authedRequest(undefined, "GET", path, params, undefined);
};
MatrixBaseApis.prototype.getTerms = function(serviceType, baseUrl) {
const url = termsUrlForService(serviceType, baseUrl);
return this._http.requestOtherUrl(
undefined, 'GET', url,
return this._http.authedRequestWithPrefix(
undefined, "GET", path, params, undefined,
httpApi.PREFIX_UNSTABLE,
);
};
MatrixBaseApis.prototype.agreeToTerms = function(
serviceType, baseUrl, accessToken, termsUrls,
) {
const url = termsUrlForService(serviceType, baseUrl);
const headers = {
Authorization: "Bearer " + accessToken,
};
return this._http.requestOtherUrl(
undefined, 'POST', url, null, { user_accepts: termsUrls }, { headers },
);
};
/**
* Reports an event as inappropriate to the server, which may then notify the appropriate people.
* @param {string} roomId The room in which the event being reported is located.
* @param {string} eventId The event to report.
* @param {number} score The score to rate this content as where -100 is most offensive and 0 is inoffensive.
* @param {string} reason The reason the content is being reported. May be blank.
* @returns {module:client.Promise} Resolves to an empty object if successful
*/
MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, score, reason) {
const path = utils.encodeUri("/rooms/$roomId/report/$eventId", {
$roomId: roomId,
$eventId: eventId,
});
return this._http.authedRequest(undefined, "POST", path, null, {score, reason});
};
/**
* MatrixBaseApis object
*/
+130 -865
View File
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -47,14 +47,14 @@ module.exports = {
}
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/r0/download/";
let prefix = "/_matrix/media/v1/download/";
const params = {};
if (width) {
params.width = Math.round(width);
params.width = width;
}
if (height) {
params.height = Math.round(height);
params.height = height;
}
if (resizeMethod) {
params.method = resizeMethod;
@@ -62,7 +62,7 @@ module.exports = {
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/r0/thumbnail/";
prefix = "/_matrix/media/v1/thumbnail/";
}
const fragmentOffset = serverAndMediaId.indexOf("#");
@@ -83,7 +83,6 @@ module.exports = {
* @param {Number} width The desired width of the image in pixels. Default: 96.
* @param {Number} height The desired height of the image in pixels. Default: 96.
* @return {string} The complete URL to the identicon.
* @deprecated This is no longer in the specification.
*/
getIdenticonUri: function(baseUrl, identiconString, width, height) {
if (!identiconString) {
@@ -100,7 +99,7 @@ module.exports = {
height: height,
};
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
const path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
$ident: identiconString,
});
return baseUrl + path +
+33 -10
View File
@@ -1,6 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -61,8 +61,9 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
* @alias module:crypto/DeviceList
*/
export default class DeviceList {
constructor(baseApis, cryptoStore, olmDevice) {
constructor(baseApis, cryptoStore, sessionStore, olmDevice) {
this._cryptoStore = cryptoStore;
this._sessionStore = sessionStore;
// userId -> {
// deviceId -> {
@@ -107,13 +108,30 @@ export default class DeviceList {
* Load the device tracking state from storage
*/
async load() {
let shouldDeleteSessionStore = false;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
// migrate from session store if there's data there and not here
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
this._devices = deviceData ? deviceData.devices : {},
this._deviceTrackingStatus = deviceData ?
deviceData.trackingStatus : {};
this._syncToken = deviceData ? deviceData.syncToken : null;
if (deviceData === null) {
logger.log("Migrating e2e device data...");
this._devices = this._sessionStore.getAllEndToEndDevices() || {};
this._deviceTrackingStatus = (
this._sessionStore.getEndToEndDeviceTrackingStatus() || {}
);
this._syncToken = this._sessionStore.getEndToEndDeviceSyncToken();
this._cryptoStore.storeEndToEndDeviceData({
devices: this._devices,
trackingStatus: this._deviceTrackingStatus,
syncToken: this._syncToken,
}, txn);
shouldDeleteSessionStore = true;
} else {
this._devices = deviceData ? deviceData.devices : {},
this._deviceTrackingStatus = deviceData ?
deviceData.trackingStatus : {};
this._syncToken = deviceData ? deviceData.syncToken : null;
}
this._userByIdentityKey = {};
for (const user of Object.keys(this._devices)) {
const userDevices = this._devices[user];
@@ -128,6 +146,11 @@ export default class DeviceList {
},
);
if (shouldDeleteSessionStore) {
// migrated data is now safely persisted: remove from old store
this._sessionStore.removeEndToEndDeviceData();
}
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) {
@@ -452,10 +475,10 @@ export default class DeviceList {
if (!this._deviceTrackingStatus[userId]) {
logger.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; we save all data together once the sync is done
this._dirty = true;
}
// we don't yet persist the tracking status, since there may be a lot
// of calls; we save all data together once the sync is done
this._dirty = true;
}
/**
+114 -67
View File
@@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017, 2019 New Vector Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -59,17 +59,20 @@ function checkPayloadLength(payloadString) {
* Manages the olm cryptography functions. Each OlmDevice has a single
* OlmAccount and a number of OlmSessions.
*
* Accounts and sessions are kept pickled in the cryptoStore.
* Accounts and sessions are kept pickled in a sessionStore.
*
* @constructor
* @alias module:crypto/OlmDevice
*
* @param {Object} sessionStore A store to be used for data in end-to-end
* crypto. This is deprecated and being replaced by cryptoStore.
* @param {Object} cryptoStore A store for crypto data
*
* @property {string} deviceCurve25519Key Curve25519 key for the account
* @property {string} deviceEd25519Key Ed25519 key for the account
*/
function OlmDevice(cryptoStore) {
function OlmDevice(sessionStore, cryptoStore) {
this._sessionStore = sessionStore;
this._cryptoStore = cryptoStore;
this._pickleKey = "DEFAULT_KEY";
@@ -78,7 +81,7 @@ function OlmDevice(cryptoStore) {
this.deviceEd25519Key = null;
this._maxOneTimeKeys = null;
// we don't bother stashing outboundgroupsessions in the cryptoStore -
// we don't bother stashing outboundgroupsessions in the sessionstore -
// instead we keep them here.
this._outboundGroupSessionStore = {};
@@ -99,10 +102,6 @@ function OlmDevice(cryptoStore) {
// Keys are strings of form "<senderKey>|<session_id>|<message_index>"
// Values are objects of the form "{id: <event id>, timestamp: <ts>}"
this._inboundGroupSessionMessageIndexes = {};
// Keep track of sessions that we're starting, so that we don't start
// multiple sessions for the same device at the same time.
this._sessionsInProgress = {};
}
/**
@@ -115,10 +114,14 @@ function OlmDevice(cryptoStore) {
* Reads the device keys from the OlmAccount object.
*/
OlmDevice.prototype.init = async function() {
await this._migrateFromSessionStore();
let e2eKeys;
const account = new global.Olm.Account();
try {
await _initialiseAccount(this._cryptoStore, this._pickleKey, account);
await _initialiseAccount(
this._sessionStore, this._cryptoStore, this._pickleKey, account,
);
e2eKeys = JSON.parse(account.identity_keys());
this._maxOneTimeKeys = account.max_number_of_one_time_keys();
@@ -130,7 +133,7 @@ OlmDevice.prototype.init = async function() {
this.deviceEd25519Key = e2eKeys.ed25519;
};
async function _initialiseAccount(cryptoStore, pickleKey, account) {
async function _initialiseAccount(sessionStore, cryptoStore, pickleKey, account) {
await cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
cryptoStore.getAccount(txn, (pickledAccount) => {
if (pickledAccount !== null) {
@@ -151,6 +154,95 @@ OlmDevice.getOlmVersion = function() {
return global.Olm.get_library_version();
};
OlmDevice.prototype._migrateFromSessionStore = async function() {
// account
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this._cryptoStore.getAccount(txn, (pickledAccount) => {
if (pickledAccount === null) {
// Migrate from sessionStore
pickledAccount = this._sessionStore.getEndToEndAccount();
if (pickledAccount !== null) {
logger.log("Migrating account from session store");
this._cryptoStore.storeAccount(txn, pickledAccount);
}
}
});
},
);
// remove the old account now the transaction has completed. Either we've
// migrated it or decided not to, either way we want to blow away the old data.
this._sessionStore.removeEndToEndAccount();
// sessions
const sessions = this._sessionStore.getAllEndToEndSessions();
if (Object.keys(sessions).length > 0) {
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
// Don't migrate sessions from localstorage if we already have sessions
// in indexeddb, since this means we've already migrated and an old version
// has run against the same localstorage and created some spurious sessions.
this._cryptoStore.countEndToEndSessions(txn, (count) => {
if (count) {
logger.log("Crypto store already has sessions: not migrating");
return;
}
let numSessions = 0;
for (const deviceKey of Object.keys(sessions)) {
for (const sessionId of Object.keys(sessions[deviceKey])) {
numSessions++;
this._cryptoStore.storeEndToEndSession(
deviceKey, sessionId, sessions[deviceKey][sessionId], txn,
);
}
}
logger.log(
"Migrating " + numSessions + " sessions from session store",
);
});
},
);
this._sessionStore.removeAllEndToEndSessions();
}
// inbound group sessions
const ibGroupSessions = this._sessionStore.getAllEndToEndInboundGroupSessionKeys();
if (Object.keys(ibGroupSessions).length > 0) {
let numIbSessions = 0;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
// We always migrate inbound group sessions, even if we already have some
// in the new store. They should be be safe to migrate.
for (const s of ibGroupSessions) {
try {
this._cryptoStore.addEndToEndInboundGroupSession(
s.senderKey, s.sessionId,
JSON.parse(
this._sessionStore.getEndToEndInboundGroupSession(
s.senderKey, s.sessionId,
),
), txn,
);
} catch (e) {
logger.warn(
"Failed to migrate session " + s.senderKey + "/" +
s.sessionId + ": " + e.stack || e,
);
}
++numIbSessions;
}
logger.log(
"Migrated " + numIbSessions +
" inbound group sessions from session store",
);
},
);
this._sessionStore.removeAllEndToEndInboundGroupSessions();
}
};
/**
* extract our OlmAccount from the crypto store and call the given function
* with the account object
@@ -461,15 +553,6 @@ OlmDevice.prototype.createInboundSession = async function(
* @return {Promise<string[]>} a list of known session ids for the device
*/
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
if (this._sessionsInProgress[theirDeviceIdentityKey]) {
logger.log("waiting for session to be created");
try {
await this._sessionsInProgress[theirDeviceIdentityKey];
} catch (e) {
// if the session failed to be created, just fall through and
// return an empty result
}
}
let sessionIds;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_SESSIONS],
@@ -490,18 +573,10 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @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.
* @return {Promise<?string>} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = async function(
theirDeviceIdentityKey, nowait,
) {
const sessionInfos = await this.getSessionInfoForDevice(
theirDeviceIdentityKey, nowait,
);
OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKey) {
const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey);
if (sessionInfos.length === 0) {
return null;
}
@@ -536,21 +611,9 @@ OlmDevice.prototype.getSessionIdForDevice = async function(
* message and is therefore past the pre-key stage), and 'sessionId'.
*
* @param {string} deviceIdentityKey Curve25519 identity key for the device
* @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.
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
*/
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey, nowait) {
if (this._sessionsInProgress[deviceIdentityKey] && !nowait) {
logger.log("waiting for session to be created");
try {
await this._sessionsInProgress[deviceIdentityKey];
} catch (e) {
// if the session failed to be created, then just fall through and
// return an empty result
}
}
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey) {
const info = [];
await this._cryptoStore.doTxn(
@@ -594,11 +657,6 @@ OlmDevice.prototype.encryptMessage = async function(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
const sessionDesc = sessionInfo.session.describe();
console.log(
"Session ID " + sessionId + " to " +
theirDeviceIdentityKey + ": " + sessionDesc,
);
res = sessionInfo.session.encrypt(payloadString);
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
@@ -626,11 +684,6 @@ OlmDevice.prototype.decryptMessage = async function(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
const sessionDesc = sessionInfo.session.describe();
console.log(
"Session ID " + sessionId + " to " +
theirDeviceIdentityKey + ": " + sessionDesc,
);
payloadString = sessionInfo.session.decrypt(messageType, ciphertext);
sessionInfo.lastReceivedMessageTs = Date.now();
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
@@ -862,6 +915,14 @@ OlmDevice.prototype.addInboundGroupSession = async function(
this._getInboundGroupSession(
roomId, senderKey, sessionId, txn,
(existingSession, existingSessionData) => {
if (existingSession) {
logger.log(
"Update for megolm session " + senderKey + "/" + sessionId,
);
// for now we just ignore updates. TODO: implement something here
return;
}
// new session.
const session = new global.Olm.InboundGroupSession();
try {
@@ -877,20 +938,6 @@ OlmDevice.prototype.addInboundGroupSession = async function(
);
}
if (existingSession) {
logger.log(
"Update for megolm session "
+ senderKey + "/" + sessionId,
);
if (existingSession.first_known_index()
<= session.first_known_index()) {
// existing session has lower index (i.e. can
// decrypt more), so keep it
logger.log("Keeping existing session");
return;
}
}
const sessionData = {
room_id: roomId,
session: session.pickle(this._pickleKey),
@@ -898,7 +945,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
};
this._cryptoStore.storeEndToEndInboundGroupSession(
this._cryptoStore.addEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn,
);
} finally {
+30 -100
View File
@@ -124,118 +124,35 @@ export default class OutgoingRoomKeyRequestManager {
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* @param {Array<{userId: string, deviceId: string}>} recipients
* @param {boolean} resend whether to resend the key request if there is
* already one
*
* @returns {Promise} resolves when the request has been added to the
* pending list (or we have established that a similar request already
* exists)
*/
async sendRoomKeyRequest(requestBody, recipients, resend=false) {
const req = await this._cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
);
if (!req) {
await this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
requestBody: requestBody,
recipients: recipients,
requestId: this._baseApis.makeTxnId(),
state: ROOM_KEY_REQUEST_STATES.UNSENT,
});
} else {
switch (req.state) {
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
case ROOM_KEY_REQUEST_STATES.UNSENT:
// nothing to do here, since we're going to send a request anyways
return;
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: {
// existing request is about to be cancelled. If we want to
// resend, then change the state so that it resends after
// cancelling. Otherwise, just cancel the cancellation.
const state = resend ?
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND :
ROOM_KEY_REQUEST_STATES.SENT;
await this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, {
state,
cancellationTxnId: this._baseApis.makeTxnId(),
},
);
break;
sendRoomKeyRequest(requestBody, recipients) {
return this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
requestBody: requestBody,
recipients: recipients,
requestId: this._baseApis.makeTxnId(),
state: ROOM_KEY_REQUEST_STATES.UNSENT,
}).then((req) => {
if (req.state === ROOM_KEY_REQUEST_STATES.UNSENT) {
this._startTimer();
}
case ROOM_KEY_REQUEST_STATES.SENT: {
// a request has already been sent. If we don't want to
// resend, then do nothing. If we do want to, then cancel the
// existing request and send a new one.
if (resend) {
const state =
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND;
const updatedReq =
await this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
state,
cancellationTxnId: this._baseApis.makeTxnId(),
// need to use a new transaction ID so that
// the request gets sent
requestTxnId: this._baseApis.makeTxnId(),
},
);
if (!updatedReq) {
// updateOutgoingRoomKeyRequest couldn't find the request
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
// raced with another tab to mark the request cancelled.
// Try again, to make sure the request is resent.
return await this.sendRoomKeyRequest(
requestBody, recipients, resend,
);
}
// We don't want to wait for the timer, so we send it
// immediately. (We might actually end up racing with the timer,
// but that's ok: even if we make the request twice, we'll do it
// with the same transaction_id, so only one message will get
// sent).
//
// (We also don't want to wait for the response from the server
// here, as it will slow down processing of received keys if we
// do.)
try {
await this._sendOutgoingRoomKeyRequestCancellation(
updatedReq,
true,
);
} catch (e) {
logger.error(
"Error sending room key request cancellation;"
+ " will retry later.", e,
);
}
// The request has transitioned from
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
// still need to resend the request which is now UNSENT, so
// start the timer if it isn't already started.
}
break;
}
default:
throw new Error('unhandled state: ' + req.state);
}
}
// some of the branches require the timer to be started. Just start it
// all the time, because it doesn't hurt to start it.
this._startTimer();
});
}
/**
* Cancel room key requests, if any match the given requestBody
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* @param {boolean} andResend if true, transition to UNSENT instead of
* deleting after sending cancellation.
*
* @returns {Promise} resolves when the request has been updated in our
* pending list.
*/
cancelRoomKeyRequest(requestBody) {
cancelRoomKeyRequest(requestBody, andResend=false) {
return this._cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
).then((req) => {
@@ -266,10 +183,15 @@ export default class OutgoingRoomKeyRequestManager {
);
case ROOM_KEY_REQUEST_STATES.SENT: {
// If `andResend` is set, transition to UNSENT once the
// cancellation has successfully been sent.
const state = andResend ?
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND :
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING;
// send a cancellation.
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
state: ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
state,
cancellationTxnId: this._baseApis.makeTxnId(),
},
).then((updatedReq) => {
@@ -299,12 +221,20 @@ export default class OutgoingRoomKeyRequestManager {
// do.)
this._sendOutgoingRoomKeyRequestCancellation(
updatedReq,
andResend,
).catch((e) => {
logger.error(
"Error sending room key request cancellation;"
+ " will retry later.", e,
);
this._startTimer();
}).then(() => {
if (!andResend) return;
// The request has transitioned from
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
// still need to resend the request which is now UNSENT, so
// start the timer if it isn't already started.
this._startTimer();
});
});
}
@@ -350,7 +280,7 @@ export default class OutgoingRoomKeyRequestManager {
logger.warn(
`error in OutgoingRoomKeyRequestManager: ${e}`,
);
});
}).done();
};
this._sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
@@ -401,7 +331,7 @@ export default class OutgoingRoomKeyRequestManager {
logger.error("Error sending room key request; will retry later.", e);
this._sendOutgoingRoomKeyRequestsTimer = null;
this._startTimer();
});
}).done();
});
}
@@ -421,7 +351,7 @@ export default class OutgoingRoomKeyRequestManager {
};
return this._sendMessageToDevices(
requestMessage, req.recipients, req.requestTxnId || req.requestId,
requestMessage, req.recipients, req.requestId,
).then(() => {
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
+22 -3
View File
@@ -1,5 +1,5 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -26,21 +26,40 @@ import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
* @alias module:crypto/RoomList
*/
export default class RoomList {
constructor(cryptoStore) {
constructor(cryptoStore, sessionStore) {
this._cryptoStore = cryptoStore;
this._sessionStore = sessionStore;
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
this._roomEncryption = {};
}
async init() {
let removeSessionStoreRooms = false;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this._cryptoStore.getEndToEndRooms(txn, (result) => {
this._roomEncryption = result;
if (result === null || Object.keys(result).length === 0) {
// migrate from session store, if there's data there
const sessStoreRooms = this._sessionStore.getAllEndToEndRooms();
if (sessStoreRooms !== null) {
for (const roomId of Object.keys(sessStoreRooms)) {
this._cryptoStore.storeEndToEndRoom(
roomId, sessStoreRooms[roomId], txn,
);
}
}
this._roomEncryption = sessStoreRooms;
removeSessionStoreRooms = true;
} else {
this._roomEncryption = result;
}
});
},
);
if (removeSessionStoreRooms) {
this._sessionStore.removeAllEndToEndRooms();
}
}
getRoomEncryption(roomId) {
+31 -36
View File
@@ -23,8 +23,8 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../../src/logger';
const logger = require("../../logger");
const utils = require("../../utils");
const olmlib = require("../olmlib");
const base = require("./base");
@@ -278,7 +278,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
logger.log("Failed to back up group session", e);
console.log("Failed to back up group session", e);
});
}
@@ -758,7 +758,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
} catch (e) {
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
this._requestKeysForEvent(event);
errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX';
@@ -766,7 +766,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
throw new base.DecryptionError(
errorCode,
e ? e.toString() : "Unknown Error: Error is undefined", {
e.toString(), {
session: content.sender_key + '|' + content.session_id,
},
);
@@ -815,9 +815,19 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
};
MegolmDecryption.prototype._requestKeysForEvent = function(event) {
const sender = event.getSender();
const wireContent = event.getWireContent();
const recipients = event.getKeyRequestRecipients(this._userId);
// send the request to all of our own devices, and the
// original sending device if it wasn't us.
const recipients = [{
userId: this._userId, deviceId: '*',
}];
if (sender != this._userId) {
recipients.push({
userId: sender, deviceId: wireContent.device_id,
});
}
this._crypto.requestRoomKey({
room_id: event.getRoomId(),
@@ -928,23 +938,16 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
content.session_key, keysClaimed,
exportFormat,
).then(() => {
// cancel any outstanding room key requests for this session
this._crypto.cancelRoomKeyRequest({
algorithm: content.algorithm,
room_id: content.room_id,
session_id: content.session_id,
sender_key: senderKey,
});
// have another go at decrypting events sent with this session.
this._retryDecryption(senderKey, sessionId)
.then((success) => {
// cancel any outstanding room key requests for this session.
// Only do this if we managed to decrypt every message in the
// session, because if we didn't, we leave the other key
// requests in the hopes that someone sends us a key that
// includes an earlier index.
if (success) {
this._crypto.cancelRoomKeyRequest({
algorithm: content.algorithm,
room_id: content.room_id,
session_id: content.session_id,
sender_key: senderKey,
});
}
});
this._retryDecryption(senderKey, sessionId);
}).then(() => {
if (this._crypto.backupInfo) {
// don't wait for the keys to be backed up for the server
@@ -955,7 +958,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
logger.log("Failed to back up group session", e);
console.log("Failed to back up group session", e);
});
}
}).catch((e) => {
@@ -1088,7 +1091,7 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
logger.log("Failed to back up group session", e);
console.log("Failed to back up group session", e);
});
}
// have another go at decrypting events sent with this session.
@@ -1102,27 +1105,19 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
* @private
* @param {String} senderKey
* @param {String} sessionId
*
* @return {Boolean} whether all messages were successfully decrypted
*/
MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionId) {
MegolmDecryption.prototype._retryDecryption = function(senderKey, sessionId) {
const k = senderKey + "|" + sessionId;
const pending = this._pendingEvents[k];
if (!pending) {
return true;
return;
}
delete this._pendingEvents[k];
await Promise.all([...pending].map(async (ev) => {
try {
await ev.attemptDecryption(this._crypto);
} catch (e) {
// don't die if something goes wrong
}
}));
return !this._pendingEvents[k];
for (const ev of pending) {
ev.attemptDecryption(this._crypto);
}
};
base.registerAlgorithm(
+1 -1
View File
@@ -22,7 +22,7 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../logger';
const logger = require("../../logger");
const utils = require("../../utils");
const olmlib = require("../olmlib");
const DeviceInfo = require("../deviceinfo");
+93 -563
View File
@@ -1,7 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018-2019 New Vector Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -25,7 +25,7 @@ const anotherjson = require('another-json');
import Promise from 'bluebird';
import {EventEmitter} from 'events';
import logger from '../logger';
const logger = require("../logger");
const utils = require("../utils");
const OlmDevice = require("./OlmDevice");
const olmlib = require("./olmlib");
@@ -33,34 +33,10 @@ const algorithms = require("./algorithms");
const DeviceInfo = require("./deviceinfo");
const DeviceVerification = DeviceInfo.DeviceVerification;
const DeviceList = require('./DeviceList').default;
import { randomString } from '../randomstring';
import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
import {ShowQRCode, ScanQRCode} from './verification/QRCode';
import SAS from './verification/SAS';
import {
newUserCancelledError,
newUnexpectedMessageError,
newUnknownMethodError,
} from './verification/Error';
const defaultVerificationMethods = {
[ScanQRCode.NAME]: ScanQRCode,
[ShowQRCode.NAME]: ShowQRCode,
[SAS.NAME]: SAS,
};
/**
* verification method names
*/
export const verificationMethods = {
QR_CODE_SCAN: ScanQRCode.NAME,
QR_CODE_SHOW: ShowQRCode.NAME,
SAS: SAS.NAME,
};
export function isCryptoAvailable() {
return Boolean(global.Olm);
}
@@ -93,13 +69,9 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200;
* storage for the crypto layer.
*
* @param {RoomList} roomList An initialised RoomList object
*
* @param {Array} verificationMethods Array of verification methods to use.
* Each element can either be a string from MatrixClient.verificationMethods
* or a class that implements a verification method.
*/
export default function Crypto(baseApis, sessionStore, userId, deviceId,
clientStore, cryptoStore, roomList, verificationMethods) {
clientStore, cryptoStore, roomList) {
this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._userId = userId;
@@ -107,24 +79,6 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
this._clientStore = clientStore;
this._cryptoStore = cryptoStore;
this._roomList = roomList;
this._verificationMethods = new Map();
if (verificationMethods) {
for (const method of verificationMethods) {
if (typeof method === "string") {
if (defaultVerificationMethods[method]) {
this._verificationMethods.set(
method,
defaultVerificationMethods[method],
);
}
} else if (method.NAME) {
this._verificationMethods.set(
method.NAME,
method,
);
}
}
}
// track whether this device's megolm keys are being backed up incrementally
// to the server or not.
@@ -134,9 +88,9 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
this._checkedForBackup = false; // Have we checked the server for a backup we can use?
this._sendingBackups = false; // Are we currently sending backups?
this._olmDevice = new OlmDevice(cryptoStore);
this._olmDevice = new OlmDevice(sessionStore, cryptoStore);
this._deviceList = new DeviceList(
baseApis, cryptoStore, this._olmDevice,
baseApis, cryptoStore, sessionStore, this._olmDevice,
);
// the last time we did a check for the number of one-time-keys on the
@@ -186,8 +140,6 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
// },
// }
this._lastNewSessionForced = {};
this._verificationTransactions = new Map();
}
utils.inherits(Crypto, EventEmitter);
@@ -197,11 +149,27 @@ utils.inherits(Crypto, EventEmitter);
* Returns a promise which resolves once the crypto module is ready for use.
*/
Crypto.prototype.init = async function() {
logger.log("Crypto: initialising Olm...");
await global.Olm.init();
logger.log("Crypto: initialising Olm device...");
const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount());
let cryptoStoreHasAccount;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this._cryptoStore.getAccount(txn, (pickledAccount) => {
cryptoStoreHasAccount = Boolean(pickledAccount);
});
},
);
if (sessionStoreHasAccount && !cryptoStoreHasAccount) {
// we're about to migrate to the crypto store
this.emit("crypto.warning", 'CRYPTO_WARNING_ACCOUNT_MIGRATED');
} else if (sessionStoreHasAccount && cryptoStoreHasAccount) {
// There's an account in both stores: an old version of
// the code has been run against this store.
this.emit("crypto.warning", 'CRYPTO_WARNING_OLD_VERSION_DETECTED');
}
await this._olmDevice.init();
logger.log("Crypto: loading device list...");
await this._deviceList.load();
// build our device keys: these will later be uploaded
@@ -210,7 +178,6 @@ Crypto.prototype.init = async function() {
this._deviceKeys["curve25519:" + this._deviceId] =
this._olmDevice.deviceCurve25519Key;
logger.log("Crypto: fetching own devices...");
let myDevices = this._deviceList.getRawStoredDevicesForUser(
this._userId,
);
@@ -220,8 +187,7 @@ Crypto.prototype.init = async function() {
}
if (!myDevices[this._deviceId]) {
// add our own deviceinfo to the cryptoStore
logger.log("Crypto: adding this device to the store...");
// add our own deviceinfo to the sessionstore
const deviceInfo = {
keys: this._deviceKeys,
algorithms: this._supportedAlgorithms,
@@ -236,7 +202,6 @@ Crypto.prototype.init = async function() {
this._deviceList.saveIfDirty();
}
logger.log("Crypto: checking for key backup...");
this._checkAndStartKeyBackup();
};
@@ -247,75 +212,47 @@ Crypto.prototype.init = async function() {
* to it.
*/
Crypto.prototype._checkAndStartKeyBackup = async function() {
logger.log("Checking key backup status...");
console.log("Checking key backup status...");
if (this._baseApis.isGuest()) {
logger.log("Skipping key backup check since user is guest");
console.log("Skipping key backup check since user is guest");
this._checkedForBackup = true;
return null;
return;
}
let backupInfo;
try {
backupInfo = await this._baseApis.getKeyBackupVersion();
} catch (e) {
logger.log("Error checking for active key backup", e);
console.log("Error checking for active key backup", e);
if (e.httpStatus / 100 === 4) {
// well that's told us. we won't try again.
this._checkedForBackup = true;
}
return null;
return;
}
this._checkedForBackup = true;
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
if (trustInfo.usable && !this.backupInfo) {
logger.log(
"Found usable key backup v" + backupInfo.version +
": enabling key backups",
);
console.log("Found usable key backup: enabling key backups");
this._baseApis.enableKeyBackup(backupInfo);
} else if (!trustInfo.usable && this.backupInfo) {
logger.log("No usable key backup: disabling key backup");
console.log("No usable key backup: disabling key backup");
this._baseApis.disableKeyBackup();
} else if (!trustInfo.usable && !this.backupInfo) {
logger.log("No usable key backup: not enabling key backup");
} else if (trustInfo.usable && this.backupInfo) {
// may not be the same version: if not, we should switch
if (backupInfo.version !== this.backupInfo.version) {
logger.log(
"On backup version " + this.backupInfo.version + " but found " +
"version " + backupInfo.version + ": switching.",
);
this._baseApis.disableKeyBackup();
this._baseApis.enableKeyBackup(backupInfo);
} else {
logger.log("Backup version " + backupInfo.version + " still current");
}
console.log("No usable key backup: not enabling key backup");
}
return {backupInfo, trustInfo};
};
Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) {
// This should be redundant post cross-signing is a thing, so just
// plonk it in localStorage for now.
this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey);
await this.checkKeyBackup();
};
/**
* Forces a re-check of the key backup and enables/disables it
* as appropriate.
* as appropriate
*
* @return {Object} Object with backup info (as returned by
* getKeyBackupVersion) in backupInfo and
* trust information (as returned by isKeyBackupTrusted)
* in trustInfo.
* @param {object} backupInfo Backup info from /room_keys/version endpoint
*/
Crypto.prototype.checkKeyBackup = async function() {
Crypto.prototype.checkKeyBackup = async function(backupInfo) {
this._checkedForBackup = false;
const returnInfo = await this._checkAndStartKeyBackup();
return returnInfo;
await this._checkAndStartKeyBackup();
};
/**
@@ -323,16 +260,14 @@ Crypto.prototype.checkKeyBackup = async function() {
* @return {object} {
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
* sigs: [
* valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation
* deviceId: [string],
* device: [DeviceInfo || null],
* valid: [bool],
* device: [DeviceInfo],
* ]
* }
*/
Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
const ret = {
usable: false,
trusted_locally: false,
sigs: [],
};
@@ -343,61 +278,42 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
!backupInfo.auth_data.public_key ||
!backupInfo.auth_data.signatures
) {
logger.info("Key backup is absent or missing required data");
console.log("Key backup is absent or missing required data");
return ret;
}
const trustedPubkey = this._sessionStore.getLocalTrustedBackupPubKey();
if (backupInfo.auth_data.public_key === trustedPubkey) {
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
ret.trusted_locally = true;
const mySigs = backupInfo.auth_data.signatures[this._userId];
if (!mySigs || mySigs.length === 0) {
console.log("Ignoring key backup because it lacks any signatures from this user");
return ret;
}
const mySigs = backupInfo.auth_data.signatures[this._userId] || [];
for (const keyId of Object.keys(mySigs)) {
const keyIdParts = keyId.split(':');
if (keyIdParts[0] !== 'ed25519') {
logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
const device = this._deviceList.getStoredDevice(
this._userId, keyId.split(':')[1], // XXX: is this how we're supposed to get the device ID?
);
if (!device) {
console.log("Ignoring signature from unknown key " + keyId);
continue;
}
const sigInfo = { deviceId: keyIdParts[1] }; // XXX: is this how we're supposed to get the device ID?
const device = this._deviceList.getStoredDevice(
this._userId, sigInfo.deviceId,
);
if (device) {
sigInfo.device = device;
try {
await olmlib.verifySignature(
this._olmDevice,
// verifySignature modifies the object so we need to copy
// if we verify more than one sig
Object.assign({}, backupInfo.auth_data),
this._userId,
device.deviceId,
device.getFingerprint(),
);
sigInfo.valid = true;
} catch (e) {
logger.info(
"Bad signature from key ID " + keyId + " userID " + this._userId +
" device ID " + device.deviceId + " fingerprint: " +
device.getFingerprint(), backupInfo.auth_data, e,
);
sigInfo.valid = false;
}
} else {
sigInfo.valid = null; // Can't determine validity because we don't have the signing device
logger.info("Ignoring signature from unknown key " + keyId);
const sigInfo = { device };
try {
await olmlib.verifySignature(
this._olmDevice,
backupInfo.auth_data,
this._userId,
device.deviceId,
device.getFingerprint(),
);
sigInfo.valid = true;
} catch (e) {
console.log("Bad signature from device " + device.deviceId, e);
sigInfo.valid = false;
}
ret.sigs.push(sigInfo);
}
ret.usable = (
ret.sigs.some((s) => s.valid && s.device.isVerified()) ||
ret.trusted_locally
);
ret.usable = ret.sigs.some((s) => s.valid && s.device.isVerified());
return ret;
};
@@ -756,81 +672,6 @@ Crypto.prototype.setDeviceVerification = async function(
};
Crypto.prototype.requestVerification = function(userId, methods, devices) {
if (!methods) {
// .keys() returns an iterator, so we need to explicitly turn it into an array
methods = [...this._verificationMethods.keys()];
}
if (!devices) {
devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(userId));
}
if (!this._verificationTransactions.has(userId)) {
this._verificationTransactions.set(userId, new Map);
}
const transactionId = randomString(32);
const promise = new Promise((resolve, reject) => {
this._verificationTransactions.get(userId).set(transactionId, {
request: {
methods: methods,
devices: devices,
resolve: resolve,
reject: reject,
},
});
});
const message = {
transaction_id: transactionId,
from_device: this._baseApis.deviceId,
methods: methods,
timestamp: Date.now(),
};
const msgMap = {};
for (const deviceId of devices) {
msgMap[deviceId] = message;
}
this._baseApis.sendToDevice("m.key.verification.request", {[userId]: msgMap});
return promise;
};
Crypto.prototype.beginKeyVerification = function(
method, userId, deviceId, transactionId,
) {
if (!this._verificationTransactions.has(userId)) {
this._verificationTransactions.set(userId, new Map());
}
transactionId = transactionId || randomString(32);
if (method instanceof Array) {
if (method.length !== 2
|| !this._verificationMethods.has(method[0])
|| !this._verificationMethods.has(method[1])) {
throw newUnknownMethodError();
}
/*
return new TwoPartVerification(
this._verificationMethods[method[0]],
this._verificationMethods[method[1]],
userId, deviceId, transactionId,
);
*/
} else if (this._verificationMethods.has(method)) {
const verifier = new (this._verificationMethods.get(method))(
this._baseApis, userId, deviceId, transactionId,
);
if (!this._verificationTransactions.get(userId).has(transactionId)) {
this._verificationTransactions.get(userId).set(transactionId, {});
}
this._verificationTransactions.get(userId).get(transactionId).verifier = verifier;
return verifier;
} else {
throw newUnknownMethodError();
}
};
/**
* Get information on the active olm sessions with a user
* <p>
@@ -940,7 +781,7 @@ Crypto.prototype.forceDiscardSession = function(roomId) {
};
/**
* Configure a room to use encryption (ie, save a flag in the cryptoStore).
* Configure a room to use encryption (ie, save a flag in the sessionstore).
*
* @param {string} roomId The room ID to enable encryption in.
*
@@ -951,15 +792,6 @@ Crypto.prototype.forceDiscardSession = function(roomId) {
* the device query is always inhibited as the members are not tracked.
*/
Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) {
// ignore crypto events with no algorithm defined
// This will happen if a crypto event is redacted before we fetch the room state
// It would otherwise just throw later as an unknown algorithm would, but we may
// as well catch this here
if (!config.algorithm) {
console.log("Ignoring setRoomEncryption with no algorithm");
return;
}
// if state is being replayed from storage, we might already have a configuration
// for this room as they are persisted as well.
// We just need to make sure the algorithm is initialized in this case.
@@ -1158,19 +990,15 @@ Crypto.prototype.importRoomKeys = function(keys) {
/**
* Schedules sending all keys waiting to be sent to the backup, if not already
* scheduled. Retries if necessary.
*
* @param {number} maxDelay Maximum delay to wait in ms. 0 means no delay.
*/
Crypto.prototype.scheduleKeyBackupSend = async function(maxDelay = 10000) {
Crypto.prototype._scheduleKeyBackupSend = async function() {
if (this._sendingBackups) return;
this._sendingBackups = true;
try {
// wait between 0 and `maxDelay` seconds, to avoid backup
// wait between 0 and 10 seconds, to avoid backup
// requests from different clients hitting the server all at
// the same time when a new key is sent
const delay = Math.random() * maxDelay;
const delay = Math.random() * 10000;
await Promise.delay(delay);
let numFailures = 0; // number of consecutive failures
while (1) {
@@ -1187,15 +1015,12 @@ Crypto.prototype.scheduleKeyBackupSend = async function(maxDelay = 10000) {
numFailures = 0;
} catch (err) {
numFailures++;
logger.log("Key backup request failed", err);
console.log("Key backup request failed", err);
if (err.data) {
if (
err.data.errcode == 'M_NOT_FOUND' ||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
) {
// Re-check key backup status on error, so we can be
// sure to present the current situation when asked.
await this.checkKeyBackup();
// Backup version has changed or this backup version
// has been deleted
this.emit("crypto.keyBackupFailed", err.data.errcode);
@@ -1226,9 +1051,6 @@ Crypto.prototype._backupPendingKeys = async function(limit) {
return 0;
}
let remaining = await this._cryptoStore.countSessionsNeedingBackup();
this.emit("crypto.keyBackupSessionsRemaining", remaining);
const data = {};
for (const session of sessions) {
const roomId = session.sessionData.room_id;
@@ -1265,10 +1087,7 @@ Crypto.prototype._backupPendingKeys = async function(limit) {
undefined, undefined, this.backupInfo.version,
{rooms: data},
);
await this._cryptoStore.unmarkSessionsNeedingBackup(sessions);
remaining = await this._cryptoStore.countSessionsNeedingBackup();
this.emit("crypto.keyBackupSessionsRemaining", remaining);
return sessions.length;
};
@@ -1289,26 +1108,10 @@ Crypto.prototype.backupGroupSession = async function(
// don't wait for this to complete: it will delay so
// happens in the background
this.scheduleKeyBackupSend();
this._scheduleKeyBackupSend();
};
/**
* Marks all group sessions as needing to be backed up and schedules them to
* upload in the background as soon as possible.
*/
Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
await this.flagAllGroupSessionsForBackup();
// Schedule keys to upload in the background as soon as possible.
this.scheduleKeyBackupSend(0 /* maxDelay */);
};
/**
* Marks all group sessions as needing to be backed up without scheduling
* them to upload in the background.
* @returns {Promise<int>} Resolves to the number of sessions requiring a backup.
*/
Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
Crypto.prototype.backupAllGroupSessions = async function(version) {
await this._cryptoStore.doTxn(
'readwrite',
[
@@ -1324,9 +1127,10 @@ Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
},
);
const remaining = await this._cryptoStore.countSessionsNeedingBackup();
this.emit("crypto.keyBackupSessionsRemaining", remaining);
return remaining;
let numKeysBackedUp;
do {
numKeysBackedUp = await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
} while (numKeysBackedUp > 0);
};
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
@@ -1443,14 +1247,10 @@ Crypto.prototype.handleDeviceListChanges = async function(syncData, syncDeviceLi
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* @param {Array<{userId: string, deviceId: string}>} recipients
* @param {boolean} resend whether to resend the key request if there is
* already one
*
* @return {Promise} a promise that resolves when the key request is queued
*/
Crypto.prototype.requestRoomKey = function(requestBody, recipients, resend=false) {
return this._outgoingRoomKeyRequestManager.sendRoomKeyRequest(
requestBody, recipients, resend,
Crypto.prototype.requestRoomKey = function(requestBody, recipients) {
this._outgoingRoomKeyRequestManager.sendRoomKeyRequest(
requestBody, recipients,
).catch((e) => {
// this normally means we couldn't talk to the store
logger.error(
@@ -1464,9 +1264,11 @@ Crypto.prototype.requestRoomKey = function(requestBody, recipients, resend=false
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* parameters to match for cancellation
* @param {boolean} andResend
* if true, resend the key request after cancelling.
*/
Crypto.prototype.cancelRoomKeyRequest = function(requestBody) {
this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
Crypto.prototype.cancelRoomKeyRequest = function(requestBody, andResend) {
this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody, andResend)
.catch((e) => {
logger.warn("Error clearing pending room key requests", e);
}).done();
@@ -1524,10 +1326,6 @@ Crypto.prototype.onSyncCompleted = async function(syncData) {
// catch up on any new devices we got told about during the sync.
this._deviceList.lastKnownSyncToken = nextSyncToken;
// we always track our own device list (for key backups etc)
this._deviceList.startTrackingDeviceList(this._userId);
this._deviceList.refreshOutdatedDeviceLists();
// we don't start uploading one-time keys until we've caught up with
@@ -1617,12 +1415,6 @@ Crypto.prototype._onToDeviceEvent = function(event) {
this._onRoomKeyEvent(event);
} else if (event.getType() == "m.room_key_request") {
this._onRoomKeyRequestEvent(event);
} else if (event.getType() === "m.key.verification.request") {
this._onKeyVerificationRequest(event);
} else if (event.getType() === "m.key.verification.start") {
this._onKeyVerificationStart(event);
} else if (event.getContent().transaction_id) {
this._onKeyVerificationMessage(event);
} else if (event.getContent().msgtype === "m.bad.encrypted") {
this._onToDeviceBadEncrypted(event);
} else if (event.isBeingDecrypted()) {
@@ -1660,279 +1452,6 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
alg.onRoomKeyEvent(event);
};
/**
* Handle a key verification request event.
*
* @private
* @param {module:models/event.MatrixEvent} event verification request event
*/
Crypto.prototype._onKeyVerificationRequest = function(event) {
if (event.isCancelled()) {
logger.warn("Ignoring flagged verification request from " + event.getSender());
return;
}
const content = event.getContent();
if (!("from_device" in content) || typeof content.from_device !== "string"
|| !("transaction_id" in content) || typeof content.from_device !== "string"
|| !("methods" in content) || !(content.methods instanceof Array)
|| !("timestamp" in content) || typeof content.timestamp !== "number") {
logger.warn("received invalid verification request from " + event.getSender());
// ignore event if malformed
return;
}
const now = Date.now();
if (now < content.timestamp - (5 * 60 * 1000)
|| now > content.timestamp + (10 * 60 * 1000)) {
// ignore if event is too far in the past or too far in the future
logger.log("received verification that is too old or from the future");
return;
}
const sender = event.getSender();
if (sender === this._userId && content.from_device === this._deviceId) {
// ignore requests from ourselves, because it doesn't make sense for a
// device to verify itself
return;
}
if (this._verificationTransactions.has(sender)) {
if (this._verificationTransactions.get(sender).has(content.transaction_id)) {
// transaction already exists: cancel it and drop the existing
// request because someone has gotten confused
const err = newUnexpectedMessageError({
transaction_id: content.transaction_id,
});
if (this._verificationTransactions.get(sender).get(content.transaction_id)
.verifier) {
this._verificationTransactions.get(sender).get(content.transaction_id)
.verifier.cancel(err);
} else {
this._verificationTransactions.get(sender).get(content.transaction_id)
.reject(err);
this.sendToDevice("m.key.verification.cancel", {
[sender]: {
[content.from_device]: err.getContent(),
},
});
}
this._verificationTransactions.get(sender).delete(content.transaction_id);
return;
}
} else {
this._verificationTransactions.set(sender, new Map());
}
// determine what requested methods we support
const methods = [];
for (const method of content.methods) {
if (typeof method !== "string") {
continue;
}
if (this._verificationMethods.has(method)) {
methods.push(method);
}
}
if (methods.length === 0) {
this._baseApis.emit(
"crypto.verification.request.unknown",
event.getSender(),
() => {
this.sendToDevice("m.key.verification.cancel", {
[sender]: {
[content.from_device]: newUserCancelledError({
transaction_id: content.transaction_id,
}).getContent(),
},
});
},
);
} else {
// notify the application of the verification request, so it can
// decide what to do with it
const request = {
event: event,
methods: methods,
beginKeyVerification: (method) => {
const verifier = this.beginKeyVerification(
method,
sender,
content.from_device,
content.transaction_id,
);
this._verificationTransactions.get(sender).get(content.transaction_id)
.verifier = verifier;
return verifier;
},
cancel: () => {
this._baseApis.sendToDevice("m.key.verification.cancel", {
[sender]: {
[content.from_device]: newUserCancelledError({
transaction_id: content.transaction_id,
}).getContent(),
},
});
},
};
this._verificationTransactions.get(sender).set(content.transaction_id, {
request: request,
});
this._baseApis.emit("crypto.verification.request", request);
}
};
/**
* Handle a key verification start event.
*
* @private
* @param {module:models/event.MatrixEvent} event verification start event
*/
Crypto.prototype._onKeyVerificationStart = function(event) {
if (event.isCancelled()) {
logger.warn("Ignoring flagged verification start from " + event.getSender());
return;
}
const sender = event.getSender();
const content = event.getContent();
const transactionId = content.transaction_id;
const deviceId = content.from_device;
if (!transactionId || !deviceId) {
// invalid request, and we don't have enough information to send a
// cancellation, so just ignore it
return;
}
let handler = this._verificationTransactions.has(sender)
&& this._verificationTransactions.get(sender).get(transactionId);
// if the verification start message is invalid, send a cancel message to
// the other side, and also send a cancellation event
const cancel = (err) => {
if (handler.verifier) {
handler.verifier.cancel(err);
} else if (handler.request && handler.request.cancel) {
handler.request.cancel(err);
}
this.sendToDevice(
"m.key.verification.cancel", {
[sender]: {
[deviceId]: err.getContent(),
},
},
);
};
if (!this._verificationMethods.has(content.method)) {
cancel(newUnknownMethodError({
transaction_id: content.transactionId,
}));
return;
} else if (content.next_method) {
if (!this._verificationMethods.has(content.next_method)) {
cancel(newUnknownMethodError({
transaction_id: content.transactionId,
}));
return;
} else {
/* TODO:
const verification = new TwoPartVerification(
this._verificationMethods[content.method],
this._verificationMethods[content.next_method],
userId, deviceId,
);
this.emit(verification.event_type, verification);
this.emit(verification.first.event_type, verification);*/
}
} else {
const verifier = new (this._verificationMethods.get(content.method))(
this._baseApis, sender, deviceId, content.transaction_id,
event, handler && handler.request,
);
if (!handler) {
if (!this._verificationTransactions.has(sender)) {
this._verificationTransactions.set(sender, new Map());
}
handler = this._verificationTransactions.get(sender).set(transactionId, {
verifier: verifier,
});
} else {
if (!handler.verifier) {
handler.verifier = verifier;
if (handler.request) {
// the verification start was sent as a response to a
// verification request
if (!handler.request.devices.includes(deviceId)) {
// didn't send a request to that device, so it
// shouldn't have responded
cancel(newUnexpectedMessageError({
transaction_id: content.transactionId,
}));
return;
}
if (!handler.request.methods.includes(content.method)) {
// verification method wasn't one that was requested
cancel(newUnknownMethodError({
transaction_id: content.transactionId,
}));
return;
}
// send cancellation messages to all the other devices that
// the request was sent to
const message = {
transaction_id: transactionId,
code: "m.accepted",
reason: "Verification request accepted by another device",
};
const msgMap = {};
for (const devId of handler.request.devices) {
if (devId !== deviceId) {
msgMap[devId] = message;
}
}
this._baseApis.sendToDevice("m.key.verification.cancel", {
[sender]: msgMap,
});
handler.request.resolve(verifier);
}
} else {
// FIXME: make sure we're in a two-part verification, and the start matches the second part
}
}
this._baseApis.emit("crypto.verification.start", verifier);
}
};
/**
* Handle a general key verification event.
*
* @private
* @param {module:models/event.MatrixEvent} event verification start event
*/
Crypto.prototype._onKeyVerificationMessage = function(event) {
const sender = event.getSender();
const transactionId = event.getContent().transaction_id;
const handler = this._verificationTransactions.has(sender)
&& this._verificationTransactions.get(sender).get(transactionId);
if (!handler) {
return;
} else if (event.getType() === "m.key.verification.cancel") {
logger.log(event);
if (handler.verifier) {
handler.verifier.cancel(event);
} else if (handler.request && handler.request.cancel) {
handler.request.cancel(event);
}
} else if (handler.verifier) {
const verifier = handler.verifier;
if (verifier.events
&& verifier.events.includes(event.getType())) {
verifier.handleEvent(event);
}
}
};
/**
* Handle a toDevice event that couldn't be decrypted
*
@@ -2018,7 +1537,7 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) {
sender, device.deviceId,
);
for (const keyReq of requestsToResend) {
this.requestRoomKey(keyReq.requestBody, keyReq.recipients, true);
this.cancelRoomKeyRequest(keyReq.requestBody, true);
}
};
@@ -2390,6 +1909,17 @@ class IncomingRoomKeyRequestCancellation {
* Fires when the app may wish to warn the user about something related
* the end-to-end crypto.
*
* Comes with a type which is one of:
* * CRYPTO_WARNING_ACCOUNT_MIGRATED: Account data has been migrated from an older
* version of the store in such a way that older clients will no longer be
* able to read it. The app may wish to warn the user against going back to
* an older version of the app.
* * CRYPTO_WARNING_OLD_VERSION_DETECTED: js-sdk has detected that an older version
* of js-sdk has been run against the same store after a migration has been
* performed. This is likely have caused unexpected behaviour in the old
* version. For example, the old version and the new version may have two
* different identity keys.
*
* @event module:client~MatrixClient#"crypto.warning"
* @param {string} type One of the strings listed above
*/
+15 -60
View File
@@ -1,6 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -24,7 +23,7 @@ limitations under the License.
import Promise from 'bluebird';
const anotherjson = require('another-json');
import logger from '../logger';
const logger = require("../logger");
const utils = require("../utils");
/**
@@ -138,7 +137,6 @@ module.exports.ensureOlmSessionsForDevices = async function(
// [userId, deviceId], ...
];
const result = {};
const resolveSession = {};
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
@@ -150,36 +148,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
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 sessionId = await olmDevice.getSessionIdForDevice(
key, resolveSession[key],
);
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];
}
const sessionId = await olmDevice.getSessionIdForDevice(key);
if (sessionId === null || force) {
devicesWithoutSession.push([userId, deviceId]);
}
@@ -194,19 +163,16 @@ module.exports.ensureOlmSessionsForDevices = async function(
return result;
}
// TODO: this has a race condition - if we try to send another message
// while we are claiming a key, we will end up claiming two and setting up
// two sessions.
//
// That should eventually resolve itself, but it's poor form.
const oneTimeKeyAlgorithm = "signed_curve25519";
let res;
try {
res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm,
);
} catch (e) {
for (const resolver of Object.values(resolveSession)) {
resolver.resolve();
}
logger.log("failed to claim one-time keys", e, devicesWithoutSession);
throw e;
}
const res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm,
);
const otk_res = res.one_time_keys || {};
const promises = [];
@@ -219,7 +185,6 @@ module.exports.ensureOlmSessionsForDevices = async function(
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
if (result[userId][deviceId].sessionId && !force) {
// we already have a result for this device
continue;
@@ -234,12 +199,10 @@ module.exports.ensureOlmSessionsForDevices = async function(
}
if (!oneTimeKey) {
const msg = "No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId;
logger.warn(msg);
if (resolveSession[key]) {
resolveSession[key].resolve();
}
logger.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId,
);
continue;
}
@@ -247,15 +210,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
_verifyKeyAndStartSession(
olmDevice, oneTimeKey, userId, deviceInfo,
).then((sid) => {
if (resolveSession[key]) {
resolveSession[key].resolve(sid);
}
result[userId][deviceId].sessionId = sid;
}, (e) => {
if (resolveSession[key]) {
resolveSession[key].resolve();
}
throw e;
}),
);
}
+1 -1
View File
@@ -21,7 +21,7 @@ import bs58 from 'bs58';
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
export function encodeRecoveryKey(key) {
const buf = new Buffer(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
@@ -563,22 +563,8 @@ export class Backend {
});
}
countSessionsNeedingBackup(txn) {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readonly");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return new Promise((resolve, reject) => {
const req = objectStore.count();
req.onerror = reject;
req.onsuccess = () => resolve(req.result);
});
}
unmarkSessionsNeedingBackup(sessions, txn) {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite");
}
unmarkSessionsNeedingBackup(sessions) {
const txn = this._db.transaction("sessions_needing_backup", "readwrite");
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
@@ -690,21 +676,12 @@ function promiseifyTxn(txn) {
}
resolve();
};
txn.onerror = (event) => {
txn.onerror = () => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
} else {
logger.log("Error performing indexeddb txn", event);
reject(event.target.error);
}
};
txn.onabort = (event) => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
} else {
logger.log("Error performing indexeddb txn", event);
reject(event.target.error);
}
reject();
};
txn.onabort = () => reject(txn._mx_abortexception);
});
}
+2 -21
View File
@@ -22,7 +22,6 @@ import LocalStorageCryptoStore from './localStorage-crypto-store';
import MemoryCryptoStore from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
import {InvalidCryptoStoreError} from '../../errors';
import * as IndexedDBHelpers from "../../indexeddb-helpers";
/**
* Internal module. indexeddb storage for e2e.
@@ -49,10 +48,6 @@ export default class IndexedDBCryptoStore {
this._backendPromise = null;
}
static exists(indexedDB, dbName) {
return IndexedDBHelpers.exists(indexedDB, dbName);
}
/**
* Ensure the database exists and is up-to-date, or fall back to
* a local storage or in-memory store.
@@ -90,7 +85,6 @@ export default class IndexedDBCryptoStore {
};
req.onerror = (ev) => {
logger.log("Error connecting to indexeddb", ev);
reject(ev.target.error);
};
@@ -160,7 +154,6 @@ export default class IndexedDBCryptoStore {
};
req.onerror = (ev) => {
logger.log("Error deleting data from indexeddb", ev);
reject(ev.target.error);
};
@@ -487,26 +480,14 @@ export default class IndexedDBCryptoStore {
});
}
/**
* Count the inbound group sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves to the number of sessions
*/
countSessionsNeedingBackup(txn) {
return this._connect().then((backend) => {
return backend.countSessionsNeedingBackup(txn);
});
}
/**
* Unmark sessions as needing to be backed up.
* @param {Array<object>} sessions The sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are unmarked
*/
unmarkSessionsNeedingBackup(sessions, txn) {
unmarkSessionsNeedingBackup(sessions) {
return this._connect().then((backend) => {
return backend.unmarkSessionsNeedingBackup(sessions, txn);
return backend.unmarkSessionsNeedingBackup(sessions);
});
}
@@ -57,16 +57,6 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
this.store = webStore;
}
static exists(webStore) {
const length = webStore.length;
for (let i = 0; i < length; i++) {
if (webStore.key(i).startsWith(E2E_PREFIX)) {
return true;
}
}
return false;
}
// Olm Sessions
countEndToEndSessions(txn, func) {
@@ -231,12 +221,6 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
return Promise.resolve(sessions);
}
countSessionsNeedingBackup() {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
}
unmarkSessionsNeedingBackup(sessions) {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
-4
View File
@@ -336,10 +336,6 @@ export default class MemoryCryptoStore {
return Promise.resolve(sessions);
}
countSessionsNeedingBackup() {
return Promise.resolve(Object.keys(this._sessionsNeedingBackup).length);
}
unmarkSessionsNeedingBackup(sessions) {
for (const session of sessions) {
const sessionKey = session.senderKey + '/' + session.sessionId;
-253
View File
@@ -1,253 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Base class for verification methods.
* @module crypto/verification/Base
*/
import {MatrixEvent} from '../../models/event';
import {EventEmitter} from 'events';
import logger from '../../logger';
import {newTimeoutError} from "./Error";
const timeoutException = new Error("Verification timed out");
export default class VerificationBase extends EventEmitter {
/**
* Base class for verification methods.
*
* <p>Once a verifier object is created, the verification can be started by
* calling the verify() method, which will return a promise that will
* resolve when the verification is completed, or reject if it could not
* complete.</p>
*
* <p>Subclasses must have a NAME class property.</p>
*
* @class
*
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
*
* @param {string} userId the user ID that is being verified
*
* @param {string} deviceId the device ID that is being verified
*
* @param {string} transactionId the transaction ID to be used when sending events
*
* @param {object} startEvent the m.key.verification.start event that
* initiated this verification, if any
*
* @param {object} request the key verification request object related to
* this verification, if any
*
* @param {object} parent parent verification for this verification, if any
*/
constructor(baseApis, userId, deviceId, transactionId, startEvent, request, parent) {
super();
this._baseApis = baseApis;
this.userId = userId;
this.deviceId = deviceId;
this.transactionId = transactionId;
this.startEvent = startEvent;
this.request = request;
this.cancelled = false;
this._parent = parent;
this._done = false;
this._promise = null;
this._transactionTimeoutTimer = null;
// At this point, the verification request was received so start the timeout timer.
this._resetTimer();
}
_resetTimer() {
console.log("Refreshing/starting the verification transaction timeout timer");
if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer);
}
this._transactionTimeoutTimer = setTimeout(() => {
if (!this._done && !this.cancelled) {
console.log("Triggering verification timeout");
this.cancel(timeoutException);
}
}, 10 * 60 * 1000); // 10 minutes
}
_endTimer() {
if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer);
this._transactionTimeoutTimer = null;
}
}
_sendToDevice(type, content) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
content.transaction_id = this.transactionId;
return this._baseApis.sendToDevice(type, {
[this.userId]: { [this.deviceId]: content },
});
}
_waitForEvent(type) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
this._expectedEvent = type;
return new Promise((resolve, reject) => {
this._resolveEvent = resolve;
this._rejectEvent = reject;
});
}
handleEvent(e) {
if (this._done) {
return;
} else if (e.getType() === this._expectedEvent) {
this._expectedEvent = undefined;
this._rejectEvent = undefined;
this._resetTimer();
this._resolveEvent(e);
} else {
this._expectedEvent = undefined;
const exception = new Error(
"Unexpected message: expecting " + this._expectedEvent
+ " but got " + e.getType(),
);
if (this._rejectEvent) {
const reject = this._rejectEvent;
this._rejectEvent = undefined;
reject(exception);
}
this.cancel(exception);
}
}
done() {
this._endTimer(); // always kill the activity timer
if (!this._done) {
this._resolve();
}
}
cancel(e) {
this._endTimer(); // always kill the activity timer
if (!this._done) {
this.cancelled = true;
if (this.userId && this.deviceId && this.transactionId) {
// send a cancellation to the other user (if it wasn't
// cancelled by the other user)
if (e === timeoutException) {
const timeoutEvent = newTimeoutError();
this._sendToDevice(timeoutEvent.getType(), timeoutEvent.getContent());
} else if (e instanceof MatrixEvent) {
const sender = e.getSender();
if (sender !== this.userId) {
const content = e.getContent();
if (e.getType() === "m.key.verification.cancel") {
content.code = content.code || "m.unknown";
content.reason = content.reason || content.body
|| "Unknown reason";
content.transaction_id = this.transactionId;
this._sendToDevice("m.key.verification.cancel", content);
} else {
this._sendToDevice("m.key.verification.cancel", {
code: "m.unknown",
reason: content.body || "Unknown reason",
transaction_id: this.transactionId,
});
}
}
} else {
this._sendToDevice("m.key.verification.cancel", {
code: "m.unknown",
reason: e.toString(),
transaction_id: this.transactionId,
});
}
}
if (this._promise !== null) {
// when we cancel without a promise, we end up with a promise
// but no reject function. If cancel is called again, we'd error.
if (this._reject) this._reject(e);
} else {
this._promise = Promise.reject(e);
}
// Also emit a 'cancel' event that the app can listen for to detect cancellation
// before calling verify()
this.emit('cancel', e);
}
}
/**
* Begin the key verification
*
* @returns {Promise} Promise which resolves when the verification has
* completed.
*/
verify() {
if (this._promise) return this._promise;
this._promise = new Promise((resolve, reject) => {
this._resolve = (...args) => {
this._done = true;
this._endTimer();
resolve(...args);
};
this._reject = (...args) => {
this._done = true;
this._endTimer();
reject(...args);
};
});
if (this._doVerification && !this._started) {
this._started = true;
this._resetTimer(); // restart the timeout
Promise.resolve(this._doVerification())
.then(this.done.bind(this), this.cancel.bind(this));
}
return this._promise;
}
async _verifyKeys(userId, keys, verifier) {
// we try to verify all the keys that we're told about, but we might
// not know about all of them, so keep track of the keys that we know
// about, and ignore the rest
const verifiedDevices = [];
for (const [keyId, keyInfo] of Object.entries(keys)) {
const deviceId = keyId.split(':', 2)[1];
const device = await this._baseApis.getStoredDevice(userId, deviceId);
if (!device) {
logger.warn(`verification: Could not find device ${deviceId} to verify`);
} else {
await verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId);
}
}
// if none of the keys could be verified, then error because the app
// should be informed about that
if (!verifiedDevices.length) {
throw new Error("No devices could be verified");
}
for (const deviceId of verifiedDevices) {
await this._baseApis.setDeviceVerified(userId, deviceId);
}
}
}
-87
View File
@@ -1,87 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Error messages.
*
* @module crypto/verification/Error
*/
import {MatrixEvent} from "../../models/event";
export function newVerificationError(code, reason, extradata) {
extradata = extradata || {};
extradata.code = code;
extradata.reason = reason;
return new MatrixEvent({
type: "m.key.verification.cancel",
content: extradata,
});
}
export function errorFactory(code, reason) {
return function(extradata) {
return newVerificationError(code, reason, extradata);
};
}
/**
* The verification was cancelled by the user.
*/
export const newUserCancelledError = errorFactory("m.user", "Cancelled by user");
/**
* The verification timed out.
*/
export const newTimeoutError = errorFactory("m.timeout", "Timed out");
/**
* The transaction is unknown.
*/
export const newUnknownTransactionError = errorFactory(
"m.unknown_transaction", "Unknown transaction",
);
/**
* An unknown method was selected.
*/
export const newUnknownMethodError = errorFactory("m.unknown_method", "Unknown method");
/**
* An unexpected message was sent.
*/
export const newUnexpectedMessageError = errorFactory(
"m.unexpected_message", "Unexpected message",
);
/**
* The key does not match.
*/
export const newKeyMismatchError = errorFactory(
"m.key_mismatch", "Key mismatch",
);
/**
* The user does not match.
*/
export const newUserMismatchError = errorFactory("m.user_error", "User mismatch");
/**
* An invalid message was sent.
*/
export const newInvalidMessageError = errorFactory(
"m.invalid_message", "Invalid message",
);
-123
View File
@@ -1,123 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* QR code key verification.
* @module crypto/verification/QRCode
*/
import Base from "./Base";
import {
errorFactory,
newUserCancelledError,
newKeyMismatchError,
newUserMismatchError,
} from './Error';
const MATRIXTO_REGEXP = /^(?:https?:\/\/)?(?:www\.)?matrix\.to\/#\/([#@!+][^?]+)\?(.+)$/;
const KEY_REGEXP = /^key_([^:]+:.+)$/;
const newQRCodeError = errorFactory("m.qr_code.invalid", "Invalid QR code");
/**
* @class crypto/verification/QRCode/ShowQRCode
* @extends {module:crypto/verification/Base}
*/
export class ShowQRCode extends Base {
_doVerification() {
if (!this._done) {
const url = "https://matrix.to/#/" + this._baseApis.getUserId()
+ "?device=" + encodeURIComponent(this._baseApis.deviceId)
+ "&action=verify&key_ed25519%3A"
+ encodeURIComponent(this._baseApis.deviceId) + "="
+ encodeURIComponent(this._baseApis.getDeviceEd25519Key());
this.emit("show_qr_code", {
url: url,
});
}
}
}
ShowQRCode.NAME = "m.qr_code.show.v1";
/**
* @class crypto/verification/QRCode/ScanQRCode
* @extends {module:crypto/verification/Base}
*/
export class ScanQRCode extends Base {
static factory(...args) {
return new ScanQRCode(...args);
}
async _doVerification() {
const code = await new Promise((resolve, reject) => {
this.emit("scan", {
done: resolve,
cancel: () => reject(newUserCancelledError()),
});
});
const match = code.match(MATRIXTO_REGEXP);
let deviceId;
const keys = {};
if (!match) {
throw newQRCodeError();
}
const userId = match[1];
const params = match[2].split("&").map(
(x) => x.split("=", 2).map(decodeURIComponent),
);
let action;
for (const [name, value] of params) {
if (name === "device") {
deviceId = value;
} else if (name === "action") {
action = value;
} else {
const keyMatch = name.match(KEY_REGEXP);
if (keyMatch) {
keys[keyMatch[1]] = value;
}
}
}
if (!deviceId || action !== "verify" || Object.keys(keys).length === 0) {
throw newQRCodeError();
}
if (!this.userId) {
await new Promise((resolve, reject) => {
this.emit("confirm_user_id", {
userId: userId,
confirm: resolve,
cancel: () => reject(newUserMismatchError()),
});
});
} else if (this.userId !== userId) {
throw newUserMismatchError({
expected: this.userId,
actual: userId,
});
}
await this._verifyKeys(userId, keys, (keyId, device, key) => {
if (device.keys[keyId] !== key) {
throw newKeyMismatchError();
}
});
}
}
ScanQRCode.NAME = "m.qr_code.scan.v1";
-399
View File
@@ -1,399 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Short Authentication String (SAS) verification.
* @module crypto/verification/SAS
*/
import Base from "./Base";
import anotherjson from 'another-json';
import {
errorFactory,
newUserCancelledError,
newUnknownMethodError,
newKeyMismatchError,
newInvalidMessageError,
} from './Error';
const EVENTS = [
"m.key.verification.accept",
"m.key.verification.key",
"m.key.verification.mac",
];
let olmutil;
const newMismatchedSASError = errorFactory(
"m.mismatched_sas", "Mismatched short authentication string",
);
const newMismatchedCommitmentError = errorFactory(
"m.mismatched_commitment", "Mismatched commitment",
);
function generateDecimalSas(sasBytes) {
/**
* +--------+--------+--------+--------+--------+
* | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
* +--------+--------+--------+--------+--------+
* bits: 87654321 87654321 87654321 87654321 87654321
* \____________/\_____________/\____________/
* 1st number 2nd number 3rd number
*/
return [
(sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000,
((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000,
((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000,
];
}
const emojiMapping = [
["🐶", "dog"], // 0
["🐱", "cat"], // 1
["🦁", "lion"], // 2
["🐎", "horse"], // 3
["🦄", "unicorn"], // 4
["🐷", "pig"], // 5
["🐘", "elephant"], // 6
["🐰", "rabbit"], // 7
["🐼", "panda"], // 8
["🐓", "rooster"], // 9
["🐧", "penguin"], // 10
["🐢", "turtle"], // 11
["🐟", "fish"], // 12
["🐙", "octopus"], // 13
["🦋", "butterfly"], // 14
["🌷", "flower"], // 15
["🌳", "tree"], // 16
["🌵", "cactus"], // 17
["🍄", "mushroom"], // 18
["🌏", "globe"], // 19
["🌙", "moon"], // 20
["☁️", "cloud"], // 21
["🔥", "fire"], // 22
["🍌", "banana"], // 23
["🍎", "apple"], // 24
["🍓", "strawberry"], // 25
["🌽", "corn"], // 26
["🍕", "pizza"], // 27
["🎂", "cake"], // 28
["❤️", "heart"], // 29
["🙂", "smiley"], // 30
["🤖", "robot"], // 31
["🎩", "hat"], // 32
["👓", "glasses"], // 33
["🔧", "spanner"], // 34
["🎅", "santa"], // 35
["👍", "thumbs up"], // 36
["☂️", "umbrella"], // 37
["⌛", "hourglass"], // 38
["⏰", "clock"], // 39
["🎁", "gift"], // 40
["💡", "light bulb"], // 41
["📕", "book"], // 42
["✏️", "pencil"], // 43
["📎", "paperclip"], // 44
["✂️", "scissors"], // 45
["🔒", "padlock"], // 46
["🔑", "key"], // 47
["🔨", "hammer"], // 48
["☎️", "telephone"], // 49
["🏁", "flag"], // 50
["🚂", "train"], // 51
["🚲", "bicycle"], // 52
["✈️", "aeroplane"], // 53
["🚀", "rocket"], // 54
["🏆", "trophy"], // 55
["⚽", "ball"], // 56
["🎸", "guitar"], // 57
["🎺", "trumpet"], // 58
["🔔", "bell"], // 59
["⚓️", "anchor"], // 60
["🎧", "headphones"], // 61
["📁", "folder"], // 62
["📌", "pin"], // 63
];
function generateEmojiSas(sasBytes) {
const emojis = [
// just like base64 encoding
sasBytes[0] >> 2,
(sasBytes[0] & 0x3) << 4 | sasBytes[1] >> 4,
(sasBytes[1] & 0xf) << 2 | sasBytes[2] >> 6,
sasBytes[2] & 0x3f,
sasBytes[3] >> 2,
(sasBytes[3] & 0x3) << 4 | sasBytes[4] >> 4,
(sasBytes[4] & 0xf) << 2 | sasBytes[5] >> 6,
];
return emojis.map((num) => emojiMapping[num]);
}
const sasGenerators = {
decimal: generateDecimalSas,
emoji: generateEmojiSas,
};
function generateSas(sasBytes, methods) {
const sas = {};
for (const method of methods) {
if (method in sasGenerators) {
sas[method] = sasGenerators[method](sasBytes);
}
}
return sas;
}
const macMethods = {
"hkdf-hmac-sha256": "calculate_mac",
"hmac-sha256": "calculate_mac_long_kdf",
};
/* lists of algorithms/methods that are supported. The key agreement, hashes,
* and MAC lists should be sorted in order of preference (most preferred
* first).
*/
const KEY_AGREEMENT_LIST = ["curve25519"];
const HASHES_LIST = ["sha256"];
const MAC_LIST = ["hkdf-hmac-sha256", "hmac-sha256"];
const SAS_LIST = Object.keys(sasGenerators);
const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST);
const HASHES_SET = new Set(HASHES_LIST);
const MAC_SET = new Set(MAC_LIST);
const SAS_SET = new Set(SAS_LIST);
function intersection(anArray, aSet) {
return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : [];
}
/**
* @alias module:crypto/verification/SAS
* @extends {module:crypto/verification/Base}
*/
export default class SAS extends Base {
get events() {
return EVENTS;
}
async _doVerification() {
await global.Olm.init();
olmutil = olmutil || new global.Olm.Utility();
// make sure user's keys are downloaded
await this._baseApis.downloadKeys([this.userId]);
if (this.startEvent) {
return await this._doRespondVerification();
} else {
return await this._doSendVerification();
}
}
async _doSendVerification() {
const initialMessage = {
method: SAS.NAME,
from_device: this._baseApis.deviceId,
key_agreement_protocols: KEY_AGREEMENT_LIST,
hashes: HASHES_LIST,
message_authentication_codes: MAC_LIST,
// FIXME: allow app to specify what SAS methods can be used
short_authentication_string: SAS_LIST,
transaction_id: this.transactionId,
};
this._sendToDevice("m.key.verification.start", initialMessage);
let e = await this._waitForEvent("m.key.verification.accept");
let content = e.getContent();
const sasMethods
= intersection(content.short_authentication_string, SAS_SET);
if (!(KEY_AGREEMENT_SET.has(content.key_agreement_protocol)
&& HASHES_SET.has(content.hash)
&& MAC_SET.has(content.message_authentication_code)
&& sasMethods.length)) {
throw newUnknownMethodError();
}
if (typeof content.commitment !== "string") {
throw newInvalidMessageError();
}
const macMethod = content.message_authentication_code;
const hashCommitment = content.commitment;
const olmSAS = new global.Olm.SAS();
try {
this._sendToDevice("m.key.verification.key", {
key: olmSAS.get_pubkey(),
});
e = await this._waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
const commitmentStr = content.key + anotherjson.stringify(initialMessage);
// TODO: use selected hash function (when we support multiple)
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
throw newMismatchedCommitmentError();
}
olmSAS.set_their_key(content.key);
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.userId + this.deviceId
+ this.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const verifySAS = new Promise((resolve, reject) => {
this.emit("show_sas", {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS, macMethod);
resolve();
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
});
});
[e] = await Promise.all([
this._waitForEvent("m.key.verification.mac"),
verifySAS,
]);
content = e.getContent();
await this._checkMAC(olmSAS, content, macMethod);
} finally {
olmSAS.free();
}
}
async _doRespondVerification() {
let content = this.startEvent.getContent();
// Note: we intersect using our pre-made lists, rather than the sets,
// so that the result will be in our order of preference. Then
// fetching the first element from the array will give our preferred
// method out of the ones offered by the other party.
const keyAgreement
= intersection(
KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols),
)[0];
const hashMethod
= intersection(HASHES_LIST, new Set(content.hashes))[0];
const macMethod
= intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
// FIXME: allow app to specify what SAS methods can be used
const sasMethods
= intersection(content.short_authentication_string, SAS_SET);
if (!(keyAgreement !== undefined
&& hashMethod !== undefined
&& macMethod !== undefined
&& sasMethods.length)) {
throw newUnknownMethodError();
}
const olmSAS = new global.Olm.SAS();
try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
this._sendToDevice("m.key.verification.accept", {
key_agreement_protocol: keyAgreement,
hash: hashMethod,
message_authentication_code: macMethod,
short_authentication_string: sasMethods,
// TODO: use selected hash function (when we support multiple)
commitment: olmutil.sha256(commitmentStr),
});
let e = await this._waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
olmSAS.set_their_key(content.key);
this._sendToDevice("m.key.verification.key", {
key: olmSAS.get_pubkey(),
});
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
+ this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const verifySAS = new Promise((resolve, reject) => {
this.emit("show_sas", {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS, macMethod);
resolve();
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
});
});
[e] = await Promise.all([
this._waitForEvent("m.key.verification.mac"),
verifySAS,
]);
content = e.getContent();
await this._checkMAC(olmSAS, content, macMethod);
} finally {
olmSAS.free();
}
}
_sendMAC(olmSAS, method) {
const keyId = `ed25519:${this._baseApis.deviceId}`;
const mac = {};
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.userId + this.deviceId
+ this.transactionId;
mac[keyId] = olmSAS[macMethods[method]](
this._baseApis.getDeviceEd25519Key(),
baseInfo + keyId,
);
const keys = olmSAS[macMethods[method]](
keyId,
baseInfo + "KEY_IDS",
);
this._sendToDevice("m.key.verification.mac", { mac, keys });
}
async _checkMAC(olmSAS, content, method) {
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.transactionId;
if (content.keys !== olmSAS[macMethods[method]](
Object.keys(content.mac).sort().join(","),
baseInfo + "KEY_IDS",
)) {
throw newKeyMismatchError();
}
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
if (keyInfo !== olmSAS[macMethods[method]](
device.keys[keyId],
baseInfo + keyId,
)) {
throw newKeyMismatchError();
}
});
}
}
SAS.NAME = "m.sas.v1";
+92 -59
View File
@@ -1,6 +1,5 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -23,7 +22,6 @@ import Promise from 'bluebird';
const parseContentType = require('content-type').parse;
const utils = require("./utils");
import logger from '../src/logger';
// we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
@@ -47,15 +45,10 @@ module.exports.PREFIX_R0 = "/_matrix/client/r0";
module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
/**
* URI path for v1 of the the identity API
* URI path for the identity API
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* URI path for the v2 identity API
*/
module.exports.PREFIX_IDENTITY_V2 = "/_matrix/identity/v2";
/**
* URI path for the media repo API
*/
@@ -96,13 +89,6 @@ module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
};
module.exports.MatrixHttpApi.prototype = {
/**
* Sets the baase URL for the identity server
* @param {string} url The new base url
*/
setIdBaseUrl: function(url) {
this.opts.idBaseUrl = url;
},
/**
* Get the content repository url with query parameters.
@@ -115,7 +101,7 @@ module.exports.MatrixHttpApi.prototype = {
};
return {
base: this.opts.baseUrl,
path: "/_matrix/media/r0/upload",
path: "/_matrix/media/v1/upload",
params: params,
};
},
@@ -178,21 +164,9 @@ module.exports.MatrixHttpApi.prototype = {
const contentType = opts.type || file.type || 'application/octet-stream';
const fileName = opts.name || file.name;
// We used to recommend setting file.stream to the thing to upload on
// Node.js. As of 2019-06-11, this is still in widespread use in various
// clients, so we should preserve this for simple objects used in
// Node.js. File API objects (via either the File or Blob interfaces) in
// the browser now define a `stream` method, which leads to trouble
// here, so we also check the type of `stream`.
let body = file;
if (body.stream && typeof body.stream !== "function") {
logger.warn(
"Using `file.stream` as the content to upload. Future " +
"versions of the js-sdk will change this to expect `file` to " +
"be the content directly.",
);
body = body.stream;
}
// we used to recommend setting file.stream to the thing to upload on
// nodejs.
const body = file.stream ? file.stream : file;
// backwards-compatibility hacks where we used to do different things
// between browser and node.
@@ -201,7 +175,7 @@ module.exports.MatrixHttpApi.prototype = {
if (global.XMLHttpRequest) {
rawResponse = false;
} else {
logger.warn(
console.warn(
"Returning the raw JSON from uploadContent(). Future " +
"versions of the js-sdk will change this default, to " +
"return the parsed object. Set opts.rawResponse=false " +
@@ -214,7 +188,7 @@ module.exports.MatrixHttpApi.prototype = {
let onlyContentUri = opts.onlyContentUri;
if (!rawResponse && onlyContentUri === undefined) {
if (global.XMLHttpRequest) {
logger.warn(
console.warn(
"Returning only the content-uri from uploadContent(). " +
"Future versions of the js-sdk will change this " +
"default, to return the whole response object. Set " +
@@ -304,7 +278,7 @@ module.exports.MatrixHttpApi.prototype = {
});
}
});
let url = this.opts.baseUrl + "/_matrix/media/r0/upload";
let url = this.opts.baseUrl + "/_matrix/media/v1/upload";
const queryArgs = [];
@@ -340,7 +314,7 @@ module.exports.MatrixHttpApi.prototype = {
promise = this.authedRequest(
opts.callback, "POST", "/upload", queryParams, body, {
prefix: "/_matrix/media/r0",
prefix: "/_matrix/media/v1",
headers: {"Content-Type": contentType},
json: false,
bodyParser: bodyParser,
@@ -381,18 +355,7 @@ module.exports.MatrixHttpApi.prototype = {
return this.uploads;
},
idServerRequest: function(
callback,
method,
path,
params,
prefix,
accessToken,
) {
if (!this.opts.idBaseUrl) {
throw new Error("No Identity Server base URL set");
}
idServerRequest: function(callback, method, path, params, prefix) {
const fullUri = this.opts.idBaseUrl + prefix + path;
if (callback !== undefined && !utils.isFunction(callback)) {
@@ -405,17 +368,13 @@ module.exports.MatrixHttpApi.prototype = {
uri: fullUri,
method: method,
withCredentials: false,
json: true, // we want a JSON response if we can
json: false,
_matrix_opts: this.opts,
headers: {},
};
if (method === 'GET') {
if (method == 'GET') {
opts.qs = params;
} else if (typeof params === "object") {
opts.json = params;
}
if (accessToken) {
opts.headers['Authorization'] = `Bearer ${accessToken}`;
} else {
opts.form = params;
}
const defer = Promise.defer();
@@ -423,7 +382,12 @@ module.exports.MatrixHttpApi.prototype = {
opts,
requestCallback(defer, callback, this.opts.onlyData),
);
return defer.promise;
// ID server does not always take JSON, so we can't use requests' 'json'
// option as we do with the home server, but it does return JSON, so
// parse it manually
return defer.promise.then(function(response) {
return JSON.parse(response);
});
},
/**
@@ -493,7 +457,7 @@ module.exports.MatrixHttpApi.prototype = {
const self = this;
requestPromise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out", err);
self.event_emitter.emit("Session.logged_out");
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
self.event_emitter.emit(
"no_consent",
@@ -548,6 +512,76 @@ module.exports.MatrixHttpApi.prototype = {
);
},
/**
* Perform an authorised request to the homeserver with a specific path
* prefix which overrides the default for this call only. Useful for hitting
* different Matrix Client-Server versions.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer authedRequest with opts.prefix
*/
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
prefix, localTimeoutMs) {
return this.authedRequest(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
},
);
},
/**
* Perform a request to the homeserver without any credentials but with a
* specific path prefix which overrides the default for this call only.
* Useful for hitting different Matrix Client-Server versions.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer request with opts.prefix
*/
requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
localTimeoutMs) {
return this.request(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
},
);
},
/**
* Perform a request to an arbitrary URL.
* @param {Function} callback Optional. The callback to invoke on
@@ -836,8 +870,7 @@ function parseErrorResponse(response, body) {
let err;
if (contentType) {
if (contentType.type === 'application/json') {
const jsonBody = typeof(body) === 'object' ? body : JSON.parse(body);
err = new module.exports.MatrixError(jsonBody);
err = new module.exports.MatrixError(JSON.parse(body));
} else if (contentType.type === 'text/plain') {
err = new Error(`Server returned ${httpStatus} error: ${body}`);
}
-52
View File
@@ -1,52 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
/**
* Check if an IndexedDB database exists. The only way to do so is to try opening it, so
* we do that and then delete it did not exist before.
*
* @param {Object} indexedDB The `indexedDB` interface
* @param {string} dbName The database name to test for
* @returns {boolean} Whether the database exists
*/
export function exists(indexedDB, dbName) {
return new Promise((resolve, reject) => {
let exists = true;
const req = indexedDB.open(dbName);
req.onupgradeneeded = () => {
// Since we did not provide an explicit version when opening, this event
// should only fire if the DB did not exist before at any version.
exists = false;
};
req.onblocked = () => reject();
req.onsuccess = () => {
const db = req.result;
db.close();
if (!exists) {
// The DB did not exist before, but has been created as part of this
// existence check. Delete it now to restore previous state. Delete can
// actually take a while to complete in some browsers, so don't wait for
// it. This won't block future open calls that a store might issue next to
// properly set up the DB.
indexedDB.deleteDatabase(dbName);
}
resolve(exists);
};
req.onerror = ev => reject(ev.target.error);
});
}
+79 -164
View File
@@ -1,7 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -22,7 +21,6 @@ import Promise from 'bluebird';
const url = require("url");
const utils = require("./utils");
import logger from '../src/logger';
const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn";
@@ -49,18 +47,11 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
* @param {object?} opts.authData error response from the last request. If
* null, a request will be made with no auth before starting.
*
* @param {function(object?): module:client.Promise} opts.doRequest
* called with the new auth dict to submit the request. Also passes a
* second deprecated arg which is a flag set to true if this request
* is a background request. The busyChanged callback should be used
* instead of the backfround flag. Should return a promise which resolves
* to the successful response or rejects with a MatrixError.
*
* @param {function(bool): module:client.Promise} opts.busyChanged
* called whenever the interactive auth logic becomes busy submitting
* information provided by the user or finsihes. After this has been
* called with true the UI should indicate that a request is in progress
* until it is called again with false.
* @param {function(object?, bool?): module:client.Promise} opts.doRequest
* called with the new auth dict to submit the request and a flag set
* to true if this request is a background request. Should return a
* promise which resolves to the successful response or rejects with a
* MatrixError.
*
* @param {function(string, object?)} opts.stateUpdated
* called when the status of the UI auth changes, ie. when the state of
@@ -97,37 +88,22 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
* @param {string?} opts.emailSid If returning from having completed m.login.email.identity
* auth, the sid for the email verification session.
*
* @param {function?} opts.requestEmailToken A function that takes the email address (string),
* clientSecret (string), attempt number (int) and sessionId (string) and calls the
* relevant requestToken function and returns the promise returned by that function.
* If the resulting promise rejects, the rejection will propagate through to the
* attemptAuth promise.
*
*/
function InteractiveAuth(opts) {
this._matrixClient = opts.matrixClient;
this._data = opts.authData || {};
this._requestCallback = opts.doRequest;
this._busyChangedCallback = opts.busyChanged;
// startAuthStage included for backwards compat
this._stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage;
this._resolveFunc = null;
this._rejectFunc = null;
this._completionDeferred = null;
this._inputs = opts.inputs || {};
this._requestEmailTokenCallback = opts.requestEmailToken;
if (opts.sessionId) this._data.session = opts.sessionId;
this._clientSecret = opts.clientSecret || this._matrixClient.generateClientSecret();
this._emailSid = opts.emailSid;
if (this._emailSid === undefined) this._emailSid = null;
this._requestingEmailToken = false;
this._chosenFlow = null;
this._currentStage = null;
// if we are currently trying to submit an auth dict (which includes polling)
// the promise the will resolve/reject when it completes
this._submitPromise = null;
}
InteractiveAuth.prototype = {
@@ -139,22 +115,19 @@ InteractiveAuth.prototype = {
* no suitable authentication flow can be found
*/
attemptAuth: function() {
// This promise will be quite long-lived and will resolve when the
// request is authenticated and completes successfully.
return new Promise((resolve, reject) => {
this._resolveFunc = resolve;
this._rejectFunc = reject;
this._completionDeferred = Promise.defer();
// wrap in a promise so that if _startNextAuthStage
// throws, it rejects the promise in a consistent way
return Promise.resolve().then(() => {
// if we have no flows, try a request (we'll have
// just a session ID in _data if resuming)
if (!this._data.flows) {
if (this._busyChangedCallback) this._busyChangedCallback(true);
this._doRequest(this._data).finally(() => {
if (this._busyChangedCallback) this._busyChangedCallback(false);
});
this._doRequest(this._data);
} else {
this._startNextAuthStage();
}
return this._completionDeferred.promise;
});
},
@@ -163,30 +136,24 @@ InteractiveAuth.prototype = {
* completed out-of-band. If so, the attemptAuth promise will
* be resolved.
*/
poll: async function() {
poll: function() {
if (!this._data.session) return;
// if we currently have a request in flight, there's no point making
// another just to check what the status is
if (this._submitPromise) return;
let authDict = {};
if (this._currentStage == EMAIL_STAGE_TYPE) {
// The email can be validated out-of-band, but we need to provide the
// creds so the HS can go & check it.
if (this._emailSid) {
const creds = {
sid: this._emailSid,
client_secret: this._clientSecret,
};
if (await this._matrixClient.doesServerRequireIdServerParam()) {
const idServerParsedUrl = url.parse(
this._matrixClient.getIdentityServerUrl(),
);
creds.id_server = idServerParsedUrl.host;
}
const idServerParsedUrl = url.parse(
this._matrixClient.getIdentityServerUrl(),
);
authDict = {
type: EMAIL_STAGE_TYPE,
threepid_creds: creds,
threepid_creds: {
sid: this._emailSid,
client_secret: this._clientSecret,
id_server: idServerParsedUrl.host,
},
};
}
}
@@ -227,10 +194,6 @@ InteractiveAuth.prototype = {
return params[loginType];
},
getChosenFlow() {
return this._chosenFlow;
},
/**
* submit a new auth dict and fire off the request. This will either
* make attemptAuth resolve/reject, or cause the startAuthStage callback
@@ -243,44 +206,18 @@ InteractiveAuth.prototype = {
* in the attemptAuth promise being rejected. This can be set to true
* for requests that just poll to see if auth has been completed elsewhere.
*/
submitAuthDict: async function(authData, background) {
if (!this._resolveFunc) {
submitAuthDict: function(authData, background) {
if (!this._completionDeferred) {
throw new Error("submitAuthDict() called before attemptAuth()");
}
if (!background && this._busyChangedCallback) {
this._busyChangedCallback(true);
}
// if we're currently trying a request, wait for it to finish
// as otherwise we can get multiple 200 responses which can mean
// things like multiple logins for register requests.
// (but discard any expections as we only care when its done,
// not whether it worked or not)
while (this._submitPromise) {
try {
await this._submitPromise;
} catch (e) {
}
}
// use the sessionid from the last request.
const auth = {
session: this._data.session,
};
utils.extend(auth, authData);
try {
// NB. the 'background' flag is deprecated by the busyChanged
// callback and is here for backwards compat
this._submitPromise = this._doRequest(auth, background);
await this._submitPromise;
} finally {
this._submitPromise = null;
if (!background && this._busyChangedCallback) {
this._busyChangedCallback(false);
}
}
this._doRequest(auth, background);
},
/**
@@ -316,78 +253,58 @@ InteractiveAuth.prototype = {
* This can be set to true for requests that just poll to see if auth has
* been completed elsewhere.
*/
_doRequest: async function(auth, background) {
try {
const result = await this._requestCallback(auth, background);
this._resolveFunc(result);
} catch (error) {
// sometimes UI auth errors don't come with flows
const errorFlows = error.data ? error.data.flows : null;
const haveFlows = Boolean(this._data.flows) || Boolean(errorFlows);
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
// doesn't look like an interactive-auth failure.
if (!background) {
this._rejectFunc(error);
} else {
// We ignore all failures here (even non-UI auth related ones)
// since we don't want to suddenly fail if the internet connection
// had a blip whilst we were polling
logger.log(
"Background poll request failed doing UI auth: ignoring",
error,
);
}
}
// if the error didn't come with flows, completed flows or session ID,
// copy over the ones we have. Synapse sometimes sends responses without
// any UI auth data (eg. when polling for email validation, if the email
// has not yet been validated). This appears to be a Synapse bug, which
// we workaround here.
if (!error.data.flows && !error.data.completed && !error.data.session) {
error.data.flows = this._data.flows;
error.data.completed = this._data.completed;
error.data.session = this._data.session;
}
this._data = error.data;
this._startNextAuthStage();
_doRequest: function(auth, background) {
const self = this;
if (
!this._emailSid &&
!this._requestingEmailToken &&
this._chosenFlow.stages.includes('m.login.email.identity')
) {
// If we've picked a flow with email auth, we send the email
// now because we want the request to fail as soon as possible
// if the email address is not valid (ie. already taken or not
// registered, depending on what the operation is).
this._requestingEmailToken = true;
try {
const requestTokenResult = await this._requestEmailTokenCallback(
this._inputs.emailAddress,
this._clientSecret,
1, // TODO: Multiple send attempts?
this._data.session,
);
this._emailSid = requestTokenResult.sid;
// NB. promise is not resolved here - at some point, doRequest
// will be called again and if the user has jumped through all
// the hoops correctly, auth will be complete and the request
// will succeed.
// Also, we should expose the fact that this request has compledted
// so clients can know that the email has actually been sent.
} catch (e) {
// we failed to request an email token, so fail the request.
// This could be due to the email already beeing registered
// (or not being registered, depending on what we're trying
// to do) or it could be a network failure. Either way, pass
// the failure up as the user can't complete auth if we can't
// send the email, foe whatever reason.
this._rejectFunc(e);
} finally {
this._requestingEmailToken = false;
}
}
// hackery to make sure that synchronous exceptions end up in the catch
// handler (without the additional event loop entailed by q.fcall or an
// extra Promise.resolve().then)
let prom;
try {
prom = this._requestCallback(auth, background);
} catch (e) {
prom = Promise.reject(e);
}
prom = prom.then(
function(result) {
console.log("result from request: ", result);
self._completionDeferred.resolve(result);
}, function(error) {
// sometimes UI auth errors don't come with flows
const errorFlows = error.data ? error.data.flows : null;
const haveFlows = Boolean(self._data.flows) || Boolean(errorFlows);
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
// doesn't look like an interactive-auth failure. fail the whole lot.
throw error;
}
// if the error didn't come with flows, completed flows or session ID,
// copy over the ones we have. Synapse sometimes sends responses without
// any UI auth data (eg. when polling for email validation, if the email
// has not yet been validated). This appears to be a Synapse bug, which
// we workaround here.
if (!error.data.flows && !error.data.completed && !error.data.session) {
error.data.flows = self._data.flows;
error.data.completed = self._data.completed;
error.data.session = self._data.session;
}
self._data = error.data;
self._startNextAuthStage();
},
);
if (!background) {
prom = prom.catch((e) => {
this._completionDeferred.reject(e);
});
} else {
// We ignore all failures here (even non-UI auth related ones)
// since we don't want to suddenly fail if the internet connection
// had a blip whilst we were polling
prom = prom.catch((error) => {
console.log("Ignoring error from UI auth: " + error);
});
}
prom.done();
},
/**
@@ -403,7 +320,7 @@ InteractiveAuth.prototype = {
}
this._currentStage = nextStage;
if (nextStage === 'm.login.dummy') {
if (nextStage == 'm.login.dummy') {
this.submitAuthDict({
type: 'm.login.dummy',
});
@@ -433,12 +350,10 @@ InteractiveAuth.prototype = {
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
_chooseStage: function() {
if (this._chosenFlow === null) {
this._chosenFlow = this._chooseFlow();
}
logger.log("Active flow => %s", JSON.stringify(this._chosenFlow));
const nextStage = this._firstUncompletedStage(this._chosenFlow);
logger.log("Next stage: %s", nextStage);
const flow = this._chooseFlow();
console.log("Active flow => %s", JSON.stringify(flow));
const nextStage = this._firstUncompletedStage(flow);
console.log("Next stage: %s", nextStage);
return nextStage;
},
+7 -15
View File
@@ -1,7 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -23,14 +22,8 @@ module.exports.ContentHelpers = require("./content-helpers");
module.exports.MatrixEvent = require("./models/event").MatrixEvent;
/** The {@link module:models/event.EventStatus|EventStatus} enum. */
module.exports.EventStatus = require("./models/event").EventStatus;
/** The {@link module:store/memory.MemoryStore|MemoryStore} class. */
module.exports.MemoryStore = require("./store/memory").MemoryStore;
/**
* The {@link module:store/memory.MemoryStore|MemoryStore} class was previously
* exported as `MatrixInMemoryStore`, so this is preserved for SDK consumers.
* @deprecated Prefer `MemoryStore` going forward.
*/
module.exports.MatrixInMemoryStore = module.exports.MemoryStore;
/** The {@link module:store/memory.MatrixInMemoryStore|MatrixInMemoryStore} class. */
module.exports.MatrixInMemoryStore = require("./store/memory").MatrixInMemoryStore;
/** The {@link module:store/indexeddb.IndexedDBStore|IndexedDBStore} class. */
module.exports.IndexedDBStore = require("./store/indexeddb").IndexedDBStore;
/** The {@link module:store/indexeddb.IndexedDBStoreBackend|IndexedDBStoreBackend} class. */
@@ -77,7 +70,6 @@ module.exports.InteractiveAuth = require("./interactive-auth");
/** The {@link module:auto-discovery|AutoDiscovery} class. */
module.exports.AutoDiscovery = require("./autodiscovery").AutoDiscovery;
module.exports.SERVICE_TYPES = require('./service-types').SERVICE_TYPES;
module.exports.MemoryCryptoStore =
require("./crypto/store/memory-crypto-store").default;
@@ -96,21 +88,21 @@ module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCal
/**
* Set a preferred audio output device to use for MatrixCalls
* Set an audio output device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallAudioOutput = require('./webrtc/call').setAudioOutput;
/**
* Set a preferred audio input device to use for MatrixCalls
* 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 preferred video input device to use for MatrixCalls
* Set a video input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
@@ -172,7 +164,7 @@ module.exports.setCryptoStoreFactory = function(fac) {
* this is a string, it is assumed to be the base URL. These configuration
* options will be passed directly to {@link module:client~MatrixClient}.
* @param {Object} opts.store If not set, defaults to
* {@link module:store/memory.MemoryStore}.
* {@link module:store/memory.MatrixInMemoryStore}.
* @param {Object} opts.scheduler If not set, defaults to
* {@link module:scheduler~MatrixScheduler}.
* @param {requestFunction} opts.request If not set, defaults to the function
@@ -195,7 +187,7 @@ module.exports.createClient = function(opts) {
};
}
opts.request = opts.request || request;
opts.store = opts.store || new module.exports.MemoryStore({
opts.store = opts.store || new module.exports.MatrixInMemoryStore({
localStorage: global.localStorage,
});
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
+6 -190
View File
@@ -20,9 +20,6 @@ limitations under the License.
const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const EventTimeline = require("./event-timeline");
import {EventStatus} from "./event";
import logger from '../../src/logger';
import Relations from './relations';
// var DEBUG = false;
const DEBUG = true;
@@ -30,7 +27,7 @@ const DEBUG = true;
let debuglog;
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
debuglog = logger.log.bind(logger);
debuglog = console.log.bind(console);
} else {
debuglog = function() {};
}
@@ -57,48 +54,25 @@ if (DEBUG) {
* map from event_id to timeline and index.
*
* @constructor
* @param {?Room} room
* Room for this timelineSet. May be null for non-room cases, such as the
* notification timeline.
* @param {Object} opts Options inherited from Room.
*
* @param {boolean} [opts.timelineSupport = false]
* Set to true to enable improved timeline support.
* @param {Object} [opts.filter = null]
* The filter object, if any, for this timelineSet.
* @param {boolean} [opts.unstableClientRelationAggregation = false]
* Optional. Set to true to enable client-side aggregation of event relations
* via `getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
* @param {?Room} room the optional room for this timelineSet
* @param {Object} opts hash of options inherited from Room.
* opts.timelineSupport gives whether timeline support is enabled
* opts.filter is the filter object, if any, for this timelineSet.
*/
function EventTimelineSet(room, opts) {
this.room = room;
this._timelineSupport = Boolean(opts.timelineSupport);
this._liveTimeline = new EventTimeline(this);
this._unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
// just a list - *not* ordered.
this._timelines = [this._liveTimeline];
this._eventIdToTimeline = {};
this._filter = opts.filter || null;
if (this._unstableClientRelationAggregation) {
// A tree of objects to access a set of relations for an event, as in:
// this._relations[relatesToEventId][relationType][relationEventType]
this._relations = {};
}
}
utils.inherits(EventTimelineSet, EventEmitter);
/**
* Get all the timelines in this set
* @return {module:models/event-timeline~EventTimeline[]} the timelines in this set
*/
EventTimelineSet.prototype.getTimelines = function() {
return this._timelines;
};
/**
* Get the filter object this timeline set is filtered on, if any
* @return {?Filter} the optional filter for this timelineSet
@@ -431,38 +405,11 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
}
// time to join the timelines.
logger.info("Already have timeline for " + eventId +
console.info("Already have timeline for " + eventId +
" - joining timeline " + timeline + " to " +
existingTimeline);
// Variables to keep the line length limited below.
const existingIsLive = existingTimeline === this._liveTimeline;
const timelineIsLive = timeline === this._liveTimeline;
const backwardsIsLive = direction === EventTimeline.BACKWARDS && existingIsLive;
const forwardsIsLive = direction === EventTimeline.FORWARDS && timelineIsLive;
if (backwardsIsLive || forwardsIsLive) {
// The live timeline should never be spliced into a non-live position.
// We use independent logging to better discover the problem at a glance.
if (backwardsIsLive) {
logger.warn(
"Refusing to set a preceding existingTimeLine on our " +
"timeline as the existingTimeLine is live (" + existingTimeline + ")",
);
}
if (forwardsIsLive) {
logger.warn(
"Refusing to set our preceding timeline on a existingTimeLine " +
"as our timeline is live (" + timeline + ")",
);
}
continue; // abort splicing - try next event
}
timeline.setNeighbouringTimeline(existingTimeline, direction);
existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
timeline = existingTimeline;
didUpdate = true;
}
@@ -471,14 +418,6 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
// new information, we update the pagination token for whatever
// timeline we ended up on.
if (lastEventWasNew || !didUpdate) {
if (direction === EventTimeline.FORWARDS && timeline === this._liveTimeline) {
logger.warn({lastEventWasNew, didUpdate}); // for debugging
logger.warn(
`Refusing to set forwards pagination token of live timeline ` +
`${timeline} to ${paginationToken}`,
);
return;
}
timeline.setPaginationToken(paginationToken, direction);
}
};
@@ -548,9 +487,6 @@ EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
timeline.addEvent(event, toStartOfTimeline);
this._eventIdToTimeline[eventId] = timeline;
this.setRelationsTarget(event);
this.aggregateRelations(event);
const data = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
@@ -685,126 +621,6 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
return null;
};
/**
* Get a collection of relations to a given event in this timeline set.
*
* @param {String} eventId
* The ID of the event that you'd like to access relation events for.
* For example, with annotations, this would be the ID of the event being annotated.
* @param {String} relationType
* The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
* @param {String} eventType
* The relation event's type, such as "m.reaction", etc.
*
* @returns {Relations}
* A container for relation events.
*/
EventTimelineSet.prototype.getRelationsForEvent = function(
eventId, relationType, eventType,
) {
if (!this._unstableClientRelationAggregation) {
throw new Error("Client-side relation aggregation is disabled");
}
if (!eventId || !relationType || !eventType) {
throw new Error("Invalid arguments for `getRelationsForEvent`");
}
// debuglog("Getting relations for: ", eventId, relationType, eventType);
const relationsForEvent = this._relations[eventId] || {};
const relationsWithRelType = relationsForEvent[relationType] || {};
return relationsWithRelType[eventType];
};
/**
* Set an event as the target event if any Relations exist for it already
*
* @param {MatrixEvent} event
* The event to check as relation target.
*/
EventTimelineSet.prototype.setRelationsTarget = function(event) {
if (!this._unstableClientRelationAggregation) {
return;
}
const relationsForEvent = this._relations[event.getId()];
if (!relationsForEvent) {
return;
}
// don't need it for non m.replace relations for now
const relationsWithRelType = relationsForEvent["m.replace"];
if (!relationsWithRelType) {
return;
}
// only doing replacements for messages for now (e.g. edits)
const relationsWithEventType = relationsWithRelType["m.room.message"];
if (relationsWithEventType) {
relationsWithEventType.setTargetEvent(event);
}
};
/**
* Add relation events to the relevant relation collection.
*
* @param {MatrixEvent} event
* The new relation event to be aggregated.
*/
EventTimelineSet.prototype.aggregateRelations = function(event) {
if (!this._unstableClientRelationAggregation) {
return;
}
if (event.isRedacted() || event.status === EventStatus.CANCELLED) {
return;
}
// If the event is currently encrypted, wait until it has been decrypted.
if (event.isBeingDecrypted()) {
event.once("Event.decrypted", () => {
this.aggregateRelations(event);
});
return;
}
const relation = event.getRelation();
if (!relation) {
return;
}
const relatesToEventId = relation.event_id;
const relationType = relation.rel_type;
const eventType = event.getType();
// debuglog("Aggregating relation: ", event.getId(), eventType, relation);
let relationsForEvent = this._relations[relatesToEventId];
if (!relationsForEvent) {
relationsForEvent = this._relations[relatesToEventId] = {};
}
let relationsWithRelType = relationsForEvent[relationType];
if (!relationsWithRelType) {
relationsWithRelType = relationsForEvent[relationType] = {};
}
let relationsWithEventType = relationsWithRelType[eventType];
if (!relationsWithEventType) {
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
relationType,
eventType,
this.room,
);
const relatesToEvent = this.findEventById(relatesToEventId);
if (relatesToEvent) {
relationsWithEventType.setTargetEvent(relatesToEvent);
relatesToEvent.emit("Event.relationsCreated", relationType, eventType);
}
}
relationsWithEventType.addEvent(event);
};
/**
* The EventTimelineSet class.
*/
+1 -1
View File
@@ -276,7 +276,7 @@ EventTimeline.prototype.getNeighbouringTimeline = function(direction) {
EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction) {
if (this.getNeighbouringTimeline(direction)) {
throw new Error("timeline already has a neighbouring timeline - " +
"cannot reset neighbour (direction: " + direction + ")");
"cannot reset neighbour");
}
if (direction == EventTimeline.BACKWARDS) {
+30 -403
View File
@@ -24,14 +24,13 @@ limitations under the License.
import Promise from 'bluebird';
import {EventEmitter} from 'events';
import utils from '../utils.js';
import logger from '../../src/logger';
/**
* Enum for event statuses.
* @readonly
* @enum {string}
*/
const EventStatus = {
module.exports.EventStatus = {
/** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent",
@@ -49,15 +48,8 @@ const EventStatus = {
/** The event was cancelled before it was successfully sent. */
CANCELLED: "cancelled",
};
module.exports.EventStatus = EventStatus;
const interns = {};
function intern(str) {
if (!interns[str]) {
interns[str] = str;
}
return interns[str];
}
/**
* Construct a Matrix Event object
@@ -95,25 +87,20 @@ module.exports.MatrixEvent = function MatrixEvent(
if (!event[prop]) {
return;
}
event[prop] = intern(event[prop]);
if (!interns[event[prop]]) {
interns[event[prop]] = event[prop];
}
event[prop] = interns[event[prop]];
});
["membership", "avatar_url", "displayname"].forEach((prop) => {
if (!event.content || !event.content[prop]) {
return;
}
event.content[prop] = intern(event.content[prop]);
});
["rel_type"].forEach((prop) => {
if (
!event.content ||
!event.content["m.relates_to"] ||
!event.content["m.relates_to"][prop]
) {
return;
if (!interns[event.content[prop]]) {
interns[event.content[prop]] = event.content[prop];
}
event.content["m.relates_to"][prop] = intern(event.content["m.relates_to"][prop]);
event.content[prop] = interns[event.content[prop]];
});
this.event = event || {};
@@ -124,9 +111,6 @@ module.exports.MatrixEvent = function MatrixEvent(
this.error = null;
this.forwardLooking = true;
this._pushActions = null;
this._replacingEvent = null;
this._localRedactionEvent = null;
this._isCancelled = false;
this._clearEvent = {};
@@ -225,33 +209,12 @@ utils.extend(module.exports.MatrixEvent.prototype, {
},
/**
* Get the (decrypted, if necessary) event content JSON, even if the event
* was replaced by another event.
*
* @return {Object} The event content JSON, or an empty object.
*/
getOriginalContent: function() {
if (this._localRedactionEvent) {
return {};
}
return this._clearEvent.content || this.event.content || {};
},
/**
* Get the (decrypted, if necessary) event content JSON,
* or the content from the replacing event, if any.
* See `makeReplaced`.
* Get the (decrypted, if necessary) event content JSON.
*
* @return {Object} The event content JSON, or an empty object.
*/
getContent: function() {
if (this._localRedactionEvent) {
return {};
} else if (this._replacingEvent) {
return this._replacingEvent.getContent()["m.new_content"] || {};
} else {
return this.getOriginalContent();
}
return this._clearEvent.content || this.event.content || {};
},
/**
@@ -389,7 +352,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
if (
this._clearEvent && this._clearEvent.content &&
this._clearEvent.content.msgtype !== "m.bad.encrypted"
this._clearEvent.content.msgtype !== "m.bad.encrypted"
) {
// we may want to just ignore this? let's start with rejecting it.
throw new Error(
@@ -404,7 +367,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
// new info.
//
if (this._decryptionPromise) {
logger.log(
console.log(
`Event ${this.getId()} already being decrypted; queueing a retry`,
);
this._retryDecryption = true;
@@ -419,41 +382,15 @@ utils.extend(module.exports.MatrixEvent.prototype, {
* Cancel any room key request for this event and resend another.
*
* @param {module:crypto} crypto crypto module
* @param {string} userId the user who received this event
*
* @returns {Promise} a promise that resolves when the request is queued
*/
cancelAndResendKeyRequest: function(crypto, userId) {
cancelAndResendKeyRequest: function(crypto) {
const wireContent = this.getWireContent();
return crypto.requestRoomKey({
crypto.cancelRoomKeyRequest({
algorithm: wireContent.algorithm,
room_id: this.getRoomId(),
session_id: wireContent.session_id,
sender_key: wireContent.sender_key,
}, this.getKeyRequestRecipients(userId), true);
},
/**
* Calculate the recipients for keyshare requests.
*
* @param {string} userId the user who received this event.
*
* @returns {Array} array of recipients
*/
getKeyRequestRecipients: function(userId) {
// send the request to all of our own devices, and the
// original sending device if it wasn't us.
const wireContent = this.getWireContent();
const recipients = [{
userId, deviceId: '*',
}];
const sender = this.getSender();
if (sender !== userId) {
recipients.push({
userId: sender, deviceId: wireContent.device_id,
});
}
return recipients;
}, true);
},
_decryptionLoop: async function(crypto) {
@@ -478,7 +415,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
if (e.name !== "DecryptionError") {
// not a decryption error: log the whole exception as an error
// (and don't bother with a retry)
logger.error(
console.error(
`Error decrypting event (id=${this.getId()}): ${e.stack || e}`,
);
this._decryptionPromise = null;
@@ -504,7 +441,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
//
if (this._retryDecryption) {
// decryption error, but we have a retry queued.
logger.log(
console.log(
`Got error decrypting event (id=${this.getId()}: ` +
`${e}), but retrying`,
);
@@ -513,7 +450,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
// decryption error, no retries queued. Warn about the error and
// set it to m.bad.encrypted.
logger.warn(
console.warn(
`Error decrypting event (id=${this.getId()}): ${e.detailedString}`,
);
@@ -534,14 +471,6 @@ utils.extend(module.exports.MatrixEvent.prototype, {
this._retryDecryption = false;
this._setClearData(res);
// Before we emit the event, clear the push actions so that they can be recalculated
// by relevant code. We do this because the clear event has now changed, making it
// so that existing rules can be re-run over the applicable properties. Stuff like
// highlighting when the user's name is mentioned rely on this happening. We also want
// to set the push actions before emitting so that any notification listeners don't
// pick up the wrong contents.
this.setPushActions(null);
this.emit("Event.decrypted", this, err);
return;
@@ -582,17 +511,6 @@ utils.extend(module.exports.MatrixEvent.prototype, {
decryptionResult.forwardingCurve25519KeyChain || [];
},
/**
* Gets the cleartext content for this event. If the event is not encrypted,
* or encryption has not been completed, this will return null.
*
* @returns {Object} The cleartext (decrypted) content for the event
*/
getClearContent: function() {
const ev = this._clearEvent;
return ev && ev.content ? ev.content : null;
},
/**
* Check if the event is encrypted.
* @return {boolean} True if this event is encrypted.
@@ -674,27 +592,6 @@ utils.extend(module.exports.MatrixEvent.prototype, {
return this.event.unsigned || {};
},
unmarkLocallyRedacted: function() {
const value = this._localRedactionEvent;
this._localRedactionEvent = null;
if (this.event.unsigned) {
this.event.unsigned.redacted_because = null;
}
return !!value;
},
markLocallyRedacted: function(redactionEvent) {
if (this._localRedactionEvent) {
return;
}
this.emit("Event.beforeRedaction", this, redactionEvent);
this._localRedactionEvent = redactionEvent;
if (!this.event.unsigned) {
this.event.unsigned = {};
}
this.event.unsigned.redacted_because = redactionEvent.event;
},
/**
* Update the content of an event in the same way it would be by the server
* if it were redacted before it was sent to us
@@ -708,11 +605,6 @@ utils.extend(module.exports.MatrixEvent.prototype, {
throw new Error("invalid redaction_event in makeRedacted");
}
this._localRedactionEvent = null;
this.emit("Event.beforeRedaction", this, redaction_event);
this._replacingEvent = null;
// we attempt to replicate what we would see from the server if
// the event had been redacted before we saw it.
//
@@ -755,305 +647,40 @@ utils.extend(module.exports.MatrixEvent.prototype, {
return Boolean(this.getUnsigned().redacted_because);
},
/**
* Check if this event is a redaction of another event
*
* @return {boolean} True if this event is a redaction
*/
isRedaction: function() {
return this.getType() === "m.room.redaction";
},
/**
* Get the push actions, if known, for this event
*
* @return {?Object} push actions
*/
getPushActions: function() {
getPushActions: function() {
return this._pushActions;
},
},
/**
* Set the push actions for this event.
*
* @param {Object} pushActions push actions
*/
setPushActions: function(pushActions) {
setPushActions: function(pushActions) {
this._pushActions = pushActions;
},
},
/**
* Replace the `event` property and recalculate any properties based on it.
* @param {Object} event the object to assign to the `event` property
*/
handleRemoteEcho: function(event) {
const oldUnsigned = this.getUnsigned();
const oldId = this.getId();
/**
* Replace the `event` property and recalculate any properties based on it.
* @param {Object} event the object to assign to the `event` property
*/
handleRemoteEcho: function(event) {
this.event = event;
// if this event was redacted before it was sent, it's locally marked as redacted.
// At this point, we've received the remote echo for the event, but not yet for
// the redaction that we are sending ourselves. Preserve the locally redacted
// state by copying over redacted_because so we don't get a flash of
// redacted, not-redacted, redacted as remote echos come in
if (oldUnsigned.redacted_because) {
if (!this.event.unsigned) {
this.event.unsigned = {};
}
this.event.unsigned.redacted_because = oldUnsigned.redacted_because;
}
// successfully sent.
this.setStatus(null);
if (this.getId() !== oldId) {
// emit the event if it changed
this.emit("Event.localEventIdReplaced", this);
}
},
/**
* Whether the event is in any phase of sending, send failure, waiting for
* remote echo, etc.
*
* @return {boolean}
*/
isSending() {
return !!this.status;
},
/**
* Update the event's sending status and emit an event as well.
*
* @param {String} status The new status
*/
setStatus(status) {
this.status = status;
this.emit("Event.status", this, status);
},
replaceLocalEventId(eventId) {
this.event.event_id = eventId;
this.emit("Event.localEventIdReplaced", this);
},
/**
* Get whether the event is a relation event, and of a given type if
* `relType` is passed in.
*
* @param {string?} relType if given, checks that the relation is of the
* given type
* @return {boolean}
*/
isRelation(relType = undefined) {
// Relation info is lifted out of the encrypted content when sent to
// encrypted rooms, so we have to check `getWireContent` for this.
const content = this.getWireContent();
const relation = content && content["m.relates_to"];
return relation && relation.rel_type && relation.event_id &&
((relType && relation.rel_type === relType) || !relType);
},
/**
* Get relation info for the event, if any.
*
* @return {Object}
*/
getRelation() {
if (!this.isRelation()) {
return null;
}
return this.getWireContent()["m.relates_to"];
},
/**
* Set an event that replaces the content of this event, through an m.replace relation.
*
* @param {MatrixEvent?} newEvent the event with the replacing content, if any.
*/
makeReplaced(newEvent) {
// don't allow redacted events to be replaced.
// if newEvent is null we allow to go through though,
// as with local redaction, the replacing event might get
// cancelled, which should be reflected on the target event.
if (this.isRedacted() && newEvent) {
return;
}
if (this._replacingEvent !== newEvent) {
this._replacingEvent = newEvent;
this.emit("Event.replaced", this);
}
},
/**
* Returns the status of any associated edit or redaction
* (not for reactions/annotations as their local echo doesn't affect the orignal event),
* or else the status of the event.
*
* @return {EventStatus}
*/
getAssociatedStatus() {
if (this._replacingEvent) {
return this._replacingEvent.status;
} else if (this._localRedactionEvent) {
return this._localRedactionEvent.status;
}
return this.status;
},
getServerAggregatedRelation(relType) {
const relations = this.getUnsigned()["m.relations"];
if (relations) {
return relations[relType];
}
},
/**
* Returns the event ID of the event replacing the content of this event, if any.
*
* @return {string?}
*/
replacingEventId() {
const replaceRelation = this.getServerAggregatedRelation("m.replace");
if (replaceRelation) {
return replaceRelation.event_id;
} else if (this._replacingEvent) {
return this._replacingEvent.getId();
}
},
/**
* Returns the event replacing the content of this event, if any.
* Replacements are aggregated on the server, so this would only
* return an event in case it came down the sync, or for local echo of edits.
*
* @return {MatrixEvent?}
*/
replacingEvent() {
return this._replacingEvent;
},
/**
* Returns the origin_server_ts of the event replacing the content of this event, if any.
*
* @return {Date?}
*/
replacingEventDate() {
const replaceRelation = this.getServerAggregatedRelation("m.replace");
if (replaceRelation) {
const ts = replaceRelation.origin_server_ts;
if (Number.isFinite(ts)) {
return new Date(ts);
}
} else if (this._replacingEvent) {
return this._replacingEvent.getDate();
}
},
/**
* Returns the event that wants to redact this event, but hasn't been sent yet.
* @return {MatrixEvent} the event
*/
localRedactionEvent() {
return this._localRedactionEvent;
},
/**
* For relations and redactions, returns the event_id this event is referring to.
*
* @return {string?}
*/
getAssociatedId() {
const relation = this.getRelation();
if (relation) {
return relation.event_id;
} else if (this.isRedaction()) {
return this.event.redacts;
}
},
/**
* Checks if this event is associated with another event. See `getAssociatedId`.
*
* @return {bool}
*/
hasAssocation() {
return !!this.getAssociatedId();
},
/**
* Update the related id with a new one.
*
* Used to replace a local id with remote one before sending
* an event with a related id.
*
* @param {string} eventId the new event id
*/
updateAssociatedId(eventId) {
const relation = this.getRelation();
if (relation) {
relation.event_id = eventId;
} else if (this.isRedaction()) {
this.event.redacts = eventId;
}
},
/**
* Flags an event as cancelled due to future conditions. For example, a verification
* request event in the same sync transaction may be flagged as cancelled to warn
* listeners that a cancellation event is coming down the same pipe shortly.
* @param {boolean} cancelled Whether the event is to be cancelled or not.
*/
flagCancelled(cancelled = true) {
this._isCancelled = cancelled;
},
/**
* Gets whether or not the event is flagged as cancelled. See flagCancelled() for
* more information.
* @returns {boolean} True if the event is cancelled, false otherwise.
*/
isCancelled() {
return this._isCancelled;
},
/**
* Summarise the event as JSON for debugging. If encrypted, include both the
* decrypted and encrypted view of the event. This is named `toJSON` for use
* with `JSON.stringify` which checks objects for functions named `toJSON`
* and will call them to customise the output if they are defined.
*
* @return {Object}
*/
toJSON() {
const event = {
type: this.getType(),
sender: this.getSender(),
content: this.getContent(),
event_id: this.getId(),
origin_server_ts: this.getTs(),
unsigned: this.getUnsigned(),
room_id: this.getRoomId(),
};
// if this is a redaction then attach the redacts key
if (this.isRedaction()) {
event.redacts = this.event.redacts;
}
if (!this.isEncrypted()) {
return event;
}
return {
decrypted: event,
encrypted: this.event,
};
},
this.status = null;
},
});
/* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted
*
* This is specified here:
* http://matrix.org/speculator/spec/HEAD/client_server/latest.html#redactions
* http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#redactions
*
* Also:
* - We keep 'unsigned' since that is created by the local server
-342
View File
@@ -1,342 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import EventEmitter from 'events';
import { EventStatus } from '../../lib/models/event';
/**
* A container for relation events that supports easy access to common ways of
* aggregating such events. Each instance holds events that of a single relation
* type and event type. All of the events also relate to the same original event.
*
* The typical way to get one of these containers is via
* EventTimelineSet#getRelationsForEvent.
*/
export default class Relations extends EventEmitter {
/**
* @param {String} relationType
* The type of relation involved, such as "m.annotation", "m.reference",
* "m.replace", etc.
* @param {String} eventType
* The relation event's type, such as "m.reaction", etc.
* @param {?Room} room
* Room for this container. May be null for non-room cases, such as the
* notification timeline.
*/
constructor(relationType, eventType, room) {
super();
this.relationType = relationType;
this.eventType = eventType;
this._relations = new Set();
this._annotationsByKey = {};
this._annotationsBySender = {};
this._sortedAnnotationsByKey = [];
this._targetEvent = null;
}
/**
* Add relation events to this collection.
*
* @param {MatrixEvent} event
* The new relation event to be added.
*/
addEvent(event) {
if (this._relations.has(event)) {
return;
}
const relation = event.getRelation();
if (!relation) {
console.error("Event must have relation info");
return;
}
const relationType = relation.rel_type;
const eventType = event.getType();
if (this.relationType !== relationType || this.eventType !== eventType) {
console.error("Event relation info doesn't match this container");
return;
}
// If the event is in the process of being sent, listen for cancellation
// so we can remove the event from the collection.
if (event.isSending()) {
event.on("Event.status", this._onEventStatus);
}
this._relations.add(event);
if (this.relationType === "m.annotation") {
this._addAnnotationToAggregation(event);
} else if (this.relationType === "m.replace" && this._targetEvent) {
this._targetEvent.makeReplaced(this.getLastReplacement());
}
event.on("Event.beforeRedaction", this._onBeforeRedaction);
this.emit("Relations.add", event);
}
/**
* Remove relation event from this collection.
*
* @param {MatrixEvent} event
* The relation event to remove.
*/
_removeEvent(event) {
if (!this._relations.has(event)) {
return;
}
const relation = event.getRelation();
if (!relation) {
console.error("Event must have relation info");
return;
}
const relationType = relation.rel_type;
const eventType = event.getType();
if (this.relationType !== relationType || this.eventType !== eventType) {
console.error("Event relation info doesn't match this container");
return;
}
this._relations.delete(event);
if (this.relationType === "m.annotation") {
this._removeAnnotationFromAggregation(event);
} else if (this.relationType === "m.replace" && this._targetEvent) {
this._targetEvent.makeReplaced(this.getLastReplacement());
}
this.emit("Relations.remove", event);
}
/**
* Listens for event status changes to remove cancelled events.
*
* @param {MatrixEvent} event The event whose status has changed
* @param {EventStatus} status The new status
*/
_onEventStatus = (event, status) => {
if (!event.isSending()) {
// Sending is done, so we don't need to listen anymore
event.removeListener("Event.status", this._onEventStatus);
return;
}
if (status !== EventStatus.CANCELLED) {
return;
}
// Event was cancelled, remove from the collection
event.removeListener("Event.status", this._onEventStatus);
this._removeEvent(event);
}
/**
* Get all relation events in this collection.
*
* These are currently in the order of insertion to this collection, which
* won't match timeline order in the case of scrollback.
* TODO: Tweak `addEvent` to insert correctly for scrollback.
*
* @return {Array}
* Relation events in insertion order.
*/
getRelations() {
return [...this._relations];
}
_addAnnotationToAggregation(event) {
const { key } = event.getRelation();
if (!key) {
return;
}
let eventsForKey = this._annotationsByKey[key];
if (!eventsForKey) {
eventsForKey = this._annotationsByKey[key] = new Set();
this._sortedAnnotationsByKey.push([key, eventsForKey]);
}
// Add the new event to the set for this key
eventsForKey.add(event);
// Re-sort the [key, events] pairs in descending order of event count
this._sortedAnnotationsByKey.sort((a, b) => {
const aEvents = a[1];
const bEvents = b[1];
return bEvents.size - aEvents.size;
});
const sender = event.getSender();
let eventsFromSender = this._annotationsBySender[sender];
if (!eventsFromSender) {
eventsFromSender = this._annotationsBySender[sender] = new Set();
}
// Add the new event to the set for this sender
eventsFromSender.add(event);
}
_removeAnnotationFromAggregation(event) {
const { key } = event.getRelation();
if (!key) {
return;
}
const eventsForKey = this._annotationsByKey[key];
if (eventsForKey) {
eventsForKey.delete(event);
// Re-sort the [key, events] pairs in descending order of event count
this._sortedAnnotationsByKey.sort((a, b) => {
const aEvents = a[1];
const bEvents = b[1];
return bEvents.size - aEvents.size;
});
}
const sender = event.getSender();
const eventsFromSender = this._annotationsBySender[sender];
if (eventsFromSender) {
eventsFromSender.delete(event);
}
}
/**
* For relations that have been redacted, we want to remove them from
* aggregation data sets and emit an update event.
*
* To do so, we listen for `Event.beforeRedaction`, which happens:
* - after the server accepted the redaction and remote echoed back to us
* - before the original event has been marked redacted in the client
*
* @param {MatrixEvent} redactedEvent
* The original relation event that is about to be redacted.
*/
_onBeforeRedaction = (redactedEvent) => {
if (!this._relations.has(redactedEvent)) {
return;
}
this._relations.delete(redactedEvent);
if (this.relationType === "m.annotation") {
// Remove the redacted annotation from aggregation by key
this._removeAnnotationFromAggregation(redactedEvent);
} else if (this.relationType === "m.replace" && this._targetEvent) {
this._targetEvent.makeReplaced(this.getLastReplacement());
}
redactedEvent.removeListener("Event.beforeRedaction", this._onBeforeRedaction);
this.emit("Relations.redaction");
}
/**
* Get all events in this collection grouped by key and sorted by descending
* event count in each group.
*
* This is currently only supported for the annotation relation type.
*
* @return {Array}
* An array of [key, events] pairs sorted by descending event count.
* The events are stored in a Set (which preserves insertion order).
*/
getSortedAnnotationsByKey() {
if (this.relationType !== "m.annotation") {
// Other relation types are not grouped currently.
return null;
}
return this._sortedAnnotationsByKey;
}
/**
* Get all events in this collection grouped by sender.
*
* This is currently only supported for the annotation relation type.
*
* @return {Object}
* An object with each relation sender as a key and the matching Set of
* events for that sender as a value.
*/
getAnnotationsBySender() {
if (this.relationType !== "m.annotation") {
// Other relation types are not grouped currently.
return null;
}
return this._annotationsBySender;
}
/**
* Returns the most recent (and allowed) m.replace relation, if any.
*
* This is currently only supported for the m.replace relation type,
* once the target event is known, see `addEvent`.
*
* @return {MatrixEvent?}
*/
getLastReplacement() {
if (this.relationType !== "m.replace") {
// Aggregating on last only makes sense for this relation type
return null;
}
if (!this._targetEvent) {
// Don't know which replacements to accept yet.
// This method shouldn't be called before the original
// event is known anyway.
return null;
}
// the all-knowning server tells us that the event at some point had
// this timestamp for its replacement, so any following replacement should definitely not be less
const replaceRelation =
this._targetEvent.getServerAggregatedRelation("m.replace");
const minTs = replaceRelation && replaceRelation.origin_server_ts;
return this.getRelations().reduce((last, event) => {
if (event.getSender() !== this._targetEvent.getSender()) {
return last;
}
if (minTs && minTs > event.getTs()) {
return last;
}
if (last && last.getTs() > event.getTs()) {
return last;
}
return event;
}, null);
}
/*
* @param {MatrixEvent} targetEvent the event the relations are related to.
*/
setTargetEvent(event) {
if (this._targetEvent) {
return;
}
this._targetEvent = event;
if (this.relationType === "m.replace") {
const replacement = this.getLastReplacement();
// this is the initial update, so only call it if we already have something
// to not emit Event.replaced needlessly
if (replacement) {
this._targetEvent.makeReplaced(replacement);
}
}
}
}
+1 -1
View File
@@ -249,7 +249,7 @@ RoomMember.prototype.getDMInviter = function() {
* "crop" or "scale".
* @param {Boolean} allowDefault (optional) Passing false causes this method to
* return null if the user has no avatar image. Otherwise, a default image URL
* will be returned. Default: true. (Deprecated)
* will be returned. Default: true.
* @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be
* returned even if it is a direct hyperlink rather than a matrix content URL.
* If false, any non-matrix content URLs will be ignored. Setting this option to
+3 -4
View File
@@ -21,7 +21,6 @@ const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const RoomMember = require("./room-member");
import logger from '../../src/logger';
// possible statuses for out-of-band member loading
const OOB_STATUS_NOTSTARTED = 1;
@@ -448,7 +447,7 @@ RoomState.prototype.clearOutOfBandMembers = function() {
delete this.members[userId];
}
});
logger.log(`LL: RoomState removed ${count} members...`);
console.log(`LL: RoomState removed ${count} members...`);
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
};
@@ -457,11 +456,11 @@ RoomState.prototype.clearOutOfBandMembers = function() {
* @param {MatrixEvent[]} stateEvents array of membership state events
*/
RoomState.prototype.setOutOfBandMembers = function(stateEvents) {
logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
console.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
return;
}
logger.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`);
console.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`);
this._oobMemberFlags.status = OOB_STATUS_FINISHED;
stateEvents.forEach((e) => this._setOutOfBandMember(e));
};
+58 -332
View File
@@ -1,7 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -30,17 +29,9 @@ const ContentRepo = require("../content-repo");
const EventTimeline = require("./event-timeline");
const EventTimelineSet = require("./event-timeline-set");
import logger from '../../src/logger';
import ReEmitter from '../ReEmitter';
// 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
// the same as the common default room version whereas SAFE_ROOM_VERSIONS are the
// room versions which are considered okay for people to run without being asked
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
// return an m.room_versions capability.
const KNOWN_SAFE_ROOM_VERSION = '4';
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4'];
const LATEST_ROOM_VERSION = '1';
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
@@ -94,12 +85,9 @@ function synthesizeReceipt(userId, event, receiptType) {
* "<b>detached</b>", pending messages will appear in a separate list,
* accessbile via {@link module:models/room#getPendingEvents}. Default:
* "chronological".
*
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
* timeline support.
* @param {boolean} [opts.unstableClientRelationAggregation = false]
* Optional. Set to true to enable client-side aggregation of event relations
* via `EventTimelineSet#getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*
* @prop {string} roomId The ID of this room.
* @prop {string} name The human-readable display name for this room.
@@ -211,7 +199,7 @@ utils.inherits(Room, EventEmitter);
Room.prototype.getVersion = function() {
const createEvent = this.currentState.getStateEvents("m.room.create", "");
if (!createEvent) {
logger.warn("Room " + this.room_id + " does not have an m.room.create event");
console.warn("Room " + this.room_id + " does not have an m.room.create event");
return '1';
}
const ver = createEvent.getContent()['room_version'];
@@ -223,108 +211,12 @@ Room.prototype.getVersion = function() {
* Determines whether this room needs to be upgraded to a new version
* @returns {string?} What version the room should be upgraded to, or null if
* the room does not require upgrading at this time.
* @deprecated Use #getRecommendedVersion() instead
*/
Room.prototype.shouldUpgradeToVersion = function() {
// TODO: Remove this function.
// This makes assumptions about which versions are safe, and can easily
// be wrong. Instead, people are encouraged to use getRecommendedVersion
// which determines a safer value. This function doesn't use that function
// because this is not async-capable, and to avoid breaking the contract
// we're deprecating this.
if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) {
return KNOWN_SAFE_ROOM_VERSION;
}
return null;
};
/**
* Determines the recommended room version for the room. This returns an
* object with 3 properties: <code>version</code> as the new version the
* room should be upgraded to (may be the same as the current version);
* <code>needsUpgrade</code> to indicate if the room actually can be
* upgraded (ie: does the current version not match?); and <code>urgent</code>
* to indicate if the new version patches a vulnerability in a previous
* version.
* @returns {Promise<{version: string, needsUpgrade: bool, urgent: bool}>}
* Resolves to the version the room should be upgraded to.
*/
Room.prototype.getRecommendedVersion = async function() {
const capabilities = await this._client.getCapabilities();
let versionCap = capabilities["m.room_versions"];
if (!versionCap) {
versionCap = {
default: KNOWN_SAFE_ROOM_VERSION,
available: {},
};
for (const safeVer of SAFE_ROOM_VERSIONS) {
versionCap.available[safeVer] = "stable";
}
}
let result = this._checkVersionAgainstCapability(versionCap);
if (result.urgent && result.needsUpgrade) {
// Something doesn't feel right: we shouldn't need to update
// because the version we're on should be in the protocol's
// namespace. This usually means that the server was updated
// before the client was, making us think the newest possible
// room version is not stable. As a solution, we'll refresh
// the capability we're using to determine this.
logger.warn(
"Refreshing room version capability because the server looks " +
"to be supporting a newer room version we don't know about.",
);
const caps = await this._client.getCapabilities(true);
versionCap = caps["m.room_versions"];
if (!versionCap) {
logger.warn("No room version capability - assuming upgrade required.");
return result;
} else {
result = this._checkVersionAgainstCapability(versionCap);
}
}
return result;
};
Room.prototype._checkVersionAgainstCapability = function(versionCap) {
const currentVersion = this.getVersion();
logger.log(`[${this.roomId}] Current version: ${currentVersion}`);
logger.log(`[${this.roomId}] Version capability: `, versionCap);
const result = {
version: currentVersion,
needsUpgrade: false,
urgent: false,
};
// If the room is on the default version then nothing needs to change
if (currentVersion === versionCap.default) return result;
const stableVersions = Object.keys(versionCap.available)
.filter((v) => versionCap.available[v] === 'stable');
// Check if the room is on an unstable version. We determine urgency based
// off the version being in the Matrix spec namespace or not (if the version
// is in the current namespace and unstable, the room is probably vulnerable).
if (!stableVersions.includes(currentVersion)) {
result.version = versionCap.default;
result.needsUpgrade = true;
result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g);
if (result.urgent) {
logger.warn(`URGENT upgrade required on ${this.roomId}`);
} else {
logger.warn(`Non-urgent upgrade required on ${this.roomId}`);
}
return result;
}
// The room is on a stable, but non-default, version by this point.
// No upgrade needed.
return result;
// This almost certainly won't be the way this actually works - this
// is essentially a stub method.
if (this.getVersion() === LATEST_ROOM_VERSION) return null;
return LATEST_ROOM_VERSION;
};
/**
@@ -347,30 +239,13 @@ Room.prototype.userMayUpgradeRoom = function(userId) {
Room.prototype.getPendingEvents = function() {
if (this._opts.pendingEventOrdering !== "detached") {
throw new Error(
"Cannot call getPendingEvents with pendingEventOrdering == " +
"Cannot call getPendingEventList with pendingEventOrdering == " +
this._opts.pendingEventOrdering);
}
return this._pendingEventList;
};
/**
* Check whether the pending event list contains a given event by ID.
*
* @param {string} eventId The event ID to check for.
* @return {boolean}
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
Room.prototype.hasPendingEvent = function(eventId) {
if (this._opts.pendingEventOrdering !== "detached") {
throw new Error(
"Cannot call hasPendingEvent with pendingEventOrdering == " +
this._opts.pendingEventOrdering);
}
return this._pendingEventList.some(event => event.getId() === eventId);
};
/**
* Get the live unfiltered timeline for this room.
*
@@ -517,7 +392,7 @@ Room.prototype._loadMembers = async function() {
if (rawMembersEvents === null) {
fromServer = true;
rawMembersEvents = await this._loadMembersFromServer();
logger.log(`LL: got ${rawMembersEvents.length} ` +
console.log(`LL: got ${rawMembersEvents.length} ` +
`members from server for room ${this.roomId}`);
}
const memberEvents = rawMembersEvents.map(this._client.getEventMapper());
@@ -545,7 +420,7 @@ Room.prototype.loadMembersIfNeeded = function() {
const inMemoryUpdate = this._loadMembers().then((result) => {
this.currentState.setOutOfBandMembers(result.memberEvents);
// now the members are loaded, start to track the e2e devices if needed
if (this._client.isCryptoEnabled() && this._client.isRoomEncrypted(this.roomId)) {
if (this._client.isRoomEncrypted(this.roomId)) {
this._client._crypto.trackRoomDevices(this.roomId);
}
return result.fromServer;
@@ -561,21 +436,21 @@ Room.prototype.loadMembersIfNeeded = function() {
const oobMembers = this.currentState.getMembers()
.filter((m) => m.isOutOfBand())
.map((m) => m.events.member.event);
logger.log(`LL: telling store to write ${oobMembers.length}`
console.log(`LL: telling store to write ${oobMembers.length}`
+ ` members for room ${this.roomId}`);
const store = this._client.store;
return store.setOutOfBandMembers(this.roomId, oobMembers)
// swallow any IDB error as we don't want to fail
// because of this
.catch((err) => {
logger.log("LL: storing OOB room members failed, oh well",
console.log("LL: storing OOB room members failed, oh well",
err);
});
}
}).catch((err) => {
// as this is not awaited anywhere,
// at least show the error in the console
logger.error(err);
console.error(err);
});
this._membersPromise = inMemoryUpdate;
@@ -601,9 +476,9 @@ Room.prototype.clearLoadedMembersIfNeeded = async function() {
*/
Room.prototype._cleanupAfterLeaving = function() {
this.clearLoadedMembersIfNeeded().catch((err) => {
logger.error(`error after clearing loaded members from ` +
console.error(`error after clearing loaded members from ` +
`room ${this.roomId} after leaving`);
logger.log(err);
console.dir(err);
});
};
@@ -645,29 +520,6 @@ Room.prototype._fixUpLegacyTimelineFields = function() {
.getState(EventTimeline.FORWARDS);
};
/**
* Returns whether there are any devices in the room that are unverified
*
* Note: Callers should first check if crypto is enabled on this device. If it is
* disabled, then we aren't tracking room devices at all, so we can't answer this, and an
* error will be thrown.
*
* @return {bool} the result
*/
Room.prototype.hasUnverifiedDevices = async function() {
if (!this._client.isRoomEncrypted(this.roomId)) {
return false;
}
const e2eMembers = await this.getEncryptionTargetMembers();
for (const member of e2eMembers) {
const devices = await this._client.getStoredDevicesForUser(member.userId);
if (devices.some((device) => device.isUnverified())) {
return true;
}
}
return false;
};
/**
* Return the timeline sets for this room.
* @return {EventTimelineSet[]} array of timeline sets for this room
@@ -781,7 +633,7 @@ Room.prototype.getBlacklistUnverifiedDevices = function() {
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {boolean} allowDefault True to allow an identicon for this room if an
* avatar URL wasn't explicitly set. Default: true. (Deprecated)
* avatar URL wasn't explicitly set. Default: true.
* @return {?string} the avatar URL or null.
*/
Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
@@ -815,20 +667,20 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
* @return {array} The room's alias as an array of strings
*/
Room.prototype.getAliases = function() {
const aliasStrings = [];
const alias_strings = [];
const aliasEvents = this.currentState.getStateEvents("m.room.aliases");
if (aliasEvents) {
for (let i = 0; i < aliasEvents.length; ++i) {
const aliasEvent = aliasEvents[i];
if (utils.isArray(aliasEvent.getContent().aliases)) {
const alias_events = this.currentState.getStateEvents("m.room.aliases");
if (alias_events) {
for (let i = 0; i < alias_events.length; ++i) {
const alias_event = alias_events[i];
if (utils.isArray(alias_event.getContent().aliases)) {
Array.prototype.push.apply(
aliasStrings, aliasEvent.getContent().aliases,
alias_strings, alias_event.getContent().aliases,
);
}
}
}
return aliasStrings;
return alias_strings;
};
/**
@@ -1050,25 +902,14 @@ Room.prototype.removeFilteredTimelineSet = function(filter) {
* @private
*/
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
if (event.isRedaction()) {
let i;
if (event.getType() === "m.room.redaction") {
const redactId = event.event.redacts;
// if we know about this event, redact its contents now.
const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
if (redactedEvent) {
redactedEvent.makeRedacted(event);
// If this is in the current state, replace it with the redacted version
if (redactedEvent.getStateKey()) {
const currentStateEvent = this.currentState.getStateEvents(
redactedEvent.getType(),
redactedEvent.getStateKey(),
);
if (currentStateEvent.getId() === redactedEvent.getId()) {
this.currentState.setStateEvents([redactedEvent]);
}
}
this.emit("Room.redaction", event, this);
// TODO: we stash user displaynames (among other things) in
@@ -1095,7 +936,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
}
// add to our timeline sets
for (let i = 0; i < this._timelineSets.length; i++) {
for (i = 0; i < this._timelineSets.length; i++) {
this._timelineSets[i].addLiveEvent(event, duplicateStrategy);
}
@@ -1159,35 +1000,15 @@ Room.prototype.addPendingEvent = function(event, txnId) {
if (this._opts.pendingEventOrdering == "detached") {
if (this._pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
logger.warn("Setting event as NOT_SENT due to messages in the same state");
event.setStatus(EventStatus.NOT_SENT);
console.warn("Setting event as NOT_SENT due to messages in the same state");
event.status = EventStatus.NOT_SENT;
}
this._pendingEventList.push(event);
if (event.isRelation()) {
// For pending events, add them to the relations collection immediately.
// (The alternate case below already covers this as part of adding to
// the timeline set.)
this._aggregateNonLiveRelation(event);
}
if (event.isRedaction()) {
const redactId = event.event.redacts;
let redactedEvent = this._pendingEventList &&
this._pendingEventList.find(e => e.getId() === redactId);
if (!redactedEvent) {
redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
}
if (redactedEvent) {
redactedEvent.markLocallyRedacted(event);
this.emit("Room.redaction", event, this);
}
}
} else {
for (let i = 0; i < this._timelineSets.length; i++) {
const timelineSet = this._timelineSets[i];
if (timelineSet.getFilter()) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
if (this._filter.filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
@@ -1200,30 +1021,6 @@ Room.prototype.addPendingEvent = function(event, txnId) {
this.emit("Room.localEchoUpdated", event, this, null, null);
};
/**
* Used to aggregate the local echo for a relation, and also
* for re-applying a relation after it's redaction has been cancelled,
* as the local echo for the redaction of the relation would have
* un-aggregated the relation. Note that this is different from regular messages,
* which are just kept detached for their local echo.
*
* Also note that live events are aggregated in the live EventTimelineSet.
* @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated.
*/
Room.prototype._aggregateNonLiveRelation = function(event) {
// TODO: We should consider whether this means it would be a better
// design to lift the relations handling up to the room instead.
for (let i = 0; i < this._timelineSets.length; i++) {
const timelineSet = this._timelineSets[i];
if (timelineSet.getFilter()) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.aggregateRelations(event);
}
} else {
timelineSet.aggregateRelations(event);
}
}
};
/**
* Deal with the echo of a message we sent.
@@ -1245,7 +1042,7 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
const oldStatus = localEvent.status;
// no longer pending
delete this._txnToEvent[remoteEvent.getUnsigned().transaction_id];
delete this._txnToEvent[remoteEvent.transaction_id];
// if it's in the pending list, remove it
if (this._pendingEventList) {
@@ -1313,7 +1110,7 @@ ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
*/
Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`);
console.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`);
// if the message was sent, we expect an event id
if (newStatus == EventStatus.SENT && !newEventId) {
@@ -1345,11 +1142,11 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
newStatus);
}
event.setStatus(newStatus);
event.status = newStatus;
if (newStatus == EventStatus.SENT) {
// update the event id
event.replaceLocalEventId(newEventId);
event.event.event_id = newEventId;
// if the event was already in the timeline (which will be the case if
// opts.pendingEventOrdering==chronological), we need to update the
@@ -1360,13 +1157,12 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
} else if (newStatus == EventStatus.CANCELLED) {
// remove it from the pending event list, or the timeline.
if (this._pendingEventList) {
const idx = this._pendingEventList.findIndex(ev => ev.getId() === oldEventId);
if (idx !== -1) {
const [removedEvent] = this._pendingEventList.splice(idx, 1);
if (removedEvent.isRedaction()) {
this._revertRedactionLocalEcho(removedEvent);
}
}
utils.removeElement(
this._pendingEventList,
function(ev) {
return ev.getId() == oldEventId;
}, false,
);
}
this.removeEvent(oldEventId);
}
@@ -1374,23 +1170,6 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
};
Room.prototype._revertRedactionLocalEcho = function(redactionEvent) {
const redactId = redactionEvent.event.redacts;
if (!redactId) {
return;
}
const redactedEvent = this.getUnfilteredTimelineSet()
.findEventById(redactId);
if (redactedEvent) {
redactedEvent.unmarkLocallyRedacted();
// re-render after undoing redaction
this.emit("Room.redactionCancelled", redactionEvent, this);
// reapply relation now redaction failed
if (redactedEvent.isRelation()) {
this._aggregateNonLiveRelation(redactedEvent);
}
}
};
/**
* Add some events to this room. This can include state events, message
@@ -1432,33 +1211,28 @@ Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
}
for (i = 0; i < events.length; i++) {
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this._addLiveEvent(events[i], duplicateStrategy);
}
};
/**
* Adds/handles ephemeral events such as typing notifications and read receipts.
* @param {MatrixEvent[]} events A list of events to process
*/
Room.prototype.addEphemeralEvents = function(events) {
for (const event of events) {
if (event.getType() === 'm.typing') {
this.currentState.setTypingEvent(event);
} else if (event.getType() === 'm.receipt') {
this.addReceipt(event);
} // else ignore - life is too short for us to care about these events
if (events[i].getType() === "m.typing") {
this.currentState.setTypingEvent(events[i]);
} else if (events[i].getType() === "m.receipt") {
this.addReceipt(events[i]);
}
// N.B. account_data is added directly by /sync to avoid
// having to maintain an event.isAccountData() here
else {
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this._addLiveEvent(events[i], duplicateStrategy);
}
}
};
/**
* Removes events from this room.
* @param {String[]} eventIds A list of eventIds to remove.
* @param {String[]} event_ids A list of event_ids to remove.
*/
Room.prototype.removeEvents = function(eventIds) {
for (let i = 0; i < eventIds.length; ++i) {
this.removeEvent(eventIds[i]);
Room.prototype.removeEvents = function(event_ids) {
for (let i = 0; i < event_ids.length; ++i) {
this.removeEvent(event_ids[i]);
}
};
@@ -1474,9 +1248,6 @@ Room.prototype.removeEvent = function(eventId) {
for (let i = 0; i < this._timelineSets.length; i++) {
const removed = this._timelineSets[i].removeEvent(eventId);
if (removed) {
if (removed.isRedaction()) {
this._revertRedactionLocalEcho(removed);
}
removedAny = true;
}
}
@@ -1566,40 +1337,6 @@ Room.prototype.getEventReadUpTo = function(userId, ignoreSynthesized) {
return receipts["m.read"][userId].eventId;
};
/**
* Determines if the given user has read a particular event ID with the known
* history of the room. This is not a definitive check as it relies only on
* what is available to the room at the time of execution.
* @param {String} userId The user ID to check the read state of.
* @param {String} eventId The event ID to check if the user read.
* @returns {Boolean} True if the user has read the event, false otherwise.
*/
Room.prototype.hasUserReadEvent = function(userId, eventId) {
const readUpToId = this.getEventReadUpTo(userId, false);
if (readUpToId === eventId) return true;
if (this.timeline.length
&& this.timeline[this.timeline.length - 1].getSender()
&& this.timeline[this.timeline.length - 1].getSender() === userId) {
// It doesn't matter where the event is in the timeline, the user has read
// it because they've sent the latest event.
return true;
}
for (let i = this.timeline.length - 1; i >= 0; --i) {
const ev = this.timeline[i];
// If we encounter the target event first, the user hasn't read it
// however if we encounter the readUpToId first then the user has read
// it. These rules apply because we're iterating bottom-up.
if (ev.getId() === eventId) return false;
if (ev.getId() === readUpToId) return true;
}
// We don't know if the user has read it, so assume not.
return false;
};
/**
* Get a list of receipts for the given event.
* @param {MatrixEvent} event the event to get receipts for
@@ -1900,21 +1637,10 @@ module.exports = Room;
* event).
*
* @event module:client~MatrixClient#"Room.redaction"
* @param {MatrixEvent} event The matrix redaction event
* @param {MatrixEvent} event The matrix event which was redacted
* @param {Room} room The room containing the redacted event
*/
/**
* Fires when an event that was previously redacted isn't anymore.
* This happens when the redaction couldn't be sent and
* was subsequently cancelled by the user. Redactions have a local echo
* which is undone in this scenario.
*
* @event module:client~MatrixClient#"Room.redactionCancelled"
* @param {MatrixEvent} event The matrix redaction event that was cancelled.
* @param {Room} room The room containing the unredacted event
*/
/**
* Fires whenever the name of a room is updated.
* @event module:client~MatrixClient#"Room.name"
-2
View File
@@ -186,13 +186,11 @@ User.prototype.getLastActiveTs = function() {
/**
* Manually set the user's status message.
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
* @fires module:client~MatrixClient#event:"User._unstable_statusMessage"
*/
User.prototype._unstable_updateStatusMessage = function(event) {
if (!event.getContent()) this._unstable_statusMessage = "";
else this._unstable_statusMessage = event.getContent()["status"];
this._updateModifiedTime();
this.emit("User._unstable_statusMessage", this);
};
/**
+2 -117
View File
@@ -23,58 +23,6 @@ import {escapeRegExp, globToRegexp} from "./utils";
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
// The default override rules to apply when calculating actions for an event. These
// defaults apply under no other circumstances to avoid confusing the client with server
// state. We do this for two reasons:
// 1. Synapse is unlikely to send us the push rule in an incremental sync - see
// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for
// more details.
// 2. We often want to start using push rules ahead of the server supporting them,
// and so we can put them here.
const DEFAULT_OVERRIDE_RULES = [
{
// For homeservers which don't support MSC1930 yet
rule_id: ".m.rule.tombstone",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.room.tombstone",
},
{
kind: "event_match",
key: "state_key",
pattern: "",
},
],
actions: [
"notify",
{
set_tweak: "highlight",
value: true,
},
],
},
{
// For homeservers which don't support MSC2153 yet
rule_id: ".m.rule.reaction",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.reaction",
},
],
actions: [
"dont_notify",
],
},
];
/**
* Construct a Push Processor.
* @constructor
@@ -236,10 +184,7 @@ function PushProcessor(client) {
};
const eventFulfillsDisplayNameCondition = function(cond, ev) {
let content = ev.getContent();
if (ev.isEncrypted() && ev.getClearContent()) {
content = ev.getClearContent();
}
const content = ev.getContent();
if (!content || !content.body || typeof content.body != 'string') {
return false;
}
@@ -364,33 +309,6 @@ function PushProcessor(client) {
return actionObj;
};
const applyRuleDefaults = function(clientRuleset) {
// Deep clone the object before we mutate it
const ruleset = JSON.parse(JSON.stringify(clientRuleset));
if (!clientRuleset['global']) {
clientRuleset['global'] = {};
}
if (!clientRuleset['global']['override']) {
clientRuleset['global']['override'] = [];
}
// Apply default overrides
const globalOverrides = clientRuleset['global']['override'];
for (const override of DEFAULT_OVERRIDE_RULES) {
const existingRule = globalOverrides
.find((r) => r.rule_id === override.rule_id);
if (!existingRule) {
const ruleId = override.rule_id;
console.warn(`Adding default global override for ${ruleId}`);
globalOverrides.push(override);
}
}
return ruleset;
};
this.ruleMatchesEvent = function(rule, ev) {
let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) {
@@ -410,8 +328,7 @@ function PushProcessor(client) {
* @return {PushAction}
*/
this.actionsForEvent = function(ev) {
const rules = applyRuleDefaults(client.pushRules);
return pushActionsForEventAndRulesets(ev, rules);
return pushActionsForEventAndRulesets(ev, client.pushRules);
};
/**
@@ -460,38 +377,6 @@ PushProcessor.actionListToActionsObject = function(actionlist) {
return actionobj;
};
/**
* Rewrites conditions on a client's push rules to match the defaults
* where applicable. Useful for upgrading push rules to more strict
* conditions when the server is falling behind on defaults.
* @param {object} incomingRules The client's existing push rules
* @returns {object} The rewritten rules
*/
PushProcessor.rewriteDefaultRules = function(incomingRules) {
let newRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
// These lines are mostly to make the tests happy. We shouldn't run into these
// properties missing in practice.
if (!newRules) newRules = {};
if (!newRules.global) newRules.global = {};
if (!newRules.global.override) newRules.global.override = [];
// Fix default override rules
newRules.global.override = newRules.global.override.map(r => {
const defaultRule = DEFAULT_OVERRIDE_RULES.find(d => d.rule_id === r.rule_id);
if (!defaultRule) return r;
// Copy over the actions, default, and conditions. Don't touch the user's
// preference.
r.default = defaultRule.default;
r.conditions = defaultRule.conditions;
r.actions = defaultRule.actions;
return r;
});
return newRules;
};
/**
* @typedef {Object} PushAction
* @type {Object}
+2 -3
View File
@@ -24,7 +24,6 @@ limitations under the License.
*/
"use strict";
import logger from '../src/logger';
// we schedule a callback at least this often, to check if we've missed out on
// some wall-clock time due to being suspended.
@@ -40,7 +39,7 @@ let _realCallbackKey;
// each is an object with keys [runAt, func, params, key].
const _callbackList = [];
// var debuglog = logger.log.bind(logger);
// var debuglog = console.log.bind(console);
const debuglog = function() {};
/**
@@ -171,7 +170,7 @@ function _runCallbacks() {
try {
cb.func.apply(global, cb.params);
} catch (e) {
logger.error("Uncaught exception in callback function",
console.error("Uncaught exception in callback function",
e.stack || e);
}
}
+3 -12
View File
@@ -21,7 +21,6 @@ limitations under the License.
*/
const utils = require("./utils");
import Promise from 'bluebird';
import logger from '../src/logger';
const DEBUG = false; // set true to enable console logging.
@@ -177,8 +176,7 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
* @see module:scheduler~queueAlgorithm
*/
MatrixScheduler.QUEUE_MESSAGES = function(event) {
// enqueue messages or events that associate with another event (redactions and relations)
if (event.getType() === "m.room.message" || event.hasAssocation()) {
if (event.getType() === "m.room.message") {
// put these events in the 'message' queue.
return "message";
}
@@ -221,14 +219,7 @@ function _processQueue(scheduler, queueName) {
);
// fire the process function and if it resolves, resolve the deferred. Else
// invoke the retry algorithm.
// First wait for a resolved promise, so the resolve handlers for
// the deferred of the previously sent event can run.
// This way enqueued relations/redactions to enqueued events can receive
// the remove id of their target before being sent.
Promise.resolve().then(() => {
return scheduler._procFn(obj.event);
}).then(function(res) {
scheduler._procFn(obj.event).done(function(res) {
// remove this from the queue
_removeNextEvent(scheduler, queueName);
debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
@@ -278,7 +269,7 @@ function _removeNextEvent(scheduler, queueName) {
function debuglog() {
if (DEBUG) {
logger.log(...arguments);
console.log(...arguments);
}
}
-20
View File
@@ -1,20 +0,0 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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.
*/
export const SERVICE_TYPES = Object.freeze({
IS: 'SERVICE_TYPE_IS', // An Identity Service
IM: 'SERVICE_TYPE_IM', // An Integration Manager
});
+21 -27
View File
@@ -18,8 +18,6 @@ limitations under the License.
import Promise from 'bluebird';
import SyncAccumulator from "../sync-accumulator";
import utils from "../utils";
import * as IndexedDBHelpers from "../indexeddb-helpers";
import logger from '../../src/logger';
const VERSION = 3;
@@ -134,10 +132,6 @@ const LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend(
this._isNewlyCreated = false;
};
LocalIndexedDBStoreBackend.exists = function(indexedDB, dbName) {
dbName = "matrix-js-sdk:" + (dbName || "default");
return IndexedDBHelpers.exists(indexedDB, dbName);
};
LocalIndexedDBStoreBackend.prototype = {
/**
@@ -147,7 +141,7 @@ LocalIndexedDBStoreBackend.prototype = {
*/
connect: function() {
if (!this._disconnected) {
logger.log(
console.log(
`LocalIndexedDBStoreBackend.connect: already connected or connecting`,
);
return Promise.resolve();
@@ -155,14 +149,14 @@ LocalIndexedDBStoreBackend.prototype = {
this._disconnected = false;
logger.log(
console.log(
`LocalIndexedDBStoreBackend.connect: connecting...`,
);
const req = this.indexedDB.open(this._dbName, VERSION);
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
const oldVersion = ev.oldVersion;
logger.log(
console.log(
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
);
if (oldVersion < 1) { // The database did not previously exist.
@@ -179,16 +173,16 @@ LocalIndexedDBStoreBackend.prototype = {
};
req.onblocked = () => {
logger.log(
console.log(
`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`,
);
};
logger.log(
console.log(
`LocalIndexedDBStoreBackend.connect: awaiting connection...`,
);
return reqAsEventPromise(req).then((ev) => {
logger.log(
console.log(
`LocalIndexedDBStoreBackend.connect: connected`,
);
this.db = ev.target.result;
@@ -216,7 +210,7 @@ LocalIndexedDBStoreBackend.prototype = {
this._loadAccountData(),
this._loadSyncData(),
]).then(([accountData, syncData]) => {
logger.log(
console.log(
`LocalIndexedDBStoreBackend: loaded initial data`,
);
this._syncAccumulator.accumulate({
@@ -274,7 +268,7 @@ LocalIndexedDBStoreBackend.prototype = {
reject(err);
};
}).then((events) => {
logger.log(`LL: got ${events && events.length}` +
console.log(`LL: got ${events && events.length}` +
` membershipEvents from storage for room ${roomId} ...`);
return events;
});
@@ -288,7 +282,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @param {event[]} membershipEvents the membership events to store
*/
setOutOfBandMembers: async function(roomId, membershipEvents) {
logger.log(`LL: backend about to store ${membershipEvents.length}` +
console.log(`LL: backend about to store ${membershipEvents.length}` +
` members for ${roomId}`);
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
const store = tx.objectStore("oob_membership_events");
@@ -307,7 +301,7 @@ LocalIndexedDBStoreBackend.prototype = {
};
store.put(markerObject);
await txnAsPromise(tx);
logger.log(`LL: backend done storing for ${roomId}!`);
console.log(`LL: backend done storing for ${roomId}!`);
},
clearOutOfBandMembers: async function(roomId) {
@@ -342,7 +336,7 @@ LocalIndexedDBStoreBackend.prototype = {
[roomId, maxStateKey],
);
logger.log(`LL: Deleting all users + marker in storage for ` +
console.log(`LL: Deleting all users + marker in storage for ` +
`room ${roomId}, with key range:`,
[roomId, minStateKey], [roomId, maxStateKey]);
await reqAsPromise(writeStore.delete(membersKeyRange));
@@ -355,11 +349,11 @@ LocalIndexedDBStoreBackend.prototype = {
*/
clearDatabase: function() {
return new Promise((resolve, reject) => {
logger.log(`Removing indexeddb instance: ${this._dbName}`);
console.log(`Removing indexeddb instance: ${this._dbName}`);
const req = this.indexedDB.deleteDatabase(this._dbName);
req.onblocked = () => {
logger.log(
console.log(
`can't yet delete indexeddb ${this._dbName}` +
` because it is open elsewhere`,
);
@@ -369,14 +363,14 @@ LocalIndexedDBStoreBackend.prototype = {
// in firefox, with indexedDB disabled, this fails with a
// DOMError. We treat this as non-fatal, so that we can still
// use the app.
logger.warn(
console.warn(
`unable to delete js-sdk store indexeddb: ${ev.target.error}`,
);
resolve();
};
req.onsuccess = () => {
logger.log(`Removed indexeddb instance: ${this._dbName}`);
console.log(`Removed indexeddb instance: ${this._dbName}`);
resolve();
};
});
@@ -435,7 +429,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise} Resolves if the data was persisted.
*/
_persistSyncData: function(nextBatch, roomsData, groupsData) {
logger.log("Persisting sync data up to ", nextBatch);
console.log("Persisting sync data up to ", nextBatch);
return Promise.try(() => {
const txn = this.db.transaction(["sync"], "readwrite");
const store = txn.objectStore("sync");
@@ -509,7 +503,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise<Object[]>} A list of raw global account events.
*/
_loadAccountData: function() {
logger.log(
console.log(
`LocalIndexedDBStoreBackend: loading account data...`,
);
return Promise.try(() => {
@@ -518,7 +512,7 @@ LocalIndexedDBStoreBackend.prototype = {
return selectQuery(store, undefined, (cursor) => {
return cursor.value;
}).then((result) => {
logger.log(
console.log(
`LocalIndexedDBStoreBackend: loaded account data`,
);
return result;
@@ -531,7 +525,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
*/
_loadSyncData: function() {
logger.log(
console.log(
`LocalIndexedDBStoreBackend: loading sync data...`,
);
return Promise.try(() => {
@@ -540,11 +534,11 @@ LocalIndexedDBStoreBackend.prototype = {
return selectQuery(store, undefined, (cursor) => {
return cursor.value;
}).then((results) => {
logger.log(
console.log(
`LocalIndexedDBStoreBackend: loaded sync data`,
);
if (results.length > 1) {
logger.warn("loadSyncData: More than 1 sync row found.");
console.warn("loadSyncData: More than 1 sync row found.");
}
return (results.length > 0 ? results[0] : {});
});
+4 -5
View File
@@ -16,7 +16,6 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../src/logger';
/**
* An IndexedDB store backend where the actual backend sits in a web
@@ -141,7 +140,7 @@ RemoteIndexedDBStoreBackend.prototype = {
// tell the worker the db name.
this._startPromise = this._doCmd('_setupWorker', [this._dbName]).then(() => {
logger.log("IndexedDB worker is ready");
console.log("IndexedDB worker is ready");
});
}
return this._startPromise;
@@ -171,13 +170,13 @@ RemoteIndexedDBStoreBackend.prototype = {
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
if (msg.seq === undefined) {
logger.error("Got reply from worker with no seq");
console.error("Got reply from worker with no seq");
return;
}
const def = this._inFlight[msg.seq];
if (def === undefined) {
logger.error("Got reply for unknown seq " + msg.seq);
console.error("Got reply for unknown seq " + msg.seq);
return;
}
delete this._inFlight[msg.seq];
@@ -190,7 +189,7 @@ RemoteIndexedDBStoreBackend.prototype = {
def.reject(error);
}
} else {
logger.warn("Unrecognised message from worker: " + msg);
console.warn("Unrecognised message from worker: " + msg);
}
},
};
+2 -3
View File
@@ -17,7 +17,6 @@ limitations under the License.
import Promise from 'bluebird';
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
import logger from '../../src/logger';
/**
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
@@ -130,8 +129,8 @@ class IndexedDBStoreWorker {
result: ret,
});
}, (err) => {
logger.error("Error running command: "+msg.command);
logger.error(err);
console.error("Error running command: "+msg.command);
console.error(err);
this.postMessage.call(null, {
command: 'cmd_fail',
seq: msg.seq,
+40 -100
View File
@@ -15,17 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable babel/no-invalid-this */
import Promise from 'bluebird';
import {MemoryStore} from "./memory";
import {MatrixInMemoryStore} from "./memory";
import utils from "../utils";
import {EventEmitter} from 'events';
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
import RemoteIndexedDBStoreBackend from "./indexeddb-remote-backend.js";
import User from "../models/user";
import {MatrixEvent} from "../models/event";
import logger from '../../src/logger';
/**
* This is an internal module. See {@link IndexedDBStore} for the public class.
@@ -41,9 +37,9 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
/**
* Construct a new Indexed Database store, which extends MemoryStore.
* Construct a new Indexed Database store, which extends MatrixInMemoryStore.
*
* This store functions like a MemoryStore except it periodically persists
* This store functions like a MatrixInMemoryStore except it periodically persists
* the contents of the store to an IndexedDB backend.
*
* All data is still kept in-memory but can be loaded from disk by calling
@@ -66,7 +62,7 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
* </pre>
*
* @constructor
* @extends MemoryStore
* @extends MatrixInMemoryStore
* @param {Object} opts Options object.
* @param {Object} opts.indexedDB The Indexed DB interface e.g.
* <code>window.indexedDB</code>
@@ -83,7 +79,7 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
* database.
*/
const IndexedDBStore = function IndexedDBStore(opts) {
MemoryStore.call(this, opts);
MatrixInMemoryStore.call(this, opts);
if (!opts.indexedDB) {
throw new Error('Missing required option: indexedDB');
@@ -113,28 +109,23 @@ const IndexedDBStore = function IndexedDBStore(opts) {
// user_id : timestamp
};
};
utils.inherits(IndexedDBStore, MemoryStore);
utils.extend(IndexedDBStore.prototype, EventEmitter.prototype);
IndexedDBStore.exists = function(indexedDB, dbName) {
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
};
utils.inherits(IndexedDBStore, MatrixInMemoryStore);
/**
* @return {Promise} Resolved when loaded from indexed db.
*/
IndexedDBStore.prototype.startup = function() {
if (this.startedUp) {
logger.log(`IndexedDBStore.startup: already started`);
console.log(`IndexedDBStore.startup: already started`);
return Promise.resolve();
}
logger.log(`IndexedDBStore.startup: connecting to backend`);
console.log(`IndexedDBStore.startup: connecting to backend`);
return this.backend.connect().then(() => {
logger.log(`IndexedDBStore.startup: loading presence events`);
console.log(`IndexedDBStore.startup: loading presence events`);
return this.backend.getUserPresenceEvents();
}).then((userPresenceEvents) => {
logger.log(`IndexedDBStore.startup: processing presence events`);
console.log(`IndexedDBStore.startup: processing presence events`);
userPresenceEvents.forEach(([userId, rawEvent]) => {
const u = new User(userId);
if (rawEvent) {
@@ -151,36 +142,36 @@ IndexedDBStore.prototype.startup = function() {
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
IndexedDBStore.prototype.getSavedSync = degradable(function() {
IndexedDBStore.prototype.getSavedSync = function() {
return this.backend.getSavedSync();
}, "getSavedSync");
};
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
IndexedDBStore.prototype.isNewlyCreated = degradable(function() {
IndexedDBStore.prototype.isNewlyCreated = function() {
return this.backend.isNewlyCreated();
}, "isNewlyCreated");
};
/**
* @return {Promise} If there is a saved sync, the nextBatch token
* for this sync, otherwise null.
*/
IndexedDBStore.prototype.getSavedSyncToken = degradable(function() {
IndexedDBStore.prototype.getSavedSyncToken = function() {
return this.backend.getNextBatchToken();
}, "getSavedSyncToken"),
},
/**
* Delete all data from this store.
* @return {Promise} Resolves if the data was deleted from the database.
*/
IndexedDBStore.prototype.deleteAllData = degradable(function() {
MemoryStore.prototype.deleteAllData.call(this);
IndexedDBStore.prototype.deleteAllData = function() {
MatrixInMemoryStore.prototype.deleteAllData.call(this);
return this.backend.clearDatabase().then(() => {
logger.log("Deleted indexeddb data.");
console.log("Deleted indexeddb data.");
}, (err) => {
logger.error(`Failed to delete indexeddb data: ${err}`);
console.error(`Failed to delete indexeddb data: ${err}`);
throw err;
});
});
};
/**
* Whether this store would like to save its data
@@ -198,19 +189,17 @@ IndexedDBStore.prototype.wantsSave = function() {
/**
* Possibly write data to the database.
*
* @param {bool} force True to force a save to happen
* @return {Promise} Promise resolves after the write completes
* (or immediately if no write is performed)
*/
IndexedDBStore.prototype.save = function(force) {
if (force || this.wantsSave()) {
IndexedDBStore.prototype.save = function() {
if (this.wantsSave()) {
return this._reallySave();
}
return Promise.resolve();
};
IndexedDBStore.prototype._reallySave = degradable(function() {
IndexedDBStore.prototype._reallySave = function() {
this._syncTs = Date.now(); // set now to guard against multi-writes
// work out changed users (this doesn't handle deletions but you
@@ -226,12 +215,14 @@ IndexedDBStore.prototype._reallySave = degradable(function() {
this._userModifiedMap[u.userId] = u.getLastModifiedTime();
}
return this.backend.syncToDatabase(userTuples);
});
return this.backend.syncToDatabase(userTuples).catch((err) => {
console.error("sync fail:", err);
});
};
IndexedDBStore.prototype.setSyncData = degradable(function(syncData) {
IndexedDBStore.prototype.setSyncData = function(syncData) {
return this.backend.setSyncData(syncData);
}, "setSyncData");
};
/**
* Returns the out-of-band membership events for this room that
@@ -240,9 +231,9 @@ IndexedDBStore.prototype.setSyncData = degradable(function(syncData) {
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
IndexedDBStore.prototype.getOutOfBandMembers = degradable(function(roomId) {
IndexedDBStore.prototype.getOutOfBandMembers = function(roomId) {
return this.backend.getOutOfBandMembers(roomId);
}, "getOutOfBandMembers");
};
/**
* Stores the out-of-band membership events for this room. Note that
@@ -252,71 +243,20 @@ IndexedDBStore.prototype.getOutOfBandMembers = degradable(function(roomId) {
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
IndexedDBStore.prototype.setOutOfBandMembers = degradable(function(
roomId,
membershipEvents,
) {
MemoryStore.prototype.setOutOfBandMembers.call(this, roomId, membershipEvents);
IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, membershipEvents) {
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
}, "setOutOfBandMembers");
};
IndexedDBStore.prototype.clearOutOfBandMembers = degradable(function(roomId) {
MemoryStore.prototype.clearOutOfBandMembers.call(this);
IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) {
return this.backend.clearOutOfBandMembers(roomId);
}, "clearOutOfBandMembers");
};
IndexedDBStore.prototype.getClientOptions = degradable(function() {
IndexedDBStore.prototype.getClientOptions = function() {
return this.backend.getClientOptions();
}, "getClientOptions");
};
IndexedDBStore.prototype.storeClientOptions = degradable(function(options) {
MemoryStore.prototype.storeClientOptions.call(this, options);
IndexedDBStore.prototype.storeClientOptions = function(options) {
return this.backend.storeClientOptions(options);
}, "storeClientOptions");
};
module.exports.IndexedDBStore = IndexedDBStore;
/**
* All member functions of `IndexedDBStore` that access the backend use this wrapper to
* watch for failures after initial store startup, including `QuotaExceededError` as
* free disk space changes, etc.
*
* When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore`
* in place so that the current operation and all future ones are in-memory only.
*
* @param {Function} func The degradable work to do.
* @param {String} fallback The method name for fallback.
* @returns {Function} A wrapped member function.
*/
function degradable(func, fallback) {
return async function(...args) {
try {
return await func.call(this, ...args);
} catch (e) {
logger.error("IndexedDBStore failure, degrading to MemoryStore", e);
this.emit("degraded", e);
try {
// We try to delete IndexedDB after degrading since this store is only a
// cache (the app will still function correctly without the data).
// It's possible that deleting repair IndexedDB for the next app load,
// potenially by making a little more space available.
logger.log("IndexedDBStore trying to delete degraded data");
await this.backend.clearDatabase();
logger.log("IndexedDBStore delete after degrading succeeeded");
} catch (e) {
logger.warn("IndexedDBStore delete after degrading failed", e);
}
// Degrade the store from being an instance of `IndexedDBStore` to instead be
// an instance of `MemoryStore` so that future API calls use the memory path
// directly and skip IndexedDB entirely. This should be safe as
// `IndexedDBStore` already extends from `MemoryStore`, so we are making the
// store become its parent type in a way. The mutator methods of
// `IndexedDBStore` also maintain the state that `MemoryStore` uses (many are
// not overridden at all).
Object.setPrototypeOf(this, MemoryStore.prototype);
if (fallback) {
return await MemoryStore.prototype[fallback].call(this, ...args);
}
}
};
}
+4 -13
View File
@@ -17,7 +17,7 @@ limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link MemoryStore} for the public class.
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
* @module store/memory
*/
const utils = require("../utils");
@@ -31,7 +31,7 @@ import Promise from 'bluebird';
* @param {LocalStorage} opts.localStorage The local storage instance to persist
* some forms of data such as tokens. Rooms will NOT be stored.
*/
module.exports.MemoryStore = function MemoryStore(opts) {
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
opts = opts || {};
this.rooms = {
// roomId: Room
@@ -58,7 +58,7 @@ module.exports.MemoryStore = function MemoryStore(opts) {
this._clientOptions = {};
};
module.exports.MemoryStore.prototype = {
module.exports.MatrixInMemoryStore.prototype = {
/**
* Retrieve the token to stream from.
@@ -335,10 +335,8 @@ module.exports.MemoryStore.prototype = {
/**
* Save does nothing as there is no backing data store.
* @param {bool} force True to force a save (but the memory
* store still can't save anything)
*/
save: function(force) {},
save: function() {},
/**
* Startup does nothing as this store doesn't require starting up.
@@ -387,7 +385,6 @@ module.exports.MemoryStore.prototype = {
};
return Promise.resolve();
},
/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
@@ -398,7 +395,6 @@ module.exports.MemoryStore.prototype = {
getOutOfBandMembers: function(roomId) {
return Promise.resolve(this._oobMembers[roomId] || null);
},
/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
@@ -412,11 +408,6 @@ module.exports.MemoryStore.prototype = {
return Promise.resolve();
},
clearOutOfBandMembers: function() {
this._oobMembers = {};
return Promise.resolve();
},
getClientOptions: function() {
return Promise.resolve(this._clientOptions);
},
+1 -13
View File
@@ -22,7 +22,6 @@ limitations under the License.
*/
const utils = require("../../utils");
import logger from '../../logger';
const DEBUG = false; // set true to enable console logging.
const E2E_PREFIX = "session.e2e.";
@@ -191,22 +190,11 @@ WebStorageSessionStore.prototype = {
removeAllEndToEndRooms: function() {
removeByPrefix(this.store, keyEndToEndRoom(''));
},
setLocalTrustedBackupPubKey: function(pubkey) {
this.store.setItem(KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY, pubkey);
},
// XXX: This store is deprecated really, but added this as a temporary
// thing until cross-signing lands.
getLocalTrustedBackupPubKey: function() {
return this.store.getItem(KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY);
},
};
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
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";
const KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY = E2E_PREFIX + "trusted_backup_pubkey";
function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
@@ -258,7 +246,7 @@ function removeByPrefix(store, prefix) {
function debuglog() {
if (DEBUG) {
logger.log(...arguments);
console.log(...arguments);
}
}
+1 -2
View File
@@ -21,7 +21,6 @@ limitations under the License.
*/
import utils from "./utils";
import logger from '../src/logger';
/**
@@ -169,7 +168,7 @@ class SyncAccumulator {
}
break;
default:
logger.error("Unknown cateogory: ", category);
console.error("Unknown cateogory: ", category);
}
}
+31 -127
View File
@@ -32,8 +32,6 @@ const Group = require('./models/group');
const utils = require("./utils");
const Filter = require("./filter");
const EventTimeline = require("./models/event-timeline");
const PushProcessor = require("./pushprocessor");
import logger from '../src/logger';
import {InvalidStoreError} from './errors';
@@ -60,7 +58,7 @@ function debuglog(...params) {
if (!DEBUG) {
return;
}
logger.log(...params);
console.log(...params);
}
@@ -118,25 +116,17 @@ function SyncApi(client, opts) {
*/
SyncApi.prototype.createRoom = function(roomId) {
const client = this.client;
const {
timelineSupport,
unstableClientRelationAggregation,
} = client;
const room = new Room(roomId, client, client.getUserId(), {
lazyLoadMembers: this.opts.lazyLoadMembers,
pendingEventOrdering: this.opts.pendingEventOrdering,
timelineSupport,
unstableClientRelationAggregation,
timelineSupport: client.timelineSupport,
});
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline",
"Room.redaction",
"Room.redactionCancelled",
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction",
"Room.receipt", "Room.tags",
"Room.timelineReset",
"Room.localEchoUpdated",
"Room.accountData",
"Room.myMembership",
"Room.replaceEvent",
]);
this._registerStateListeners(room);
return room;
@@ -396,7 +386,7 @@ SyncApi.prototype._peekPoll = function(peekRoom, token) {
peekRoom.addLiveEvents(events);
self._peekPoll(peekRoom, res.end);
}, function(err) {
logger.error("[%s] Peek poll failed: %s", peekRoom.roomId, err);
console.error("[%s] Peek poll failed: %s", peekRoom.roomId, err);
setTimeout(function() {
self._peekPoll(peekRoom, token);
}, 30 * 1000);
@@ -455,16 +445,6 @@ SyncApi.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) {
return false;
};
SyncApi.prototype._shouldAbortSync = function(error) {
if (error.errcode === "M_UNKNOWN_TOKEN") {
// The logout already happened, we just need to stop.
logger.warn("Token no longer valid - assuming logout");
this.stop();
return true;
}
return false;
};
/**
* Main entry point
*/
@@ -492,17 +472,13 @@ SyncApi.prototype.sync = function() {
async function getPushRules() {
try {
debuglog("Getting push rules...");
const result = await client.getPushRules();
debuglog("Got push rules");
client.pushRules = result;
} catch (err) {
logger.error("Getting push rules failed", err);
if (self._shouldAbortSync(err)) return;
// wait for saved sync to complete before doing anything else,
// otherwise the sync state will end up being incorrect
debuglog("Waiting for saved sync before retrying push rules...");
await self.recoverFromSyncStartupError(savedSyncPromise, err);
getPushRules();
return;
@@ -511,35 +487,22 @@ SyncApi.prototype.sync = function() {
}
const checkLazyLoadStatus = async () => {
debuglog("Checking lazy load status...");
if (this.opts.lazyLoadMembers && client.isGuest()) {
this.opts.lazyLoadMembers = false;
}
if (this.opts.lazyLoadMembers) {
debuglog("Checking server lazy load support...");
const supported = await client.doesServerSupportLazyLoading();
if (supported) {
try {
debuglog("Creating and storing lazy load sync filter...");
this.opts.filter = await client.createFilter(
Filter.LAZY_LOADING_SYNC_FILTER,
);
debuglog("Created and stored lazy load sync filter");
} catch (err) {
logger.error(
"Creating and storing lazy load sync filter failed",
err,
);
throw err;
}
this.opts.filter = await client.createFilter(
Filter.LAZY_LOADING_SYNC_FILTER,
);
} else {
debuglog("LL: lazy loading requested but not supported " +
console.log("LL: lazy loading requested but not supported " +
"by server, so disabling");
this.opts.lazyLoadMembers = false;
}
}
// need to vape the store when enabling LL and wasn't enabled before
debuglog("Checking whether lazy loading has changed in store...");
const shouldClear = await this._wasLazyLoadingToggled(this.opts.lazyLoadMembers);
if (shouldClear) {
this._storeIsInvalid = true;
@@ -550,26 +513,18 @@ SyncApi.prototype.sync = function() {
// we leave the state as 'ERROR' which isn't great since this normally means
// we're retrying. The client must be stopped before clearing the stores anyway
// so the app should stop the client, clear the store and start it again.
logger.warn("InvalidStoreError: store is not usable: stopping sync.");
console.warn("InvalidStoreError: store is not usable: stopping sync.");
return;
}
if (this.opts.lazyLoadMembers && this.opts.crypto) {
this.opts.crypto.enableLazyLoading();
}
try {
debuglog("Storing client options...");
await this.client._storeClientOptions();
debuglog("Stored client options");
} catch (err) {
logger.error("Storing client options failed", err);
throw err;
}
await this.client._storeClientOptions();
getFilter(); // Now get the filter and start syncing
};
async function getFilter() {
debuglog("Getting filter...");
let filter;
if (self.opts.filter) {
filter = self.opts.filter;
@@ -584,11 +539,8 @@ SyncApi.prototype.sync = function() {
getFilterName(client.credentials.userId), filter,
);
} catch (err) {
logger.error("Getting filter failed", err);
if (self._shouldAbortSync(err)) return;
// wait for saved sync to complete before doing anything else,
// otherwise the sync state will end up being incorrect
debuglog("Waiting for saved sync before retrying filter...");
await self.recoverFromSyncStartupError(savedSyncPromise, err);
getFilter();
return;
@@ -602,12 +554,11 @@ SyncApi.prototype.sync = function() {
if (self._currentSyncRequest === null) {
// Send this first sync request here so we can then wait for the saved
// sync data to finish processing before we process the results of this one.
debuglog("Sending first sync request...");
console.log("Sending first sync request...");
self._currentSyncRequest = self._doSyncRequest({ filterId }, savedSyncToken);
}
// Now wait for the saved sync to finish...
debuglog("Waiting for saved sync before starting sync processing...");
await savedSyncPromise;
self._sync({ filterId });
}
@@ -619,19 +570,13 @@ SyncApi.prototype.sync = function() {
// Pull the saved sync token out first, before the worker starts sending
// all the sync data which could take a while. This will let us send our
// first incremental sync request before we've processed our saved data.
debuglog("Getting saved sync token...");
savedSyncPromise = client.store.getSavedSyncToken().then((tok) => {
debuglog("Got saved sync token");
savedSyncToken = tok;
debuglog("Getting saved sync...");
return client.store.getSavedSync();
}).then((savedSync) => {
debuglog(`Got reply from saved sync, exists? ${!!savedSync}`);
if (savedSync) {
return self._syncFromCache(savedSync);
}
}).catch(err => {
logger.error("Getting saved sync failed", err);
});
// Now start the first incremental sync request: this can also
// take a while so if we set it going now, we can wait for it
@@ -703,7 +648,7 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
try {
await this._processSyncResponse(syncEventData, data);
} catch(e) {
logger.error("Error processing cached sync", e.stack || e);
console.error("Error processing cached sync", e.stack || e);
}
// Don't emit a prepared if we've bailed because the store is invalid:
@@ -778,10 +723,7 @@ SyncApi.prototype._sync = async function(syncOptions) {
} catch(e) {
// log the exception with stack if we have it, else fall back
// to the plain description
logger.error("Caught /sync error", e.stack || e);
// Emit the exception for client handling
this.client.emit("sync.unexpectedError", e);
console.error("Caught /sync error", e.stack || e);
}
// update this as it may have changed
@@ -892,15 +834,11 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) {
return;
}
logger.error("/sync error %s", err);
logger.error(err);
if(this._shouldAbortSync(err)) {
return;
}
console.error("/sync error %s", err);
console.error(err);
this._failedSyncCount++;
logger.log('Number of consecutive failed sync requests:', this._failedSyncCount);
console.log('Number of consecutive failed sync requests:', this._failedSyncCount);
debuglog("Starting keep-alive");
// Note that we do *not* mark the sync connection as
@@ -1031,9 +969,8 @@ SyncApi.prototype._processSyncResponse = async function(
// honour push rules that were previously cached. Base rules
// will be updated when we recieve push rules via getPushRules
// (see SyncApi.prototype.sync) before syncing over the network.
if (accountDataEvent.getType() === 'm.push_rules') {
const rules = accountDataEvent.getContent();
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
if (accountDataEvent.getType() == 'm.push_rules') {
client.pushRules = accountDataEvent.getContent();
}
client.emit("accountData", accountDataEvent);
return accountDataEvent;
@@ -1045,26 +982,8 @@ SyncApi.prototype._processSyncResponse = async function(
if (data.to_device && utils.isArray(data.to_device.events) &&
data.to_device.events.length > 0
) {
const cancelledKeyVerificationTxns = [];
data.to_device.events
.map(client.getEventMapper())
.map((toDeviceEvent) => { // map is a cheap inline forEach
// We want to flag m.key.verification.start events as cancelled
// if there's an accompanying m.key.verification.cancel event, so
// we pull out the transaction IDs from the cancellation events
// so we can flag the verification events as cancelled in the loop
// below.
if (toDeviceEvent.getType() === "m.key.verification.cancel") {
const txnId = toDeviceEvent.getContent()['transaction_id'];
if (txnId) {
cancelledKeyVerificationTxns.push(txnId);
}
}
// as mentioned above, .map is a cheap inline forEach, so return
// the unmodified event.
return toDeviceEvent;
})
.forEach(
function(toDeviceEvent) {
const content = toDeviceEvent.getContent();
@@ -1073,21 +992,13 @@ SyncApi.prototype._processSyncResponse = async function(
content.msgtype == "m.bad.encrypted"
) {
// the mapper already logged a warning.
logger.log(
console.log(
'Ignoring undecryptable to-device event from ' +
toDeviceEvent.getSender(),
);
return;
}
if (toDeviceEvent.getType() === "m.key.verification.start"
|| toDeviceEvent.getType() === "m.key.verification.request") {
const txnId = content['transaction_id'];
if (cancelledKeyVerificationTxns.includes(txnId)) {
toDeviceEvent.flagCancelled();
}
}
client.emit("toDeviceEvent", toDeviceEvent);
},
);
@@ -1137,6 +1048,7 @@ SyncApi.prototype._processSyncResponse = async function(
const stateEvents =
self._mapSyncEventsFormat(inviteObj.invite_state, room);
room.updateMyMembership("invite");
self._processRoomEvents(room, stateEvents);
if (inviteObj.isBrandNewRoom) {
room.recalculate();
@@ -1146,7 +1058,6 @@ SyncApi.prototype._processSyncResponse = async function(
stateEvents.forEach(function(e) {
client.emit("event", e);
});
room.updateMyMembership("invite");
});
// Handle joins
@@ -1162,20 +1073,13 @@ SyncApi.prototype._processSyncResponse = async function(
room.setUnreadNotificationCount(
'total', joinObj.unread_notifications.notification_count,
);
// We track unread notifications ourselves in encrypted rooms, so don't
// bother setting it here. We trust our calculations better than the
// server's for this case, and therefore will assume that our non-zero
// count is accurate.
const encrypted = client.isRoomEncrypted(room.roomId);
if (!encrypted
|| (encrypted && room.getUnreadNotificationCount('highlight') <= 0)) {
room.setUnreadNotificationCount(
'highlight', joinObj.unread_notifications.highlight_count,
);
}
room.setUnreadNotificationCount(
'highlight', joinObj.unread_notifications.highlight_count,
);
}
room.updateMyMembership("join");
joinObj.timeline = joinObj.timeline || {};
if (joinObj.isBrandNewRoom) {
@@ -1247,8 +1151,10 @@ SyncApi.prototype._processSyncResponse = async function(
room.setSummary(joinObj.summary);
}
// we deliberately don't add ephemeral events to the timeline
room.addEphemeralEvents(ephemeralEvents);
// XXX: should we be adding ephemeralEvents to the timeline?
// It feels like that for symmetry with room.addAccountData()
// there should be a room.addEphemeralEvents() or similar.
room.addLiveEvents(ephemeralEvents);
// we deliberately don't add accountData to the timeline
room.addAccountData(accountDataEvents);
@@ -1286,8 +1192,6 @@ SyncApi.prototype._processSyncResponse = async function(
accountDataEvents.forEach(function(e) {
client.emit("event", e);
});
room.updateMyMembership("join");
});
// Handle leaves (e.g. kicked rooms)
@@ -1300,6 +1204,8 @@ SyncApi.prototype._processSyncResponse = async function(
const accountDataEvents =
self._mapSyncEventsFormat(leaveObj.account_data);
room.updateMyMembership("leave");
self._processRoomEvents(room, stateEvents, timelineEvents);
room.addAccountData(accountDataEvents);
@@ -1320,8 +1226,6 @@ SyncApi.prototype._processSyncResponse = async function(
accountDataEvents.forEach(function(e) {
client.emit("event", e);
});
room.updateMyMembership("leave");
});
// update the notification timeline, if appropriate.

Some files were not shown because too many files have changed in this diff Show More