Compare commits
614 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d59963b53 | |||
| c669aafedb | |||
| 358f13500b | |||
| 016f16954a | |||
| c1543545d2 | |||
| 5da936d96a | |||
| 0dead73837 | |||
| 92ca2386ea | |||
| 59b25d6837 | |||
| a6f7936311 | |||
| 7392b4de17 | |||
| 79b0a5fada | |||
| aee9442e52 | |||
| 569d5d1fce | |||
| 6ea8003df2 | |||
| c8ab82010a | |||
| bf1bec9c6c | |||
| e0c90ec9e3 | |||
| 7ad5021147 | |||
| fd73c3fb3a | |||
| e3dbf7cc41 | |||
| 18749c580e | |||
| 396db30fbf | |||
| 6b38868de6 | |||
| 01a46ad880 | |||
| 46f8251e94 | |||
| 77f882f45a | |||
| 8c72fd104e | |||
| 549656884b | |||
| 5b8b0a8aa3 | |||
| b1924d4db6 | |||
| 1b877118ef | |||
| 682a5daf1c | |||
| fcbfaac1fd | |||
| 3787b6f1c7 | |||
| 6e08835496 | |||
| 191695da5a | |||
| 2215087f96 | |||
| 32234ee7fc | |||
| aa37f697bf | |||
| 057303d57c | |||
| ccc85d98e2 | |||
| c30a8b5a29 | |||
| 295010893d | |||
| 7fb807919c | |||
| bd8f8ef28d | |||
| 3901a381cc | |||
| 12f6e51ef6 | |||
| aa8454e30a | |||
| 6b70230e0d | |||
| 5e0ba9971c | |||
| fa577c9475 | |||
| 11a958b8ca | |||
| 6952db6762 | |||
| 51898cffe8 | |||
| d8337d703d | |||
| adac0c353c | |||
| 04fca16420 | |||
| ca89b6e7a8 | |||
| ac1173c628 | |||
| 0a0ae111f6 | |||
| 71a6e015f4 | |||
| e8bbb8a1cc | |||
| 04764998cb | |||
| 5262d716e4 | |||
| 7addacba38 | |||
| 8f8c9c8ec0 | |||
| 3a9832a8c6 | |||
| 4a40c10d4c | |||
| 58f8ca7d66 | |||
| 4d950fec66 | |||
| b4f68f4fc6 | |||
| ac742aad70 | |||
| 53d225a1d1 | |||
| 549b0f9313 | |||
| 2ce106382a | |||
| b44f43e5db | |||
| 2321b9a04e | |||
| 3bd518cf7f | |||
| c57109c2f3 | |||
| 522640edd9 | |||
| 5fc0629201 | |||
| 26edd7431a | |||
| fd58957b06 | |||
| 12bb0b86dd | |||
| 165c1fc0b6 | |||
| c785b10603 | |||
| 90512bdd5f | |||
| 10751e9a6d | |||
| d2ebc58c3c | |||
| d51c5a2d68 | |||
| 1f24845431 | |||
| 3b02b62ba5 | |||
| 24ae787736 | |||
| cd735ef459 | |||
| 180fea8ace | |||
| 5f02c4b5ad | |||
| 41680f6089 | |||
| 730f7d3dff | |||
| d32033f105 | |||
| 440274d639 | |||
| f93130a8a7 | |||
| 3d9bddfb9f | |||
| 439abbcce9 | |||
| ac91367801 | |||
| 2a63cc474c | |||
| 56261263f5 | |||
| 04b57bbe9d | |||
| c550f83a04 | |||
| 5224ef4b1f | |||
| 2ab033e76e | |||
| fa2e669eda | |||
| f0ba1f2ac0 | |||
| 6d0237ec71 | |||
| 97dff4640a | |||
| 00b571a429 | |||
| 86e0f49231 | |||
| f2f205f9bd | |||
| f84ec090cb | |||
| d37ed9ff6f | |||
| f5a5f5e51a | |||
| fe010242d9 | |||
| 545ebf81bf | |||
| 408934932a | |||
| 6f42824c35 | |||
| c3215d51bd | |||
| e541b96a71 | |||
| 904a2f466e | |||
| bad48da11a | |||
| ce2d1d6e2b | |||
| 2820071db1 | |||
| 5937185ce9 | |||
| be9b7a0d24 | |||
| 7ca09ad749 | |||
| 686a7a40f9 | |||
| 2a7b2835b6 | |||
| 69ecf3b145 | |||
| 2cd748b50c | |||
| 291133beb9 | |||
| e10c17c866 | |||
| 0048cbef08 | |||
| d9d65309b3 | |||
| d5d8032b5b | |||
| 693c749da0 | |||
| 7218e31a9c | |||
| 1798f3921f | |||
| d12c56a623 | |||
| 26aa3d3ce7 | |||
| c97a87d1f6 | |||
| 9bc185d459 | |||
| 4c651c15ea | |||
| a98e6964ef | |||
| 6f8d9c4693 | |||
| fbc4bd0c96 | |||
| 03c9241783 | |||
| 3a983271d6 | |||
| 03fe4afe32 | |||
| 12627022d1 | |||
| fabfe16d45 | |||
| a34758f938 | |||
| 20f5c3ea28 | |||
| 62e490cfe4 | |||
| a9dba39623 | |||
| f1d417597c | |||
| 549f679bf1 | |||
| 6ba052dcc4 | |||
| de873b84f5 | |||
| 37558ac1b4 | |||
| 9140d5a091 | |||
| 7827af0d90 | |||
| 1af8d20adf | |||
| 91df096698 | |||
| e8fd0498a7 | |||
| f3073e120d | |||
| a571624e13 | |||
| 74b649c04c | |||
| 7973b99f50 | |||
| e8f5a8b89d | |||
| 2d0bda933c | |||
| 49588da73d | |||
| 3e2d845342 | |||
| e92d2bd70a | |||
| de1b545df1 | |||
| 3bec28b2ff | |||
| 8cad116dd7 | |||
| 35adb75d80 | |||
| e9908b1d97 | |||
| fffd2eb70a | |||
| 136b9c0f50 | |||
| 0f1206b4ee | |||
| 46d7e4c707 | |||
| c874783742 | |||
| bb296f50d9 | |||
| da68b53ff9 | |||
| bbe141d44e | |||
| 8a03e41a7c | |||
| a79e1bc976 | |||
| 056bfbf7a3 | |||
| e0b64a487d | |||
| d47d1d8f26 | |||
| 42a07de9a7 | |||
| aead855470 | |||
| 335b2314f1 | |||
| 89bab24c14 | |||
| 3a439dcdad | |||
| 20d82eb92f | |||
| 319e1d1191 | |||
| 5f3492dbf8 | |||
| 107c8c0b1f | |||
| 8c6d9586bf | |||
| 1271fc6bf3 | |||
| c9df03c40c | |||
| d8e8dddd25 | |||
| 27f6745123 | |||
| 964f448334 | |||
| 20ee03bb44 | |||
| 77bd677182 | |||
| e024d047e3 | |||
| 40943edc06 | |||
| e6699c5424 | |||
| bd8a307e50 | |||
| f71301cafc | |||
| 562bf9331b | |||
| 11e6eb94b5 | |||
| cee3aa2a7a | |||
| 81e3783488 | |||
| fc7f9786f8 | |||
| 0808c0edf1 | |||
| 8de6746efd | |||
| eb9b8ef7c6 | |||
| b09621b915 | |||
| 8d667f9367 | |||
| 56dfe6630f | |||
| 8b3b181a48 | |||
| c952768542 | |||
| 1a368aa996 | |||
| 61449458cf | |||
| 4eb547e535 | |||
| b54acffaef | |||
| 65a1833e1f | |||
| 1ce4f25811 | |||
| 3127105516 | |||
| d59ea4be78 | |||
| f256f04440 | |||
| b444aaa67e | |||
| 745185e689 | |||
| 2bfa891f0a | |||
| 147167bed3 | |||
| 565e18e8a3 | |||
| 55b4595bbf | |||
| eeb2c463dc | |||
| d9bb0e9a52 | |||
| 7d07ab7c7e | |||
| f8ff3aac58 | |||
| 299a7728d1 | |||
| 39dc6a1742 | |||
| f21c5aa7f2 | |||
| e9bc3f26a5 | |||
| 23eaddd6ea | |||
| 8143ce8450 | |||
| 0a487ec43e | |||
| 0edb483802 | |||
| 06a32ce0a1 | |||
| 8cae00407a | |||
| a57ec87c67 | |||
| 4e62491ea4 | |||
| 5758029c1e | |||
| 8f08710c58 | |||
| 90f98105f0 | |||
| 90354aa330 | |||
| aaabebe7f5 | |||
| 80a92dcdc2 | |||
| dc9081e9d4 | |||
| 3c299637b6 | |||
| 07af333943 | |||
| 0bbc781d0c | |||
| 79bf64f079 | |||
| ed67d39456 | |||
| 2f8cc75432 | |||
| 03cccef805 | |||
| 6d5a0c2718 | |||
| 42b359eb5c | |||
| 3071587f11 | |||
| f3ec9768bc | |||
| 23159807b0 | |||
| b1ba9f76b8 | |||
| 0e51dfed46 | |||
| 09b00335f8 | |||
| 3d274815d9 | |||
| 70d60b905d | |||
| 3e2ffb25a6 | |||
| 8b9bef5cb3 | |||
| 31e72efc91 | |||
| 60b7252597 | |||
| 3980b62df2 | |||
| b306df726a | |||
| 3d5a79be3b | |||
| ba78d1a9ae | |||
| 241811298f | |||
| 8a0ddc43ab | |||
| 898fa0e41b | |||
| 081ff4dec0 | |||
| 3c69b8511d | |||
| 6843d86ecf | |||
| 2e91200136 | |||
| 852304c417 | |||
| ee752e3885 | |||
| b9480e4302 | |||
| 2ae4d07971 | |||
| 90cac8a118 | |||
| db18274f6e | |||
| 172bad8b55 | |||
| dfe454e18f | |||
| 3d8dd29b4c | |||
| c3ff213ec9 | |||
| e80e5e1f8c | |||
| bba249d5ce | |||
| f57df2bee5 | |||
| b930638156 | |||
| 39c1de19fc | |||
| 17724fc8d3 | |||
| 4c6d11d9ed | |||
| 05d77a85c9 | |||
| e95a133cdd | |||
| c21382d721 | |||
| 8c15125e23 | |||
| 64ddbd97dd | |||
| 9c24bcb7a9 | |||
| 8f016726f0 | |||
| 649fe7a490 | |||
| 35f1cdf89c | |||
| 06adc34fb3 | |||
| 87bf07f95e | |||
| f05bf3f845 | |||
| ab512d087c | |||
| 6799c29921 | |||
| a3f1da1981 | |||
| 3b225651cc | |||
| a40d691159 | |||
| 4ebe60b2ad | |||
| 5a70859593 | |||
| c7be810e65 | |||
| 101217cfb6 | |||
| 5c2aa4677f | |||
| ab9bfa68ae | |||
| aa8c2ca277 | |||
| 84509087ac | |||
| 2450d461fd | |||
| 50c590ae26 | |||
| 516dff06ee | |||
| 9a8af05bfb | |||
| c9bf61c387 | |||
| 4f0f2e8c16 | |||
| 6f042a2142 | |||
| 91416bdbb2 | |||
| 9b093f7569 | |||
| b004d1602d | |||
| 6cca73b999 | |||
| fafd6df13e | |||
| 8f77870526 | |||
| eb0462e89b | |||
| 80748d7d85 | |||
| b694d53b73 | |||
| 8004e82c50 | |||
| 85b5849228 | |||
| 6b86777e96 | |||
| efe64a4817 | |||
| ad777f36b2 | |||
| de77ad867c | |||
| 9b35f86497 | |||
| 84fc8b1931 | |||
| 455c85fb69 | |||
| 2a2fed695b | |||
| c1f28bd410 | |||
| c74e0bb6b3 | |||
| 5b9e158035 | |||
| a8d200dd02 | |||
| 34ae967cb8 | |||
| a67f14825e | |||
| 50c14d0ab8 | |||
| 0edb6e6f6f | |||
| 36d0dacda1 | |||
| 83b74070aa | |||
| f80af68686 | |||
| fe4ac06f43 | |||
| eaaa3e980a | |||
| 07629bfb9a | |||
| 88fdeca2bf | |||
| a3c8eac38b | |||
| bd5380c0b4 | |||
| c3b5767999 | |||
| 9e5c2732c9 | |||
| b8957fa917 | |||
| 52c139dcdc | |||
| 39d4bf1494 | |||
| 4c713e3387 | |||
| bb486f5148 | |||
| 524fea1297 | |||
| e9528ebb98 | |||
| de18283c3b | |||
| cc1c7561a3 | |||
| 5d928f07a0 | |||
| 01b882480f | |||
| 21d52fdbdd | |||
| 7f8b9de560 | |||
| 761f22b63d | |||
| f9baff2a3a | |||
| 71eca4ffcc | |||
| bc8dca5105 | |||
| 3ae3dffff7 | |||
| 81c6023940 | |||
| 3a0f27fa7e | |||
| 60e339bac0 | |||
| ecb88f45b7 | |||
| 24a869d15b | |||
| 1d427a1ea8 | |||
| 90e25867ad | |||
| 2ae56e61cb | |||
| 56c0830328 | |||
| 093f139d34 | |||
| 1083efc212 | |||
| 69773c2619 | |||
| 2525b5a5d8 | |||
| b00804102d | |||
| 8d1d657c44 | |||
| ff9c84ff94 | |||
| 3aa2bf8a76 | |||
| 6cd09c6af2 | |||
| 46a8486245 | |||
| c5caf8f8f4 | |||
| a229ece693 | |||
| b435137332 | |||
| 2cdbc9f4db | |||
| aa6884e484 | |||
| 4f4d694687 | |||
| 1b47999e80 | |||
| 02427651dd | |||
| 8eeb088e50 | |||
| 3f62581556 | |||
| b53a7f6ee8 | |||
| 4356603665 | |||
| 1cae5e8b97 | |||
| a4591afba6 | |||
| 6ea4c77dd5 | |||
| dc3d90d696 | |||
| 00de919eb4 | |||
| fdacc2f7ab | |||
| 5a64c29228 | |||
| c5cddf1607 | |||
| 44c7844a4b | |||
| f293da5b34 | |||
| 4f044de79e | |||
| da116e6077 | |||
| 2489180c47 | |||
| 4ec4d330aa | |||
| c75ca1c2d6 | |||
| 67462e9fc4 | |||
| 424b6303ef | |||
| 59c4e2c354 | |||
| ecca8bc86e | |||
| 43ca920b10 | |||
| e1a3f8f053 | |||
| 6e7cb63e7d | |||
| ab1177d987 | |||
| ee0a1d281d | |||
| aef7b9a1dc | |||
| 7cb8de5b69 | |||
| 5c2fb1c42b | |||
| 553857583d | |||
| 6d0923153f | |||
| e34eb48914 | |||
| 05ab6ef3ab | |||
| 81eefc1377 | |||
| 895d854e1c | |||
| e432d4f808 | |||
| 56c0ae294b | |||
| 0b9d68b4f2 | |||
| e2e034f795 | |||
| bb5e3d51b8 | |||
| 70b23614b5 | |||
| 24a75e3765 | |||
| 07c2e34d87 | |||
| 5bcbe76f2c | |||
| d694ee3ef3 | |||
| efbdf4e1a8 | |||
| 44bfc2e846 | |||
| 0121bdbb75 | |||
| b8ba77a7b5 | |||
| 65dd5cc6ad | |||
| 8aeb994839 | |||
| 64daa444dd | |||
| 26aab4f38d | |||
| 6059df1b67 | |||
| 2a0c85c772 | |||
| 3488fbe64c | |||
| 811a98ad19 | |||
| 4462f4b90e | |||
| 4143a79f7b | |||
| 3ed9b00398 | |||
| b005b75331 | |||
| a9f9e2cf35 | |||
| 5602b94dcb | |||
| 930de640ac | |||
| 6d9fba8191 | |||
| 624c6f0a6e | |||
| 7d2f7fae45 | |||
| 3f917b39c9 | |||
| f1336a5ce7 | |||
| 7a10d504b2 | |||
| 831aec6488 | |||
| 6eb229ac1e | |||
| c58db665dd | |||
| e222fb1783 | |||
| 4c6fa89053 | |||
| 98815ffdf6 | |||
| 31a0192c2d | |||
| 53f8091e3a | |||
| 012cbf7995 | |||
| ac26c91cba | |||
| c13162aada | |||
| 9fb6eea8b7 | |||
| 23c4f19cda | |||
| 3b34570749 | |||
| 0412ca5810 | |||
| c80518bf3e | |||
| 61ee6eb8af | |||
| 654e8b41fa | |||
| 7080458f7e | |||
| 08d236f5ec | |||
| e332a7d113 | |||
| 7879709f62 | |||
| 56e030762e | |||
| bac73150ca | |||
| 2e1fb15ada | |||
| ae9bcd6f6c | |||
| c18c679b9b | |||
| d014ee0b72 | |||
| a30ef7250b | |||
| 6f6e7ea921 | |||
| 0c714ba4a1 | |||
| 5f539aacd9 | |||
| 570ce4f4b7 | |||
| 3c7c9048eb | |||
| 6a77df7b41 | |||
| 41243757ee | |||
| 2af311bd7d | |||
| 1bc9ee7110 | |||
| 4e040f8e77 | |||
| 26c1c6db3b | |||
| d38da83656 | |||
| 4a9a1b40e9 | |||
| dc971b9a59 | |||
| 58f163ed5c | |||
| c0c9f0122c | |||
| d33395e46d | |||
| b83c7d3929 | |||
| b5df016b1b | |||
| a8b6be3b38 | |||
| 78cf175f5a | |||
| 1b78856a7d | |||
| 8194287391 | |||
| 2eecea9a07 | |||
| 465032dd4f | |||
| e473315a89 | |||
| 9d34ad5287 | |||
| a532cc5cf9 | |||
| 60c6c5bc41 | |||
| 0cbbbe8503 | |||
| 47a8d3e50a | |||
| 304da09f3b | |||
| 137c6919f6 | |||
| 842ce30190 | |||
| a0456dc430 | |||
| 52ec831b16 | |||
| 9a2bf78a8e | |||
| cb16f7a60b | |||
| ad84631ddb | |||
| 95131c7658 | |||
| 936eef194a | |||
| 941d871daf | |||
| 609ee663fa | |||
| d78426d708 | |||
| 53804cac5c | |||
| 4ef970b4da | |||
| b199f133b3 | |||
| 7d1b183a1b | |||
| 49d119e92e | |||
| feed1da570 | |||
| 193ad9e09d | |||
| a3ad835d84 | |||
| f8afee8ebd | |||
| 7e955fc312 | |||
| eebf92366f | |||
| 3c23e166a7 | |||
| de8063a43a | |||
| a73dabcb67 | |||
| 405451d783 | |||
| b0275afac2 | |||
| ae71f41138 | |||
| ec2f07e1aa | |||
| 32814d1833 | |||
| e54f71718f | |||
| 7f5584e4f5 | |||
| b3513dc8f8 | |||
| 1b82dffcb4 | |||
| 5500f0d794 | |||
| c8082535de | |||
| 7dedcb82b2 | |||
| 7195365188 | |||
| 910d0ec9c1 | |||
| 1d58a64ee1 | |||
| 1f77cc6d1a | |||
| 02d4dcb128 | |||
| 2b54f442d1 |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"presets": ["es2015", "es2016"],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ steps:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
- label: ":karma: Tests"
|
||||
- label: ":jest: Tests"
|
||||
command:
|
||||
- "yarn install"
|
||||
- "yarn test"
|
||||
|
||||
@@ -12,10 +12,12 @@ module.exports = {
|
||||
// babel's transform-runtime converts references to ES6 globals such as
|
||||
// Promise and Map to core-js polyfills, so we can use ES6 globals.
|
||||
es6: true,
|
||||
jest: true,
|
||||
},
|
||||
extends: ["eslint:recommended", "google"],
|
||||
plugins: [
|
||||
"babel",
|
||||
"jest",
|
||||
],
|
||||
rules: {
|
||||
// rules we've always adhered to or now do
|
||||
@@ -53,6 +55,7 @@ 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"],
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
patreon: matrixdotorg
|
||||
liberapay: matrixdotorg
|
||||
@@ -10,7 +10,6 @@ build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
/dist
|
||||
/lib
|
||||
/specbuild
|
||||
|
||||
+401
@@ -1,3 +1,404 @@
|
||||
Changes in [2.4.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.6) (2019-12-09)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.6-rc.1...v2.4.6)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [2.4.6-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.6-rc.1) (2019-12-04)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.5...v2.4.6-rc.1)
|
||||
|
||||
* Update alias handling
|
||||
[\#1102](https://github.com/matrix-org/matrix-js-sdk/pull/1102)
|
||||
* increase timeout on flush to fix failing unit test
|
||||
[\#1096](https://github.com/matrix-org/matrix-js-sdk/pull/1096)
|
||||
* Disable broken cross-signing test
|
||||
[\#1095](https://github.com/matrix-org/matrix-js-sdk/pull/1095)
|
||||
* Fix a couple SAS tests
|
||||
[\#1094](https://github.com/matrix-org/matrix-js-sdk/pull/1094)
|
||||
* Fix Olm unwedging test
|
||||
[\#1093](https://github.com/matrix-org/matrix-js-sdk/pull/1093)
|
||||
* Fix empty string handling in push notifications
|
||||
[\#1089](https://github.com/matrix-org/matrix-js-sdk/pull/1089)
|
||||
* expand e2ee logging to better debug UISIs
|
||||
[\#1090](https://github.com/matrix-org/matrix-js-sdk/pull/1090)
|
||||
* Remove Bluebird: phase 2
|
||||
[\#1087](https://github.com/matrix-org/matrix-js-sdk/pull/1087)
|
||||
* Relax identity server discovery checks to FAIL_PROMPT
|
||||
[\#1062](https://github.com/matrix-org/matrix-js-sdk/pull/1062)
|
||||
* Fix incorrect return value of MatrixClient.prototype.uploadKeys
|
||||
[\#1061](https://github.com/matrix-org/matrix-js-sdk/pull/1061)
|
||||
* Fix calls in e2e rooms
|
||||
[\#1086](https://github.com/matrix-org/matrix-js-sdk/pull/1086)
|
||||
* Monitor verification request over DM as well
|
||||
[\#1085](https://github.com/matrix-org/matrix-js-sdk/pull/1085)
|
||||
* Remove 'check' npm script
|
||||
[\#1084](https://github.com/matrix-org/matrix-js-sdk/pull/1084)
|
||||
* Always process call events in batches
|
||||
[\#1083](https://github.com/matrix-org/matrix-js-sdk/pull/1083)
|
||||
* Fix ringing chirp on loading
|
||||
[\#1082](https://github.com/matrix-org/matrix-js-sdk/pull/1082)
|
||||
* Remove *most* bluebird specific things
|
||||
[\#1081](https://github.com/matrix-org/matrix-js-sdk/pull/1081)
|
||||
* Switch to Jest
|
||||
[\#1080](https://github.com/matrix-org/matrix-js-sdk/pull/1080)
|
||||
|
||||
Changes in [2.4.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.5) (2019-11-27)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.4...v2.4.5)
|
||||
|
||||
* Relax identity server discovery checks to FAIL_PROMPT
|
||||
* Expand E2EE debug logging to diagnose "unable to decrypt" errors
|
||||
|
||||
Changes in [2.4.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.4) (2019-11-25)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.4-rc.1...v2.4.4)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [2.4.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.4-rc.1) (2019-11-20)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.3...v2.4.4-rc.1)
|
||||
|
||||
* Fix SAS verification in encrypted DMs
|
||||
[\#1077](https://github.com/matrix-org/matrix-js-sdk/pull/1077)
|
||||
* Cross-signing / secret storage tweaks
|
||||
[\#1078](https://github.com/matrix-org/matrix-js-sdk/pull/1078)
|
||||
* Fix local trust for key backups
|
||||
[\#1075](https://github.com/matrix-org/matrix-js-sdk/pull/1075)
|
||||
* Add method to get last active timestamp in room
|
||||
[\#1072](https://github.com/matrix-org/matrix-js-sdk/pull/1072)
|
||||
* Check the right Synapse endpoint for determining admin capabilities
|
||||
[\#1071](https://github.com/matrix-org/matrix-js-sdk/pull/1071)
|
||||
* Cross Signing Support
|
||||
[\#832](https://github.com/matrix-org/matrix-js-sdk/pull/832)
|
||||
* Don't double cancel verification request
|
||||
[\#1064](https://github.com/matrix-org/matrix-js-sdk/pull/1064)
|
||||
* Support for verification requests in the timeline
|
||||
[\#1067](https://github.com/matrix-org/matrix-js-sdk/pull/1067)
|
||||
* Use stable API prefix for 3PID APIs when supported
|
||||
[\#1066](https://github.com/matrix-org/matrix-js-sdk/pull/1066)
|
||||
* Remove Jenkins scripts
|
||||
[\#1063](https://github.com/matrix-org/matrix-js-sdk/pull/1063)
|
||||
|
||||
Changes in [2.4.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.3) (2019-11-04)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.3-rc.1...v2.4.3)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [2.4.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.3-rc.1) (2019-10-30)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.2...v2.4.3-rc.1)
|
||||
|
||||
* fix the path in references to logger.js
|
||||
[\#1056](https://github.com/matrix-org/matrix-js-sdk/pull/1056)
|
||||
* verification in DMs
|
||||
[\#1050](https://github.com/matrix-org/matrix-js-sdk/pull/1050)
|
||||
* Properly documented the function possible returns
|
||||
[\#1054](https://github.com/matrix-org/matrix-js-sdk/pull/1054)
|
||||
* Downgrade to Bluebird 3.5.5 to fix Firefox
|
||||
[\#1055](https://github.com/matrix-org/matrix-js-sdk/pull/1055)
|
||||
* Upgrade safe deps to latest major version
|
||||
[\#1053](https://github.com/matrix-org/matrix-js-sdk/pull/1053)
|
||||
* Don't include .js in the import string.
|
||||
[\#1052](https://github.com/matrix-org/matrix-js-sdk/pull/1052)
|
||||
|
||||
Changes in [2.4.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.2) (2019-10-18)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.2-rc.1...v2.4.2)
|
||||
|
||||
* No changes since v2.4.2-rc.1
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
Matrix Javascript SDK
|
||||
=====================
|
||||
[](http://matrix.org/jenkins/job/JavascriptSDK/)
|
||||
|
||||
This is the [Matrix](https://matrix.org) Client-Server v1/v2 alpha SDK for
|
||||
This is the [Matrix](https://matrix.org) Client-Server r0 SDK for
|
||||
JavaScript. This SDK can be run in a browser or in Node.js.
|
||||
|
||||
Quickstart
|
||||
@@ -297,7 +296,7 @@ Then visit ``http://localhost:8005`` to see the API docs.
|
||||
End-to-end encryption support
|
||||
=============================
|
||||
|
||||
The SDK supports end-to-end encryption via the and Megolm protocols, using
|
||||
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.
|
||||
|
||||
@@ -323,13 +322,13 @@ To provide the Olm library in a browser application:
|
||||
|
||||
To provide the Olm library in a node.js application:
|
||||
|
||||
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.0.0.tgz``
|
||||
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``
|
||||
(replace the URL with the latest version you want to use from
|
||||
https://packages.matrix.org/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.0.0.tgz``. If your
|
||||
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.
|
||||
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
#!/bin/bash -l
|
||||
|
||||
set -x
|
||||
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
|
||||
nvm use 10 || exit $?
|
||||
yarn install || exit $?
|
||||
|
||||
RC=0
|
||||
|
||||
function fail {
|
||||
echo $@ >&2
|
||||
RC=1
|
||||
}
|
||||
|
||||
# don't use last time's test reports
|
||||
rm -rf reports coverage || exit $?
|
||||
|
||||
yarn test || fail "yarn test finished with return code $?"
|
||||
|
||||
yarn -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 $?"
|
||||
|
||||
yarn gendoc || fail "JSDoc failed with code $?"
|
||||
|
||||
exit $RC
|
||||
+16
-21
@@ -1,14 +1,11 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "2.0.0",
|
||||
"version": "2.4.6",
|
||||
"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",
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"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",
|
||||
"start:watch": "babel -s -w --skip-initial-build -d lib src",
|
||||
@@ -17,7 +14,7 @@
|
||||
"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",
|
||||
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
|
||||
"lint": "eslint --max-warnings 101 src spec",
|
||||
"lint": "eslint --max-warnings 93 src spec",
|
||||
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt"
|
||||
},
|
||||
"repository": {
|
||||
@@ -44,7 +41,6 @@
|
||||
"git-revision.txt",
|
||||
"index.js",
|
||||
"browser-index.js",
|
||||
"jenkins.sh",
|
||||
"lib",
|
||||
"package.json",
|
||||
"release.sh",
|
||||
@@ -54,11 +50,11 @@
|
||||
"dependencies": {
|
||||
"another-json": "^0.2.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bluebird": "^3.5.0",
|
||||
"bluebird": "3.5.5",
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.2",
|
||||
"loglevel": "1.6.1",
|
||||
"loglevel": "^1.6.4",
|
||||
"qs": "^6.5.2",
|
||||
"request": "^2.88.0",
|
||||
"unhomoglyph": "^1.0.2"
|
||||
@@ -66,28 +62,27 @@
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^23.6.0",
|
||||
"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",
|
||||
"babel-preset-es2016": "^6.24.1",
|
||||
"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",
|
||||
"eslint-plugin-jest": "^23.0.4",
|
||||
"exorcist": "^1.0.1",
|
||||
"jest": "^23.6.0",
|
||||
"jsdoc": "^3.5.5",
|
||||
"lolex": "^1.5.2",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-jenkins-reporter": "^0.4.0",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.0.tgz",
|
||||
"rimraf": "^2.5.4",
|
||||
"source-map-support": "^0.4.11",
|
||||
"sourceify": "^0.1.0",
|
||||
"terser": "^4.0.0",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
|
||||
"rimraf": "^3.0.0",
|
||||
"source-map-support": "^0.5.13",
|
||||
"sourceify": "^1.0.0",
|
||||
"terser": "^4.3.8",
|
||||
"watchify": "^3.11.1"
|
||||
},
|
||||
"browserify": {
|
||||
|
||||
@@ -195,6 +195,11 @@ 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
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
mocha: true,
|
||||
},
|
||||
}
|
||||
+8
-8
@@ -24,9 +24,9 @@ import './olm-loader';
|
||||
import sdk from '..';
|
||||
import testUtils from './test-utils';
|
||||
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
|
||||
@@ -82,7 +82,7 @@ TestClient.prototype.toString = function() {
|
||||
* @return {Promise}
|
||||
*/
|
||||
TestClient.prototype.start = function() {
|
||||
console.log(this + ': starting');
|
||||
logger.log(this + ': starting');
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
@@ -100,7 +100,7 @@ TestClient.prototype.start = function() {
|
||||
this.httpBackend.flushAllExpected(),
|
||||
testUtils.syncPromise(this.client),
|
||||
]).then(() => {
|
||||
console.log(this + ': started');
|
||||
logger.log(this + ': started');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -122,7 +122,7 @@ TestClient.prototype.expectDeviceKeyUpload = function() {
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
expect(content.device_keys).toBeTruthy();
|
||||
|
||||
console.log(self + ': received device keys');
|
||||
logger.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);
|
||||
|
||||
@@ -158,8 +158,8 @@ TestClient.prototype.awaitOneTimeKeyUpload = function() {
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).toNotEqual({});
|
||||
console.log('%s: received %i one-time keys', this,
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
logger.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: {
|
||||
@@ -223,11 +223,11 @@ TestClient.prototype.getSigningKey = function() {
|
||||
* @returns {Promise} promise which completes once the sync has been flushed
|
||||
*/
|
||||
TestClient.prototype.flushSync = function() {
|
||||
console.log(`${this}: flushSync`);
|
||||
logger.log(`${this}: flushSync`);
|
||||
return Promise.all([
|
||||
this.httpBackend.flush('/sync', 1),
|
||||
testUtils.syncPromise(this.client),
|
||||
]).then(() => {
|
||||
console.log(`${this}: flushSync completed`);
|
||||
logger.log(`${this}: flushSync completed`);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import TestClient from '../TestClient';
|
||||
import testUtils from '../test-utils';
|
||||
import logger from '../../src/logger';
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -71,7 +71,7 @@ function getSyncResponse(roomMembers) {
|
||||
|
||||
describe("DeviceList management:", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('not running deviceList tests: Olm not present');
|
||||
logger.warn('not running deviceList tests: Olm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,8 +87,6 @@ describe("DeviceList management:", function() {
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
// we create our own sessionStoreBackend so that we can use it for
|
||||
// another TestClient.
|
||||
sessionStoreBackend = new testUtils.MockStorageApi();
|
||||
@@ -108,7 +106,7 @@ describe("DeviceList management:", function() {
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
console.log("Forcing alice to download our device keys");
|
||||
logger.log("Forcing alice to download our device keys");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
@@ -121,7 +119,7 @@ describe("DeviceList management:", function() {
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
console.log("Telling alice to send a megolm message");
|
||||
logger.log("Telling alice to send a megolm message");
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
|
||||
@@ -30,12 +30,12 @@ import 'source-map-support/register';
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
const sdk = require("../..");
|
||||
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";
|
||||
@@ -55,7 +55,7 @@ function bobUploadsDeviceKeys() {
|
||||
bobTestClient.client.uploadKeys(),
|
||||
bobTestClient.httpBackend.flush(),
|
||||
]).then(() => {
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).toNotEqual(0);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ function expectBobQueryKeys() {
|
||||
|
||||
const aliKeys = {};
|
||||
aliKeys[aliDeviceId] = aliTestClient.deviceKeys;
|
||||
console.log("query result will be", aliKeys);
|
||||
logger.log("query result will be", aliKeys);
|
||||
|
||||
bobTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
@@ -203,7 +203,7 @@ function aliSendsFirstMessage() {
|
||||
expectAliQueryKeys()
|
||||
.then(expectAliClaimKeys)
|
||||
.then(expectAliSendMessageRequest),
|
||||
]).spread(function(_, ciphertext) {
|
||||
]).then(function([_, ciphertext]) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
@@ -218,7 +218,7 @@ function aliSendsMessage() {
|
||||
return Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliSendMessageRequest(),
|
||||
]).spread(function(_, ciphertext) {
|
||||
]).then(function([_, ciphertext]) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
@@ -234,7 +234,7 @@ function bobSendsReplyMessage() {
|
||||
sendMessage(bobTestClient.client),
|
||||
expectBobQueryKeys()
|
||||
.then(expectBobSendMessageRequest),
|
||||
]).spread(function(_, ciphertext) {
|
||||
]).then(function([_, ciphertext]) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
@@ -279,16 +279,17 @@ function sendMessage(client) {
|
||||
|
||||
function expectSendMessageRequest(httpBackend) {
|
||||
const path = "/send/m.room.encrypted/";
|
||||
const deferred = Promise.defer();
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
deferred.resolve(content);
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
const prom = new Promise((resolve) => {
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
resolve(content);
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// it can take a while to process the key query
|
||||
return httpBackend.flush(path, 1).then(() => deferred.promise);
|
||||
return httpBackend.flush(path, 1).then(() => prom);
|
||||
}
|
||||
|
||||
function aliRecvMessage() {
|
||||
@@ -334,7 +335,7 @@ function recvMessage(httpBackend, client, sender, message) {
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
console.log(client.credentials.userId + " received event",
|
||||
logger.log(client.credentials.userId + " received event",
|
||||
event);
|
||||
|
||||
client.removeListener("event", onEvent);
|
||||
@@ -405,8 +406,6 @@ describe("MatrixClient crypto", function() {
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
|
||||
await aliTestClient.client.initCrypto();
|
||||
|
||||
@@ -429,15 +428,14 @@ describe("MatrixClient crypto", function() {
|
||||
.then(bobUploadsDeviceKeys);
|
||||
});
|
||||
|
||||
it("Ali downloads Bobs device keys", function(done) {
|
||||
Promise.resolve()
|
||||
it("Ali downloads Bobs device keys", function() {
|
||||
return Promise.resolve()
|
||||
.then(bobUploadsDeviceKeys)
|
||||
.then(aliDownloadsKeys)
|
||||
.nodeify(done);
|
||||
.then(aliDownloadsKeys);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an invalid signature", function(done) {
|
||||
Promise.resolve()
|
||||
it("Ali gets keys with an invalid signature", function() {
|
||||
return Promise.resolve()
|
||||
.then(bobUploadsDeviceKeys)
|
||||
.then(function() {
|
||||
// tamper bob's keys
|
||||
@@ -454,11 +452,10 @@ describe("MatrixClient crypto", function() {
|
||||
}).then((devices) => {
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
})
|
||||
.nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect userId", function(done) {
|
||||
it("Ali gets keys with an incorrect userId", function() {
|
||||
const eveUserId = "@eve:localhost";
|
||||
|
||||
const bobDeviceKeys = {
|
||||
@@ -487,7 +484,7 @@ describe("MatrixClient crypto", function() {
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
return Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]).then(function() {
|
||||
@@ -495,14 +492,14 @@ describe("MatrixClient crypto", function() {
|
||||
aliTestClient.client.getStoredDevicesForUser(bobUserId),
|
||||
aliTestClient.client.getStoredDevicesForUser(eveUserId),
|
||||
]);
|
||||
}).spread((bobDevices, eveDevices) => {
|
||||
}).then(([bobDevices, eveDevices]) => {
|
||||
// should get an empty list
|
||||
expect(bobDevices).toEqual([]);
|
||||
expect(eveDevices).toEqual([]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect deviceId", function(done) {
|
||||
it("Ali gets keys with an incorrect deviceId", function() {
|
||||
const bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bad_device',
|
||||
@@ -529,7 +526,7 @@ describe("MatrixClient crypto", function() {
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
return Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]).then(function() {
|
||||
@@ -537,7 +534,7 @@ describe("MatrixClient crypto", function() {
|
||||
}).then((devices) => {
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -547,19 +544,18 @@ describe("MatrixClient crypto", function() {
|
||||
.then(() => bobTestClient.awaitOneTimeKeyUpload())
|
||||
.then((keys) => {
|
||||
expect(Object.keys(keys).length).toEqual(5);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).toNotEqual(0);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali sends a message", function(done) {
|
||||
it("Ali sends a message", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
Promise.resolve()
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.nodeify(done);
|
||||
.then(aliSendsFirstMessage);
|
||||
});
|
||||
|
||||
it("Bob receives a message", function() {
|
||||
@@ -607,7 +603,7 @@ describe("MatrixClient crypto", function() {
|
||||
|
||||
const eventPromise = new Promise((resolve, reject) => {
|
||||
const onEvent = function(event) {
|
||||
console.log(bobUserId + " received event",
|
||||
logger.log(bobUserId + " received event",
|
||||
event);
|
||||
resolve(event);
|
||||
};
|
||||
@@ -627,9 +623,9 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", function(done) {
|
||||
it("Ali blocks Bob's device", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
Promise.resolve()
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
@@ -644,12 +640,12 @@ describe("MatrixClient crypto", function() {
|
||||
expect(sentContent.ciphertext).toEqual({});
|
||||
});
|
||||
return Promise.all([p1, p2]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", function(done) {
|
||||
it("Bob receives two pre-key messages", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
Promise.resolve()
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
@@ -657,8 +653,7 @@ describe("MatrixClient crypto", function() {
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobRecvMessage)
|
||||
.then(aliSendsMessage)
|
||||
.then(bobRecvMessage)
|
||||
.nodeify(done);
|
||||
.then(bobRecvMessage);
|
||||
});
|
||||
|
||||
it("Bob replies to the message", function() {
|
||||
@@ -734,7 +729,7 @@ describe("MatrixClient crypto", function() {
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
console.log(aliTestClient + ': starting');
|
||||
logger.log(aliTestClient + ': starting');
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
aliTestClient.expectDeviceKeyUpload();
|
||||
@@ -746,16 +741,16 @@ describe("MatrixClient crypto", function() {
|
||||
aliTestClient.client.startClient({});
|
||||
|
||||
return httpBackend.flushAllExpected().then(() => {
|
||||
console.log(aliTestClient + ': started');
|
||||
logger.log(aliTestClient + ': started');
|
||||
});
|
||||
})
|
||||
.then(() => httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).toNotEqual({});
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys).length)
|
||||
.toBeGreaterThanOrEqualTo(1);
|
||||
console.log('received %i one-time keys',
|
||||
.toBeGreaterThanOrEqual(1);
|
||||
logger.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,7 +4,6 @@ const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
@@ -15,7 +14,6 @@ describe("MatrixClient events", function() {
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
@@ -219,7 +217,7 @@ describe("MatrixClient events", function() {
|
||||
client.on("RoomState.events", function(event, state) {
|
||||
eventsInvokeCount++;
|
||||
const index = roomStateEventTypes.indexOf(event.getType());
|
||||
expect(index).toNotEqual(
|
||||
expect(index).not.toEqual(
|
||||
-1, "Unexpected room state event type: " + event.getType(),
|
||||
);
|
||||
if (index >= 0) {
|
||||
@@ -302,11 +300,32 @@ describe("MatrixClient events", function() {
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
|
||||
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN' };
|
||||
httpBackend.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(event, member) {
|
||||
client.on("Session.logged_out", function(errObj) {
|
||||
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,6 +5,7 @@ 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";
|
||||
@@ -82,18 +83,19 @@ function startClient(httpBackend, client) {
|
||||
client.startClient();
|
||||
|
||||
// set up a promise which will resolve once the client is initialised
|
||||
const deferred = Promise.defer();
|
||||
client.on("sync", function(state) {
|
||||
console.log("sync", state);
|
||||
if (state != "SYNCING") {
|
||||
return;
|
||||
}
|
||||
deferred.resolve();
|
||||
const prom = new Promise((resolve) => {
|
||||
client.on("sync", function(state) {
|
||||
logger.log("sync", state);
|
||||
if (state != "SYNCING") {
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
deferred.promise,
|
||||
prom,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -102,7 +104,6 @@ describe("getEventTimeline support", function() {
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
@@ -114,21 +115,20 @@ describe("getEventTimeline support", function() {
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
it("timeline support must be enabled to work", function(done) {
|
||||
it("timeline support must be enabled to work", function() {
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
});
|
||||
|
||||
startClient(httpBackend, client,
|
||||
).then(function() {
|
||||
return startClient(httpBackend, client).then(function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() {
|
||||
client.getEventTimeline(timelineSet, "event");
|
||||
}).toThrow();
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("timeline support works when enabled", function() {
|
||||
@@ -144,13 +144,13 @@ describe("getEventTimeline support", function() {
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() {
|
||||
client.getEventTimeline(timelineSet, "event");
|
||||
}).toNotThrow();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("scrollback should be able to scroll back to before a gappy /sync",
|
||||
function(done) {
|
||||
function() {
|
||||
// need a client with timelineSupport disabled to make this work
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
@@ -159,8 +159,7 @@ describe("getEventTimeline support", function() {
|
||||
});
|
||||
let room;
|
||||
|
||||
startClient(httpBackend, client,
|
||||
).then(function() {
|
||||
return startClient(httpBackend, client).then(function() {
|
||||
room = client.getRoom(roomId);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
@@ -216,18 +215,15 @@ describe("getEventTimeline support", function() {
|
||||
expect(room.timeline[0].event).toEqual(EVENTS[0]);
|
||||
expect(room.timeline[1].event).toEqual(EVENTS[1]);
|
||||
expect(room.oldState.paginationToken).toEqual("pagin_end");
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient event timelines", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
|
||||
@@ -348,25 +344,25 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
});
|
||||
|
||||
const deferred = Promise.defer();
|
||||
client.on("sync", function() {
|
||||
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token");
|
||||
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
// .toEqual("s_5_4");
|
||||
}).done(() => deferred.resolve(),
|
||||
(e) => deferred.reject(e));
|
||||
const prom = new Promise((resolve, reject) => {
|
||||
client.on("sync", function() {
|
||||
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token");
|
||||
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
// .toEqual("s_5_4");
|
||||
}).done(resolve, reject);
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
deferred.promise,
|
||||
prom,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -669,11 +665,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) {
|
||||
console.log("sendTextMessage completed");
|
||||
logger.log("sendTextMessage completed");
|
||||
expect(res.event_id).toEqual(event.event_id);
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
console.log("getEventTimeline completed (2)");
|
||||
logger.log("getEventTimeline completed (2)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
|
||||
}),
|
||||
@@ -684,7 +680,7 @@ describe("MatrixClient event timelines", function() {
|
||||
]).then(function() {
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
console.log("getEventTimeline completed (1)");
|
||||
logger.log("getEventTimeline completed (1)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(event);
|
||||
|
||||
@@ -696,7 +692,7 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
|
||||
it("should handle gappy syncs after redactions", function(done) {
|
||||
it("should handle gappy syncs after redactions", function() {
|
||||
// https://github.com/vector-im/vector-web/issues/1389
|
||||
|
||||
// a state event, followed by a redaction thereof
|
||||
@@ -728,7 +724,7 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
Promise.all([
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
@@ -764,6 +760,6 @@ describe("MatrixClient event timelines", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,8 +9,6 @@ const Filter = publicGlobals.Filter;
|
||||
const utils = require("../test-utils");
|
||||
const MockStorageApi = require("../MockStorageApi");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
@@ -21,7 +19,6 @@ describe("MatrixClient", function() {
|
||||
const accessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
store = new MemoryStore();
|
||||
|
||||
@@ -46,9 +43,9 @@ describe("MatrixClient", function() {
|
||||
|
||||
describe("uploadContent", function() {
|
||||
const buf = new Buffer('hello world');
|
||||
it("should upload the file", function(done) {
|
||||
it("should upload the file", function() {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/v1/upload",
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
).check(function(req) {
|
||||
expect(req.rawData).toEqual(buf);
|
||||
expect(req.queryParams.filename).toEqual("hi.txt");
|
||||
@@ -74,25 +71,26 @@ describe("MatrixClient", function() {
|
||||
expect(uploads[0].promise).toBe(prom);
|
||||
expect(uploads[0].loaded).toEqual(0);
|
||||
|
||||
prom.then(function(response) {
|
||||
const prom2 = prom.then(function(response) {
|
||||
// for backwards compatibility, we return the raw JSON
|
||||
expect(response).toEqual("content");
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
return prom2;
|
||||
});
|
||||
|
||||
it("should parse the response if rawResponse=false", function(done) {
|
||||
it("should parse the response if rawResponse=false", function() {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/v1/upload",
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
).check(function(req) {
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
}).respond(200, { "content_uri": "uri" });
|
||||
|
||||
client.uploadContent({
|
||||
const prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
@@ -100,14 +98,15 @@ describe("MatrixClient", function() {
|
||||
rawResponse: false,
|
||||
}).then(function(response) {
|
||||
expect(response.content_uri).toEqual("uri");
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
return prom;
|
||||
});
|
||||
|
||||
it("should parse errors into a MatrixError", function(done) {
|
||||
it("should parse errors into a MatrixError", function() {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/v1/upload",
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
).check(function(req) {
|
||||
expect(req.rawData).toEqual(buf);
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
@@ -116,7 +115,7 @@ describe("MatrixClient", function() {
|
||||
"error": "broken",
|
||||
});
|
||||
|
||||
client.uploadContent({
|
||||
const prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
@@ -126,12 +125,13 @@ describe("MatrixClient", function() {
|
||||
expect(error.httpStatus).toEqual(400);
|
||||
expect(error.errcode).toEqual("M_SNAFU");
|
||||
expect(error.message).toEqual("broken");
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
return prom;
|
||||
});
|
||||
|
||||
it("should return a promise which can be cancelled", function(done) {
|
||||
it("should return a promise which can be cancelled", function() {
|
||||
const prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
@@ -143,17 +143,18 @@ describe("MatrixClient", function() {
|
||||
expect(uploads[0].promise).toBe(prom);
|
||||
expect(uploads[0].loaded).toEqual(0);
|
||||
|
||||
prom.then(function(response) {
|
||||
const prom2 = prom.then(function(response) {
|
||||
throw Error("request not aborted");
|
||||
}, function(error) {
|
||||
expect(error).toEqual("aborted");
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
const r = client.cancelUpload(prom);
|
||||
expect(r).toBe(true);
|
||||
return prom2;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -310,7 +311,7 @@ describe("MatrixClient", function() {
|
||||
return client.initCrypto();
|
||||
});
|
||||
|
||||
it("should do an HTTP request and then store the keys", function(done) {
|
||||
it("should do an HTTP request and then store the keys", function() {
|
||||
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
|
||||
// ed25519key = client.getDeviceEd25519Key();
|
||||
const borisKeys = {
|
||||
@@ -355,9 +356,9 @@ describe("MatrixClient", function() {
|
||||
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
|
||||
};
|
||||
|
||||
console.log("Ed25519: " + ed25519key);
|
||||
console.log("boris:", sign(borisKeys.dev1));
|
||||
console.log("chaz:", sign(chazKeys.dev2));
|
||||
logger.log("Ed25519: " + ed25519key);
|
||||
logger.log("boris:", sign(borisKeys.dev1));
|
||||
logger.log("chaz:", sign(chazKeys.dev2));
|
||||
*/
|
||||
|
||||
httpBackend.when("POST", "/keys/query").check(function(req) {
|
||||
@@ -372,7 +373,7 @@ describe("MatrixClient", function() {
|
||||
},
|
||||
});
|
||||
|
||||
client.downloadKeys(["boris", "chaz"]).then(function(res) {
|
||||
const prom = client.downloadKeys(["boris", "chaz"]).then(function(res) {
|
||||
assertObjectContains(res.boris.dev1, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
@@ -386,26 +387,26 @@ describe("MatrixClient", function() {
|
||||
algorithms: ["2"],
|
||||
unsigned: { "ghi": "def" },
|
||||
});
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteDevice", function() {
|
||||
const auth = {a: 1};
|
||||
it("should pass through an auth dict", function(done) {
|
||||
it("should pass through an auth dict", function() {
|
||||
httpBackend.when(
|
||||
"DELETE", "/_matrix/client/unstable/devices/my_device",
|
||||
"DELETE", "/_matrix/client/r0/devices/my_device",
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual({auth: auth});
|
||||
}).respond(200);
|
||||
|
||||
client.deleteDevice(
|
||||
"my_device", auth,
|
||||
).nodeify(done);
|
||||
const prom = client.deleteDevice("my_device", auth);
|
||||
|
||||
httpBackend.flush();
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ const MatrixClient = sdk.MatrixClient;
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
@@ -58,7 +57,6 @@ describe("MatrixClient opts", function() {
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
});
|
||||
|
||||
@@ -101,7 +99,7 @@ describe("MatrixClient opts", function() {
|
||||
"m.room.create",
|
||||
];
|
||||
client.on("event", function(event) {
|
||||
expect(expectedEventTypes.indexOf(event.getType())).toNotEqual(
|
||||
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
|
||||
-1, "Recv unexpected event type: " + event.getType(),
|
||||
);
|
||||
expectedEventTypes.splice(
|
||||
|
||||
@@ -4,11 +4,8 @@ import Promise from 'bluebird';
|
||||
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
const EventStatus = sdk.EventStatus;
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
@@ -20,7 +17,6 @@ describe("MatrixClient retrying", function() {
|
||||
let room;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
scheduler = new sdk.MatrixScheduler();
|
||||
|
||||
@@ -6,7 +6,6 @@ const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient room timelines", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
@@ -103,8 +102,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function(done) {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
beforeEach(function() {
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
@@ -122,9 +120,9 @@ describe("MatrixClient room timelines", function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
client.startClient();
|
||||
httpBackend.flush("/pushrules").then(function() {
|
||||
return httpBackend.flush("/pushrules").then(function() {
|
||||
return httpBackend.flush("/filter");
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
||||
@@ -6,7 +6,6 @@ const utils = require("../test-utils");
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
describe("MatrixClient syncing", function() {
|
||||
@@ -23,7 +22,6 @@ describe("MatrixClient syncing", function() {
|
||||
const roomTwo = "!bar:localhost";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
@@ -528,7 +526,7 @@ describe("MatrixClient syncing", function() {
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
expect(room).toExist();
|
||||
expect(room).toBeDefined();
|
||||
const tok = room.getLiveTimeline()
|
||||
.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
expect(tok).toEqual("roomtwotok");
|
||||
@@ -693,12 +691,12 @@ describe("MatrixClient syncing", function() {
|
||||
include_leave: true }});
|
||||
}).respond(200, { filter_id: "another_id" });
|
||||
|
||||
const defer = Promise.defer();
|
||||
|
||||
httpBackend.when("GET", "/sync").check(function(req) {
|
||||
expect(req.queryParams.filter).toEqual("another_id");
|
||||
defer.resolve();
|
||||
}).respond(200, {});
|
||||
const prom = new Promise((resolve) => {
|
||||
httpBackend.when("GET", "/sync").check(function(req) {
|
||||
expect(req.queryParams.filter).toEqual("another_id");
|
||||
resolve();
|
||||
}).respond(200, {});
|
||||
});
|
||||
|
||||
client.syncLeftRooms();
|
||||
|
||||
@@ -709,7 +707,7 @@ describe("MatrixClient syncing", function() {
|
||||
// flush the syncs
|
||||
return httpBackend.flushAllExpected();
|
||||
}),
|
||||
defer.promise,
|
||||
prom,
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ limitations under the License.
|
||||
|
||||
const anotherjson = require('another-json');
|
||||
import Promise from 'bluebird';
|
||||
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";
|
||||
|
||||
@@ -203,7 +203,7 @@ function getSyncResponse(roomMembers) {
|
||||
|
||||
describe("megolm", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('not running megolm tests: Olm not present');
|
||||
logger.warn('not running megolm tests: Olm not present');
|
||||
return;
|
||||
}
|
||||
const Olm = global.Olm;
|
||||
@@ -282,8 +282,6 @@ describe("megolm", function() {
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||
);
|
||||
@@ -416,7 +414,7 @@ describe("megolm", function() {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
console.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
@@ -555,7 +553,7 @@ describe("megolm", function() {
|
||||
).respond(200, function(path, content) {
|
||||
const ct = content.ciphertext;
|
||||
const r = inboundGroupSession.decrypt(ct);
|
||||
console.log('Decrypted received megolm message', r);
|
||||
logger.log('Decrypted received megolm message', r);
|
||||
|
||||
expect(r.message_index).toEqual(0);
|
||||
const decrypted = JSON.parse(r.plaintext);
|
||||
@@ -600,7 +598,7 @@ describe("megolm", function() {
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
console.log('Forcing alice to download our device keys');
|
||||
logger.log('Forcing alice to download our device keys');
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
@@ -611,10 +609,10 @@ describe("megolm", function() {
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
console.log('Telling alice to block our device');
|
||||
logger.log('Telling alice to block our device');
|
||||
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
console.log('Telling alice to send a megolm message');
|
||||
logger.log('Telling alice to send a megolm message');
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, {
|
||||
@@ -656,7 +654,7 @@ describe("megolm", function() {
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
console.log("Fetching bob's devices and marking known");
|
||||
logger.log("Fetching bob's devices and marking known");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
@@ -669,17 +667,17 @@ describe("megolm", function() {
|
||||
aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
|
||||
});
|
||||
}).then(function() {
|
||||
console.log('Telling alice to send a megolm message');
|
||||
logger.log('Telling alice to send a megolm message');
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(path, content) {
|
||||
console.log('sendToDevice: ', content);
|
||||
logger.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));
|
||||
console.log('decrypted sendToDevice:', decrypted);
|
||||
logger.log('decrypted sendToDevice:', decrypted);
|
||||
expect(decrypted.type).toEqual('m.room_key');
|
||||
megolmSessionId = decrypted.content.session_id;
|
||||
return {};
|
||||
@@ -688,7 +686,7 @@ describe("megolm", function() {
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(path, content) {
|
||||
console.log('/send:', content);
|
||||
logger.log('/send:', content);
|
||||
expect(content.session_id).toEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
@@ -704,15 +702,15 @@ describe("megolm", function() {
|
||||
}),
|
||||
]);
|
||||
}).then(function() {
|
||||
console.log('Telling alice to block our device');
|
||||
logger.log('Telling alice to block our device');
|
||||
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
console.log('Telling alice to send another megolm message');
|
||||
logger.log('Telling alice to send another megolm message');
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(path, content) {
|
||||
console.log('/send:', content);
|
||||
expect(content.session_id).toNotEqual(megolmSessionId);
|
||||
logger.log('/send:', content);
|
||||
expect(content.session_id).not.toEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
@@ -792,7 +790,7 @@ describe("megolm", function() {
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(path, content) {
|
||||
console.log("sendToDevice: ", content);
|
||||
logger.log("sendToDevice: ", content);
|
||||
const m = content.messages[aliceTestClient.userId].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
@@ -812,7 +810,7 @@ describe("megolm", function() {
|
||||
).respond(200, function(path, content) {
|
||||
const ct = content.ciphertext;
|
||||
const r = inboundGroupSession.decrypt(ct);
|
||||
console.log('Decrypted received megolm message', r);
|
||||
logger.log('Decrypted received megolm message', r);
|
||||
decrypted = JSON.parse(r.plaintext);
|
||||
|
||||
return {
|
||||
@@ -865,7 +863,7 @@ describe("megolm", function() {
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
// this will block
|
||||
console.log('Forcing alice to download our device keys');
|
||||
logger.log('Forcing alice to download our device keys');
|
||||
downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
|
||||
// so will this.
|
||||
|
||||
+4
-3
@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// try to load the olm library.
|
||||
import logger from '../src/logger';
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
console.log('loaded libolm');
|
||||
logger.log('loaded libolm');
|
||||
} catch (e) {
|
||||
console.warn("unable to run crypto tests: libolm not available");
|
||||
logger.warn("unable to run crypto tests: libolm not available");
|
||||
}
|
||||
|
||||
+146
-17
@@ -1,10 +1,10 @@
|
||||
"use strict";
|
||||
import expect from 'expect';
|
||||
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;
|
||||
|
||||
@@ -25,7 +25,7 @@ module.exports.syncPromise = function(client, count) {
|
||||
|
||||
const p = new Promise((resolve, reject) => {
|
||||
const cb = (state) => {
|
||||
console.log(`${Date.now()} syncPromise(${count}): ${state}`);
|
||||
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
|
||||
if (state == 'SYNCING') {
|
||||
resolve();
|
||||
} else {
|
||||
@@ -40,18 +40,6 @@ module.exports.syncPromise = function(client, count) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform common actions before each test case, e.g. printing the test case
|
||||
* name to stdout.
|
||||
* @param {Mocha.Context} context The test context
|
||||
*/
|
||||
module.exports.beforeEach = function(context) {
|
||||
const desc = context.currentTest.fullTitle();
|
||||
|
||||
console.log(desc);
|
||||
console.log(new Array(1 + desc.length).join("="));
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a spy for an object and automatically spy its methods.
|
||||
* @param {*} constr The class constructor (used with 'new')
|
||||
@@ -70,7 +58,7 @@ module.exports.mock = function(constr, name) {
|
||||
for (const key in constr.prototype) { // eslint-disable-line guard-for-in
|
||||
try {
|
||||
if (constr.prototype[key] instanceof Function) {
|
||||
result[key] = expect.createSpy();
|
||||
result[key] = jest.fn();
|
||||
}
|
||||
} catch (ex) {
|
||||
// Direct access to some non-function fields of DOM prototypes may
|
||||
@@ -232,12 +220,153 @@ module.exports.awaitDecryption = function(event) {
|
||||
return Promise.resolve(event);
|
||||
}
|
||||
|
||||
console.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
console.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const HttpResponse = module.exports.HttpResponse = function(
|
||||
httpLookups, acceptKeepalives, ignoreUnhandledSync,
|
||||
) {
|
||||
this.httpLookups = httpLookups;
|
||||
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
|
||||
this.ignoreUnhandledSync = ignoreUnhandledSync;
|
||||
this.pendingLookup = null;
|
||||
};
|
||||
|
||||
HttpResponse.prototype.request = function HttpResponse(
|
||||
cb, method, path, qp, data, prefix,
|
||||
) {
|
||||
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const next = this.httpLookups.shift();
|
||||
const logLine = (
|
||||
"MatrixClient[UT] RECV " + method + " " + path + " " +
|
||||
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
|
||||
);
|
||||
logger.log(logLine);
|
||||
|
||||
if (!next) { // no more things to return
|
||||
if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
|
||||
logger.log("MatrixClient[UT] Ignoring.");
|
||||
return Promise.defer().promise;
|
||||
}
|
||||
if (this.pendingLookup) {
|
||||
if (this.pendingLookup.method === method
|
||||
&& this.pendingLookup.path === path) {
|
||||
return this.pendingLookup.promise;
|
||||
}
|
||||
// >1 pending thing, and they are different, whine.
|
||||
expect(false).toBe(
|
||||
true, ">1 pending request. You should probably handle them. " +
|
||||
"PENDING: " + JSON.stringify(this.pendingLookup) + " JUST GOT: " +
|
||||
method + " " + path,
|
||||
);
|
||||
}
|
||||
this.pendingLookup = {
|
||||
promise: Promise.defer().promise,
|
||||
method: method,
|
||||
path: path,
|
||||
};
|
||||
return this.pendingLookup.promise;
|
||||
}
|
||||
if (next.path === path && next.method === method) {
|
||||
logger.log(
|
||||
"MatrixClient[UT] Matched. Returning " +
|
||||
(next.error ? "BAD" : "GOOD") + " response",
|
||||
);
|
||||
if (next.expectBody) {
|
||||
expect(next.expectBody).toEqual(data);
|
||||
}
|
||||
if (next.expectQueryParams) {
|
||||
Object.keys(next.expectQueryParams).forEach(function(k) {
|
||||
expect(qp[k]).toEqual(next.expectQueryParams[k]);
|
||||
});
|
||||
}
|
||||
|
||||
if (next.thenCall) {
|
||||
process.nextTick(next.thenCall, 0); // next tick so we return first.
|
||||
}
|
||||
|
||||
if (next.error) {
|
||||
return Promise.reject({
|
||||
errcode: next.error.errcode,
|
||||
httpStatus: next.error.httpStatus,
|
||||
name: next.error.errcode,
|
||||
message: "Expected testing error",
|
||||
data: next.error,
|
||||
});
|
||||
}
|
||||
return Promise.resolve(next.data);
|
||||
} else if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
|
||||
logger.log("MatrixClient[UT] Ignoring.");
|
||||
this.httpLookups.unshift(next);
|
||||
return Promise.defer().promise;
|
||||
}
|
||||
expect(true).toBe(false, "Expected different request. " + logLine);
|
||||
return Promise.defer().promise;
|
||||
};
|
||||
|
||||
HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions";
|
||||
|
||||
HttpResponse.PUSH_RULES_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/pushrules/",
|
||||
data: {},
|
||||
};
|
||||
|
||||
HttpResponse.USER_ID = "@alice:bar";
|
||||
|
||||
HttpResponse.filterResponse = function(userId) {
|
||||
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
|
||||
return {
|
||||
method: "POST",
|
||||
path: filterPath,
|
||||
data: { filter_id: "f1lt3r" },
|
||||
};
|
||||
};
|
||||
|
||||
HttpResponse.SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: { events: [] },
|
||||
rooms: {},
|
||||
};
|
||||
|
||||
HttpResponse.SYNC_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
data: HttpResponse.SYNC_DATA,
|
||||
};
|
||||
|
||||
HttpResponse.defaultResponses = function(userId) {
|
||||
return [
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
HttpResponse.filterResponse(userId),
|
||||
HttpResponse.SYNC_RESPONSE,
|
||||
];
|
||||
};
|
||||
|
||||
module.exports.setHttpResponses = function setHttpResponses(
|
||||
client, responses, acceptKeepalives, ignoreUnhandledSyncs,
|
||||
) {
|
||||
const httpResponseObj = new HttpResponse(
|
||||
responses, acceptKeepalives, ignoreUnhandledSyncs,
|
||||
);
|
||||
|
||||
const httpReq = httpResponseObj.request.bind(httpResponseObj);
|
||||
client._http = [
|
||||
"authedRequest", "authedRequestWithPrefix", "getContentUri",
|
||||
"request", "requestWithPrefix", "uploadContent",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
client._http.authedRequest.mockImplementation(httpReq);
|
||||
client._http.authedRequestWithPrefix.mockImplementation(httpReq);
|
||||
client._http.requestWithPrefix.mockImplementation(httpReq);
|
||||
client._http.request.mockImplementation(httpReq);
|
||||
};
|
||||
|
||||
@@ -18,11 +18,9 @@ limitations under the License.
|
||||
import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
const sdk = require("../..");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
const AutoDiscovery = sdk.AutoDiscovery;
|
||||
|
||||
import expect from 'expect';
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
|
||||
@@ -30,7 +28,6 @@ describe("AutoDiscovery", function() {
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new MockHttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
@@ -275,7 +272,7 @@ describe("AutoDiscovery", function() {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: null,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
@@ -304,7 +301,7 @@ describe("AutoDiscovery", function() {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: null,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
@@ -335,7 +332,7 @@ describe("AutoDiscovery", function() {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: null,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
@@ -416,8 +413,8 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when the identity server configuration is wrong " +
|
||||
"(missing base_url)", function() {
|
||||
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (missing base_url)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
@@ -438,14 +435,14 @@ describe("AutoDiscovery", function() {
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
@@ -456,8 +453,8 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when the identity server configuration is wrong " +
|
||||
"(empty base_url)", function() {
|
||||
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (empty base_url)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
@@ -478,14 +475,14 @@ describe("AutoDiscovery", function() {
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
@@ -496,8 +493,8 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when the identity server configuration is wrong " +
|
||||
"(validation error: 404)", function() {
|
||||
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (validation error: 404)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
@@ -519,16 +516,16 @@ describe("AutoDiscovery", function() {
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
base_url: null,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -537,8 +534,8 @@ describe("AutoDiscovery", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when the identity server configuration is wrong " +
|
||||
"(validation error: 500)", function() {
|
||||
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
|
||||
"is wrong (validation error: 500)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
@@ -560,16 +557,16 @@ describe("AutoDiscovery", function() {
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
base_url: null,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const ContentRepo = require("../../lib/content-repo");
|
||||
const testUtils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
const baseUrl = "https://my.home.server";
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("getHttpUriForMxc", function() {
|
||||
it("should do nothing to HTTP URLs when allowing direct links", function() {
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
@@ -31,7 +24,7 @@ describe("ContentRepo", function() {
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/download/server.name/resourceid",
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -43,7 +36,7 @@ describe("ContentRepo", function() {
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
|
||||
"?width=32&height=64&method=crop",
|
||||
);
|
||||
});
|
||||
@@ -52,7 +45,7 @@ describe("ContentRepo", function() {
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
|
||||
"?width=32#automade",
|
||||
);
|
||||
});
|
||||
@@ -61,7 +54,7 @@ describe("ContentRepo", function() {
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade",
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -73,21 +66,21 @@ describe("ContentRepo", function() {
|
||||
|
||||
it("should set w/h by default to 96", function() {
|
||||
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/identicon/foobar" +
|
||||
baseUrl + "/_matrix/media/unstable/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/v1/identicon/foobar" +
|
||||
baseUrl + "/_matrix/media/unstable/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/v1/identicon/foo%23bar" +
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
|
||||
+41
-46
@@ -3,7 +3,6 @@ import 'source-map-support/register';
|
||||
import '../olm-loader';
|
||||
|
||||
import Crypto from '../../lib/crypto';
|
||||
import expect from 'expect';
|
||||
|
||||
import WebStorageSessionStore from '../../lib/store/session/webstorage';
|
||||
import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js';
|
||||
@@ -12,7 +11,6 @@ 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;
|
||||
|
||||
@@ -20,13 +18,15 @@ const sdk = require("../..");
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("Crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(function(done) {
|
||||
Olm.init().then(done);
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
it("Crypto exposes the correct olm library version", function() {
|
||||
@@ -76,9 +76,9 @@ describe("Crypto", function() {
|
||||
});
|
||||
|
||||
mockBaseApis = {
|
||||
sendToDevice: expect.createSpy(),
|
||||
getKeyBackupVersion: expect.createSpy(),
|
||||
isGuest: expect.createSpy(),
|
||||
sendToDevice: jest.fn(),
|
||||
getKeyBackupVersion: jest.fn(),
|
||||
isGuest: jest.fn(),
|
||||
};
|
||||
mockRoomList = {};
|
||||
|
||||
@@ -110,15 +110,16 @@ describe("Crypto", function() {
|
||||
});
|
||||
|
||||
fakeEmitter.emit('toDeviceEvent', {
|
||||
getType: expect.createSpy().andReturn('m.room.message'),
|
||||
getContent: expect.createSpy().andReturn({
|
||||
getId: jest.fn().mockReturnValue("$wedged"),
|
||||
getType: jest.fn().mockReturnValue('m.room.message'),
|
||||
getContent: jest.fn().mockReturnValue({
|
||||
msgtype: 'm.bad.encrypted',
|
||||
}),
|
||||
getWireContent: expect.createSpy().andReturn({
|
||||
getWireContent: jest.fn().mockReturnValue({
|
||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||
sender_key: 'this is a key',
|
||||
}),
|
||||
getSender: expect.createSpy().andReturn('@bob:home.server'),
|
||||
getSender: jest.fn().mockReturnValue('@bob:home.server'),
|
||||
});
|
||||
|
||||
await prom;
|
||||
@@ -245,7 +246,7 @@ describe("Crypto", function() {
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).toNotBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
|
||||
const cryptoStore = bobClient._cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
@@ -260,7 +261,7 @@ describe("Crypto", function() {
|
||||
// the room key request should still be there, since we haven't
|
||||
// decrypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toExist();
|
||||
.toBeDefined();
|
||||
|
||||
// keyshare the session key starting at the first message, so
|
||||
// that it can now be decrypted
|
||||
@@ -268,10 +269,10 @@ describe("Crypto", function() {
|
||||
ksEvent = await keyshareEventForEvent(events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).toNotBe("m.bad.encrypted");
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
// the room key request should be gone since we've now decypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toNotExist();
|
||||
.toBeFalsy();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -296,7 +297,7 @@ describe("Crypto", function() {
|
||||
sender_key: "senderkey",
|
||||
};
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toExist();
|
||||
.toBeDefined();
|
||||
});
|
||||
|
||||
it("uses a new txnid for re-requesting keys", async function() {
|
||||
@@ -329,38 +330,32 @@ describe("Crypto", function() {
|
||||
|
||||
aliceClient.startClient();
|
||||
|
||||
const clock = lolex.install();
|
||||
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);
|
||||
jest.runAllTimers();
|
||||
let args = await promise;
|
||||
const txnId = args[2];
|
||||
jest.runAllTimers();
|
||||
|
||||
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();
|
||||
|
||||
// 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();
|
||||
}
|
||||
// cancel and resend the room key request
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
jest.runAllTimers();
|
||||
// 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());
|
||||
jest.runAllTimers();
|
||||
args = await promise;
|
||||
jest.runAllTimers();
|
||||
expect(args[2]).not.toBe(txnId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,10 +17,9 @@ limitations under the License.
|
||||
|
||||
import DeviceList from '../../../lib/crypto/DeviceList';
|
||||
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';
|
||||
|
||||
const signedDeviceList = {
|
||||
@@ -59,11 +58,9 @@ describe('DeviceList', function() {
|
||||
let deviceLists = [];
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
deviceLists = [];
|
||||
|
||||
downloadSpy = expect.createSpy();
|
||||
downloadSpy = jest.fn();
|
||||
cryptoStore = new MemoryCryptoStore();
|
||||
});
|
||||
|
||||
@@ -91,7 +88,7 @@ describe('DeviceList', function() {
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = Promise.defer();
|
||||
downloadSpy.andReturn(queryDefer1.promise);
|
||||
downloadSpy.mockReturnValue(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
@@ -110,15 +107,15 @@ describe('DeviceList', function() {
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = Promise.defer();
|
||||
downloadSpy.andReturn(queryDefer1.promise);
|
||||
downloadSpy.mockReturnValue(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
downloadSpy.reset();
|
||||
downloadSpy.mockReset();
|
||||
|
||||
// outdated notif arrives while the request is in flight.
|
||||
const queryDefer2 = Promise.defer();
|
||||
downloadSpy.andReturn(queryDefer2.promise);
|
||||
downloadSpy.mockReturnValue(queryDefer2.promise);
|
||||
|
||||
dl.invalidateUserDeviceList('@test1:sw1v.org');
|
||||
dl.refreshOutdatedDeviceLists();
|
||||
@@ -134,11 +131,11 @@ describe('DeviceList', function() {
|
||||
}).then(() => {
|
||||
// uh-oh; user restarts before second request completes. The new instance
|
||||
// should know we never got a complete device list.
|
||||
console.log("Creating new devicelist to simulate app reload");
|
||||
downloadSpy.reset();
|
||||
logger.log("Creating new devicelist to simulate app reload");
|
||||
downloadSpy.mockReset();
|
||||
const dl2 = createTestDeviceList();
|
||||
const queryDefer3 = Promise.defer();
|
||||
downloadSpy.andReturn(queryDefer3.promise);
|
||||
downloadSpy.mockReturnValue(queryDefer3.promise);
|
||||
|
||||
const prom3 = dl2.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import '../../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import sdk from '../../../..';
|
||||
@@ -10,6 +9,7 @@ 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'];
|
||||
@@ -21,20 +21,20 @@ const Olm = global.Olm;
|
||||
|
||||
describe("MegolmDecryption", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm unit tests: libolm not present');
|
||||
logger.warn('Not running megolm unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
let megolmDecryption;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
let mockBaseApis;
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
await Olm.init();
|
||||
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockBaseApis = {};
|
||||
|
||||
@@ -54,9 +54,9 @@ describe("MegolmDecryption", function() {
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
|
||||
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
|
||||
mockOlmLib.encryptMessageForDevice =
|
||||
expect.createSpy().andReturn(Promise.resolve());
|
||||
jest.fn().mockReturnValue(Promise.resolve());
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
});
|
||||
|
||||
@@ -134,22 +134,22 @@ describe("MegolmDecryption", function() {
|
||||
|
||||
// set up some pre-conditions for the share call
|
||||
const deviceInfo = {};
|
||||
mockCrypto.getStoredDevice.andReturn(deviceInfo);
|
||||
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
|
||||
|
||||
mockOlmLib.ensureOlmSessionsForDevices.andReturn(
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockReturnValue(
|
||||
Promise.resolve({'@alice:foo': {'alidevice': {
|
||||
sessionId: 'alisession',
|
||||
}}}),
|
||||
);
|
||||
|
||||
const awaitEncryptForDevice = new Promise((res, rej) => {
|
||||
mockOlmLib.encryptMessageForDevice.andCall(() => {
|
||||
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
|
||||
res();
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
mockBaseApis.sendToDevice = expect.createSpy();
|
||||
mockBaseApis.sendToDevice = jest.fn();
|
||||
|
||||
// do the share
|
||||
megolmDecryption.shareKeysWithDevice(keyRequest);
|
||||
@@ -159,21 +159,20 @@ describe("MegolmDecryption", function() {
|
||||
}).then(() => {
|
||||
// check that it called encryptMessageForDevice with
|
||||
// appropriate args.
|
||||
expect(mockOlmLib.encryptMessageForDevice.calls.length)
|
||||
.toEqual(1);
|
||||
expect(mockOlmLib.encryptMessageForDevice).toBeCalledTimes(1);
|
||||
|
||||
const call = mockOlmLib.encryptMessageForDevice.calls[0];
|
||||
const payload = call.arguments[6];
|
||||
const call = mockOlmLib.encryptMessageForDevice.mock.calls[0];
|
||||
const payload = call[6];
|
||||
|
||||
expect(payload.type).toEqual("m.forwarded_room_key");
|
||||
expect(payload.content).toInclude({
|
||||
expect(payload.content).toMatchObject({
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
sender_claimed_ed25519_key: "SENDER_ED25519",
|
||||
session_id: groupSession.session_id(),
|
||||
chain_index: 0,
|
||||
forwarding_curve25519_key_chain: [],
|
||||
});
|
||||
expect(payload.content.session_key).toExist();
|
||||
expect(payload.content.session_key).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -200,13 +199,12 @@ describe("MegolmDecryption", function() {
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
const successHandler = expect.createSpy();
|
||||
const failureHandler = expect.createSpy()
|
||||
.andCall((err) => {
|
||||
expect(err.toString()).toMatch(
|
||||
/Duplicate message index, possible replay attack/,
|
||||
);
|
||||
});
|
||||
const successHandler = jest.fn();
|
||||
const failureHandler = jest.fn((err) => {
|
||||
expect(err.toString()).toMatch(
|
||||
/Duplicate message index, possible replay attack/,
|
||||
);
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event1).then((res) => {
|
||||
const event2 = new MatrixEvent({
|
||||
@@ -227,7 +225,7 @@ describe("MegolmDecryption", function() {
|
||||
successHandler,
|
||||
failureHandler,
|
||||
).then(() => {
|
||||
expect(successHandler).toNotHaveBeenCalled();
|
||||
expect(successHandler).not.toHaveBeenCalled();
|
||||
expect(failureHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -265,10 +263,10 @@ describe("MegolmDecryption", function() {
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
olmDevice.verifySignature = expect.createSpy();
|
||||
olmDevice.verifySignature = jest.fn();
|
||||
await olmDevice.init();
|
||||
|
||||
mockBaseApis.claimOneTimeKeys = expect.createSpy().andReturn(Promise.resolve({
|
||||
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
@@ -284,18 +282,18 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
},
|
||||
}));
|
||||
mockBaseApis.sendToDevice = expect.createSpy().andReturn(Promise.resolve());
|
||||
mockBaseApis.sendToDevice = jest.fn().mockReturnValue(Promise.resolve());
|
||||
|
||||
mockCrypto.downloadKeys.andReturn(Promise.resolve({
|
||||
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
deviceId: 'aliceDevice',
|
||||
isBlocked: expect.createSpy().andReturn(false),
|
||||
isUnverified: expect.createSpy().andReturn(false),
|
||||
getIdentityKey: expect.createSpy().andReturn(
|
||||
isBlocked: jest.fn().mockReturnValue(false),
|
||||
isUnverified: jest.fn().mockReturnValue(false),
|
||||
getIdentityKey: jest.fn().mockReturnValue(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
|
||||
),
|
||||
getFingerprint: expect.createSpy().andReturn(''),
|
||||
getFingerprint: jest.fn().mockReturnValue(''),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -311,10 +309,10 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
});
|
||||
const mockRoom = {
|
||||
getEncryptionTargetMembers: expect.createSpy().andReturn(
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
||||
[{userId: "@alice:home.server"}],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: expect.createSpy().andReturn(false),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
@@ -322,25 +320,25 @@ describe("MegolmDecryption", function() {
|
||||
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||
|
||||
// this should have claimed a key for alice as it's starting a new session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
||||
);
|
||||
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||
['@alice:home.server'], false,
|
||||
);
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
||||
);
|
||||
|
||||
mockBaseApis.claimOneTimeKeys.reset();
|
||||
mockBaseApis.claimOneTimeKeys.mockReset();
|
||||
|
||||
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some more text",
|
||||
});
|
||||
|
||||
// this should *not* have claimed a key as it should be using the same session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toNotHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
|
||||
|
||||
// likewise they should show the same session ID
|
||||
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||
|
||||
@@ -16,10 +16,9 @@ limitations under the License.
|
||||
|
||||
import '../../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
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';
|
||||
@@ -45,18 +44,18 @@ async function setupSession(initiator, opponent) {
|
||||
|
||||
describe("OlmDecryption", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm unit tests: libolm not present');
|
||||
logger.warn('Not running megolm unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
let aliceOlmDevice;
|
||||
let bobOlmDevice;
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
await global.Olm.init();
|
||||
|
||||
aliceOlmDevice = makeOlmDevice();
|
||||
bobOlmDevice = makeOlmDevice();
|
||||
await aliceOlmDevice.init();
|
||||
|
||||
+102
-24
@@ -16,7 +16,6 @@ limitations under the License.
|
||||
|
||||
import '../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import sdk from '../../..';
|
||||
@@ -28,6 +27,8 @@ import testUtils from '../../test-utils';
|
||||
|
||||
import OlmDevice from '../../../lib/crypto/OlmDevice';
|
||||
import Crypto from '../../../lib/crypto';
|
||||
import logger from '../../../src/logger';
|
||||
import olmlib from '../../../lib/crypto/olmlib';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -82,20 +83,30 @@ const BACKUP_INFO = {
|
||||
},
|
||||
};
|
||||
|
||||
const keys = {};
|
||||
|
||||
function getCrossSigningKey(type) {
|
||||
return keys[type];
|
||||
}
|
||||
|
||||
function saveCrossSigningKeys(k) {
|
||||
Object.assign(keys, k);
|
||||
}
|
||||
|
||||
function makeTestClient(sessionStore, cryptoStore) {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
const store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
|
||||
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
|
||||
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
return new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
@@ -107,15 +118,20 @@ function makeTestClient(sessionStore, cryptoStore) {
|
||||
deviceId: "device",
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: cryptoStore,
|
||||
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
|
||||
});
|
||||
}
|
||||
|
||||
describe("MegolmBackup", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
let olmDevice;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
@@ -124,9 +140,6 @@ describe("MegolmBackup", function() {
|
||||
let cryptoStore;
|
||||
let megolmDecryption;
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockCrypto.backupKey = new Olm.PkEncryption();
|
||||
mockCrypto.backupKey.set_recipient_key(
|
||||
@@ -142,9 +155,9 @@ describe("MegolmBackup", function() {
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
|
||||
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
|
||||
mockOlmLib.encryptMessageForDevice =
|
||||
expect.createSpy().andReturn(Promise.resolve());
|
||||
jest.fn().mockReturnValue(Promise.resolve());
|
||||
});
|
||||
|
||||
describe("backup", function() {
|
||||
@@ -205,7 +218,7 @@ describe("MegolmBackup", function() {
|
||||
};
|
||||
mockCrypto.cancelRoomKeyRequest = function() {};
|
||||
|
||||
mockCrypto.backupGroupSession = expect.createSpy();
|
||||
mockCrypto.backupGroupSession = jest.fn();
|
||||
|
||||
return event.attemptDecryption(mockCrypto).then(() => {
|
||||
return megolmDecryption.onRoomKeyEvent(event);
|
||||
@@ -266,7 +279,7 @@ describe("MegolmBackup", function() {
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqualTo(1);
|
||||
expect(numCalls).toBeLessThanOrEqual(1);
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
@@ -275,8 +288,8 @@ describe("MegolmBackup", function() {
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(data.rooms[ROOM_ID].sessions).toExist();
|
||||
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
@@ -295,6 +308,71 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('signs backups with the cross-signing master key', async function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
ibGroupSession.create(groupSession.session_key());
|
||||
|
||||
const client = makeTestClient(sessionStore, cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
await client.initCrypto();
|
||||
let privateKeys;
|
||||
client.uploadDeviceSigningKeys = async function(e) {return;};
|
||||
client.uploadKeySignatures = async function(e) {return;};
|
||||
client.on("crossSigning.saveCrossSigningKeys", function(e) {
|
||||
privateKeys = e;
|
||||
});
|
||||
client.on("crossSigning.getKey", function(e) {
|
||||
e.done(privateKeys[e.type]);
|
||||
});
|
||||
await client.resetCrossSigningKeys();
|
||||
let numCalls = 0;
|
||||
await new Promise(async (resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(1);
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("POST");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
try {
|
||||
// make sure auth_data is signed by the master key
|
||||
olmlib.pkVerify(
|
||||
data.auth_data, client.getCrossSigningId(), "@alice:bar",
|
||||
);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
return Promise.resolve({});
|
||||
}
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
await client.createKeyBackupVersion({
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(numCalls).toBe(1);
|
||||
});
|
||||
|
||||
it('retries when a backup fails', function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
@@ -304,16 +382,16 @@ describe("MegolmBackup", function() {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
const store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
|
||||
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
|
||||
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
const client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
@@ -371,7 +449,7 @@ describe("MegolmBackup", function() {
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqualTo(2);
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
if (numCalls >= 3) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
@@ -380,8 +458,8 @@ describe("MegolmBackup", function() {
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(data.rooms[ROOM_ID].sessions).toExist();
|
||||
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
if (numCalls > 1) {
|
||||
|
||||
@@ -0,0 +1,798 @@
|
||||
/*
|
||||
Copyright 2019 New Vector 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.
|
||||
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 '../../olm-loader';
|
||||
|
||||
import anotherjson from 'another-json';
|
||||
|
||||
import olmlib from '../../../lib/crypto/olmlib';
|
||||
|
||||
import TestClient from '../../TestClient';
|
||||
|
||||
import {HttpResponse, setHttpResponses} from '../../test-utils';
|
||||
|
||||
async function makeTestClient(userInfo, options, keys) {
|
||||
if (!keys) keys = {};
|
||||
|
||||
function getCrossSigningKey(type) {
|
||||
return keys[type];
|
||||
}
|
||||
|
||||
function saveCrossSigningKeys(k) {
|
||||
Object.assign(keys, k);
|
||||
}
|
||||
|
||||
if (!options) options = {};
|
||||
options.cryptoCallbacks = Object.assign(
|
||||
{}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {},
|
||||
);
|
||||
const client = (new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
|
||||
)).client;
|
||||
|
||||
await client.initCrypto();
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("Cross Signing", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
it("should sign the master key with the device key", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
|
||||
await olmlib.verifySignature(
|
||||
alice._crypto._olmDevice, keys.master_key, "@alice:example.com",
|
||||
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
|
||||
);
|
||||
});
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
// Alice downloads Bob's device key
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:bobs+master+pubkey": "bobs+master+pubkey",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// Alice verifies Bob's key
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
alice.uploadKeySignatures = (...args) => {
|
||||
resolve(...args);
|
||||
};
|
||||
});
|
||||
await alice.setDeviceVerified("@bob:example.com", "bobs+master+pubkey", true);
|
||||
// Alice should send a signature of Bob's key to the server
|
||||
await promise;
|
||||
});
|
||||
|
||||
it("should get cross-signing keys from sync", async function() {
|
||||
const masterKey = new Uint8Array([
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
|
||||
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
|
||||
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
|
||||
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
|
||||
]);
|
||||
const selfSigningKey = new Uint8Array([
|
||||
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
|
||||
0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0,
|
||||
0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49,
|
||||
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
|
||||
]);
|
||||
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
// will be called to sign our own device
|
||||
getCrossSigningKey: type => {
|
||||
if (type === 'master') {
|
||||
return masterKey;
|
||||
} else {
|
||||
return selfSigningKey;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const keyChangePromise = new Promise((resolve, reject) => {
|
||||
alice.once("crossSigning.keysChanged", async (e) => {
|
||||
resolve(e);
|
||||
await alice.checkOwnCrossSigningTrust();
|
||||
});
|
||||
});
|
||||
|
||||
const uploadSigsPromise = new Promise((resolve, reject) => {
|
||||
alice.uploadKeySignatures = jest.fn(async (content) => {
|
||||
await olmlib.verifySignature(
|
||||
alice._crypto._olmDevice,
|
||||
content["@alice:example.com"][
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
|
||||
],
|
||||
"@alice:example.com",
|
||||
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
|
||||
);
|
||||
olmlib.pkVerify(
|
||||
content["@alice:example.com"]["Osborne2"],
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
"@alice:example.com",
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
};
|
||||
aliceDevice.keys = deviceInfo.keys;
|
||||
aliceDevice.algorithms = deviceInfo.algorithms;
|
||||
await alice._crypto._signObject(aliceDevice);
|
||||
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
|
||||
|
||||
// feed sync result that includes master key, ssk, device key
|
||||
const responses = [
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
signed_curve25519: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
HttpResponse.filterResponse("@alice:example.com"),
|
||||
{
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
data: {
|
||||
next_batch: "abcdefg",
|
||||
device_lists: {
|
||||
changed: [
|
||||
"@alice:example.com",
|
||||
"@bob:example.com",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/query",
|
||||
data: {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@alice:example.com": {
|
||||
"Osborne2": aliceDevice,
|
||||
},
|
||||
},
|
||||
"master_keys": {
|
||||
"@alice:example.com": {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
|
||||
},
|
||||
},
|
||||
},
|
||||
"self_signing_keys": {
|
||||
"@alice:example.com": {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self-signing"],
|
||||
keys: {
|
||||
"ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ":
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
},
|
||||
signatures: {
|
||||
"@alice:example.com": {
|
||||
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
|
||||
"Wqx/HXR851KIi8/u/UX+fbAMtq9Uj8sr8FsOcqrLfVYa6lAmbXs"
|
||||
+ "Vhfy4AlZ3dnEtjgZx0U0QDrghEn2eYBeOCA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
signed_curve25519: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
setHttpResponses(alice, responses, true, true);
|
||||
|
||||
await alice.startClient();
|
||||
|
||||
// once ssk is confirmed, device key should be trusted
|
||||
await keyChangePromise;
|
||||
await uploadSigsPromise;
|
||||
|
||||
const aliceTrust = alice.checkUserTrust("@alice:example.com");
|
||||
expect(aliceTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(aliceTrust.isTofu()).toBeTruthy();
|
||||
expect(aliceTrust.isVerified()).toBeTruthy();
|
||||
|
||||
const aliceDeviceTrust = alice.checkDeviceTrust("@alice:example.com", "Osborne2");
|
||||
expect(aliceDeviceTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isTofu()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should use trust chain to determine device verification", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
// Alice downloads Bob's ssk and device key
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
["ed25519:" + bobPubkey]: bobPubkey,
|
||||
},
|
||||
};
|
||||
const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK));
|
||||
bobSSK.signatures = {
|
||||
"@bob:example.com": {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
["ed25519:" + bobMasterPubkey]: bobMasterPubkey,
|
||||
},
|
||||
},
|
||||
self_signing: bobSSK,
|
||||
},
|
||||
firstUse: 1,
|
||||
unsigned: {},
|
||||
});
|
||||
const bobDevice = {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "Dynabook",
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": "somePubkey",
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
const sig = bobSigning.sign(anotherjson.stringify(bobDevice));
|
||||
bobDevice.signatures = {
|
||||
"@bob:example.com": {
|
||||
["ed25519:" + bobPubkey]: sig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Bob's device key should be TOFU
|
||||
const bobTrust = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust.isVerified()).toBeFalsy();
|
||||
expect(bobTrust.isTofu()).toBeTruthy();
|
||||
|
||||
const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust.isVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust.isTofu()).toBeTruthy();
|
||||
|
||||
// Alice verifies Bob's SSK
|
||||
alice.uploadKeySignatures = () => {};
|
||||
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
|
||||
|
||||
// Bob's device key should be trusted
|
||||
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust2.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobTrust2.isTofu()).toBeTruthy();
|
||||
|
||||
const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust2.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust2.isLocallyVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust2.isTofu()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should trust signatures received from other devices", async function() {
|
||||
const aliceKeys = {};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
null,
|
||||
aliceKeys,
|
||||
);
|
||||
alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com");
|
||||
alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
|
||||
const selfSigningKey = new Uint8Array([
|
||||
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
|
||||
0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0,
|
||||
0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49,
|
||||
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
|
||||
]);
|
||||
|
||||
const keyChangePromise = new Promise((resolve, reject) => {
|
||||
alice._crypto._deviceList.once("userCrossSigningUpdated", (userId) => {
|
||||
if (userId === "@bob:example.com") {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
};
|
||||
aliceDevice.keys = deviceInfo.keys;
|
||||
aliceDevice.algorithms = deviceInfo.algorithms;
|
||||
await alice._crypto._signObject(aliceDevice);
|
||||
|
||||
const bobOlmAccount = new global.Olm.Account();
|
||||
bobOlmAccount.create();
|
||||
const bobKeys = JSON.parse(bobOlmAccount.identity_keys());
|
||||
const bobDevice = {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "Dynabook",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobKeys.ed25519,
|
||||
"curve25519:Dynabook": bobKeys.curve25519,
|
||||
},
|
||||
};
|
||||
const deviceStr = anotherjson.stringify(bobDevice);
|
||||
bobDevice.signatures = {
|
||||
"@bob:example.com": {
|
||||
"ed25519:Dynabook": bobOlmAccount.sign(deviceStr),
|
||||
},
|
||||
};
|
||||
olmlib.pkSign(bobDevice, selfSigningKey, "@bob:example.com");
|
||||
|
||||
const bobMaster = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
|
||||
},
|
||||
};
|
||||
olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com");
|
||||
|
||||
// Alice downloads Bob's keys
|
||||
// - device key
|
||||
// - ssk
|
||||
// - master key signed by her usk (pretend that it was signed by another
|
||||
// of Alice's devices)
|
||||
const responses = [
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
signed_curve25519: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
HttpResponse.filterResponse("@alice:example.com"),
|
||||
{
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
data: {
|
||||
next_batch: "abcdefg",
|
||||
device_lists: {
|
||||
changed: [
|
||||
"@bob:example.com",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/query",
|
||||
data: {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@alice:example.com": {
|
||||
"Osborne2": aliceDevice,
|
||||
},
|
||||
"@bob:example.com": {
|
||||
"Dynabook": bobDevice,
|
||||
},
|
||||
},
|
||||
"master_keys": {
|
||||
"@bob:example.com": bobMaster,
|
||||
},
|
||||
"self_signing_keys": {
|
||||
"@bob:example.com": {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self-signing"],
|
||||
keys: {
|
||||
"ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ":
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
},
|
||||
signatures: {
|
||||
"@bob:example.com": {
|
||||
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
|
||||
"2KLiufImvEbfJuAFvsaZD+PsL8ELWl7N1u9yr/9hZvwRghBfQMB"
|
||||
+ "LAI86b1kDV9+Cq1lt85ykReeCEzmTEPY2BQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
signed_curve25519: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
setHttpResponses(alice, responses);
|
||||
|
||||
await alice.startClient();
|
||||
|
||||
await keyChangePromise;
|
||||
|
||||
// Bob's device key should be trusted
|
||||
const bobTrust = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobTrust.isTofu()).toBeTruthy();
|
||||
|
||||
const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust.isLocallyVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust.isTofu()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should dis-trust an unsigned device", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
// Alice downloads Bob's ssk and device key
|
||||
// (NOTE: device key is not signed by ssk)
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
["ed25519:" + bobPubkey]: bobPubkey,
|
||||
},
|
||||
};
|
||||
const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK));
|
||||
bobSSK.signatures = {
|
||||
"@bob:example.com": {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
["ed25519:" + bobMasterPubkey]: bobMasterPubkey,
|
||||
},
|
||||
},
|
||||
self_signing: bobSSK,
|
||||
},
|
||||
firstUse: 1,
|
||||
unsigned: {},
|
||||
});
|
||||
const bobDevice = {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "Dynabook",
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": "somePubkey",
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Bob's device key should be untrusted
|
||||
const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust.isVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust.isTofu()).toBeFalsy();
|
||||
|
||||
// Alice verifies Bob's SSK
|
||||
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
|
||||
|
||||
// Bob's device key should be untrusted
|
||||
const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust2.isVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust2.isTofu()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should dis-trust a user when their ssk changes", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
await alice.resetCrossSigningKeys();
|
||||
// Alice downloads Bob's keys
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
["ed25519:" + bobPubkey]: bobPubkey,
|
||||
},
|
||||
};
|
||||
const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK));
|
||||
bobSSK.signatures = {
|
||||
"@bob:example.com": {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
["ed25519:" + bobMasterPubkey]: bobMasterPubkey,
|
||||
},
|
||||
},
|
||||
self_signing: bobSSK,
|
||||
},
|
||||
firstUse: 1,
|
||||
unsigned: {},
|
||||
});
|
||||
const bobDevice = {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "Dynabook",
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": "somePubkey",
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
const bobDeviceString = anotherjson.stringify(bobDevice);
|
||||
const sig = bobSigning.sign(bobDeviceString);
|
||||
bobDevice.signatures = {};
|
||||
bobDevice.signatures["@bob:example.com"] = {};
|
||||
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig;
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Alice verifies Bob's SSK
|
||||
alice.uploadKeySignatures = () => {};
|
||||
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
|
||||
|
||||
// Bob's device key should be trusted
|
||||
const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust.isVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust.isTofu()).toBeTruthy();
|
||||
|
||||
// Alice downloads new SSK for Bob
|
||||
const bobMasterSigning2 = new global.Olm.PkSigning();
|
||||
const bobMasterPrivkey2 = bobMasterSigning2.generate_seed();
|
||||
const bobMasterPubkey2 = bobMasterSigning2.init_with_seed(bobMasterPrivkey2);
|
||||
const bobSigning2 = new global.Olm.PkSigning();
|
||||
const bobPrivkey2 = bobSigning2.generate_seed();
|
||||
const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2);
|
||||
const bobSSK2 = {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
["ed25519:" + bobPubkey2]: bobPubkey2,
|
||||
},
|
||||
};
|
||||
const sskSig2 = bobMasterSigning2.sign(anotherjson.stringify(bobSSK2));
|
||||
bobSSK2.signatures = {
|
||||
"@bob:example.com": {
|
||||
["ed25519:" + bobMasterPubkey2]: sskSig2,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
["ed25519:" + bobMasterPubkey2]: bobMasterPubkey2,
|
||||
},
|
||||
},
|
||||
self_signing: bobSSK2,
|
||||
},
|
||||
firstUse: 0,
|
||||
unsigned: {},
|
||||
});
|
||||
// Bob's and his device should be untrusted
|
||||
const bobTrust = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust.isVerified()).toBeFalsy();
|
||||
expect(bobTrust.isTofu()).toBeFalsy();
|
||||
|
||||
const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust2.isVerified()).toBeFalsy();
|
||||
expect(bobDeviceTrust2.isTofu()).toBeFalsy();
|
||||
|
||||
// Alice verifies Bob's SSK
|
||||
alice.uploadKeySignatures = () => {};
|
||||
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey2, true);
|
||||
|
||||
// Bob should be trusted but not his device
|
||||
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust2.isVerified()).toBeTruthy();
|
||||
|
||||
const bobDeviceTrust3 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust3.isVerified()).toBeFalsy();
|
||||
|
||||
// Alice gets new signature for device
|
||||
const sig2 = bobSigning2.sign(bobDeviceString);
|
||||
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
|
||||
// Bob's device should be trusted again (but not TOFU)
|
||||
const bobTrust3 = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust3.isVerified()).toBeTruthy();
|
||||
|
||||
const bobDeviceTrust4 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
|
||||
expect(bobDeviceTrust4.isCrossSigningVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should offer to upgrade device verifications to cross-signing", async function() {
|
||||
let upgradeResolveFunc;
|
||||
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
shouldUpgradeDeviceVerifications: (verifs) => {
|
||||
expect(verifs.users["@bob:example.com"]).toBeDefined();
|
||||
upgradeResolveFunc();
|
||||
return ["@bob:example.com"];
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const bob = await makeTestClient(
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
);
|
||||
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
// set Bob's cross-signing key
|
||||
await bob.resetCrossSigningKeys();
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: {
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": bob._crypto._olmDevice.deviceCurve25519Key,
|
||||
"ed25519:Dynabook": bob._crypto._olmDevice.deviceEd25519Key,
|
||||
},
|
||||
verified: 1,
|
||||
known: true,
|
||||
},
|
||||
});
|
||||
alice._crypto._deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
bob._crypto._crossSigningInfo.toStorage(),
|
||||
);
|
||||
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// when alice sets up cross-signing, she should notice that bob's
|
||||
// cross-signing key is signed by his Dynabook, which alice has
|
||||
// verified, and ask if the device verification should be upgraded to a
|
||||
// cross-signing verification
|
||||
let upgradePromise = new Promise((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
await alice.resetCrossSigningKeys();
|
||||
await upgradePromise;
|
||||
|
||||
const bobTrust = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobTrust.isTofu()).toBeTruthy();
|
||||
|
||||
// "forget" that Bob is trusted
|
||||
delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"]
|
||||
.keys.master.signatures["@alice:example.com"];
|
||||
|
||||
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust2.isCrossSigningVerified()).toBeFalsy();
|
||||
expect(bobTrust2.isTofu()).toBeTruthy();
|
||||
|
||||
upgradePromise = new Promise((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
|
||||
await upgradePromise;
|
||||
|
||||
const bobTrust3 = alice.checkUserTrust("@bob:example.com");
|
||||
expect(bobTrust3.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(bobTrust3.isTofu()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
|
||||
import { MatrixEvent } from '../../../lib/models/event';
|
||||
import { SECRET_STORAGE_ALGORITHM_V1 } from '../../../lib/crypto/SecretStorage';
|
||||
|
||||
import olmlib from '../../../lib/crypto/olmlib';
|
||||
|
||||
import TestClient from '../../TestClient';
|
||||
import { makeTestClients } from './verification/util';
|
||||
|
||||
async function makeTestClient(userInfo, options) {
|
||||
const client = (new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
|
||||
)).client;
|
||||
|
||||
await client.initCrypto();
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("Secrets", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
it("should store and retrieve a secret", async function() {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const pubkey = decryption.generate_key();
|
||||
const privkey = decryption.get_private_key();
|
||||
|
||||
const signing = new global.Olm.PkSigning();
|
||||
const signingKey = signing.generate_seed();
|
||||
const signingPubKey = signing.init_with_seed(signingKey);
|
||||
|
||||
const signingkeyInfo = {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ['master'],
|
||||
keys: {
|
||||
['ed25519:' + signingPubKey]: signingPubKey,
|
||||
},
|
||||
};
|
||||
|
||||
const getKey = jest.fn(e => {
|
||||
expect(Object.keys(e.keys)).toEqual(["abc"]);
|
||||
return ['abc', privkey];
|
||||
});
|
||||
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => signingKey,
|
||||
getSecretStorageKey: getKey,
|
||||
},
|
||||
},
|
||||
);
|
||||
alice._crypto._crossSigningInfo.setKeys({
|
||||
master: signingkeyInfo,
|
||||
});
|
||||
|
||||
const secretStorage = alice._crypto._secretStorage;
|
||||
|
||||
alice.setAccountData = async function(eventType, contents, callback) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
}),
|
||||
]);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const keyAccountData = {
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1,
|
||||
pubkey: pubkey,
|
||||
};
|
||||
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.key.abc",
|
||||
content: keyAccountData,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(secretStorage.isStored("foo")).toBe(false);
|
||||
|
||||
await secretStorage.store("foo", "bar", ["abc"]);
|
||||
|
||||
expect(secretStorage.isStored("foo")).toBe(true);
|
||||
expect(await secretStorage.get("foo")).toBe("bar");
|
||||
|
||||
expect(getKey).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw if given a key that doesn't exist", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
);
|
||||
|
||||
try {
|
||||
await alice.storeSecret("foo", "bar", ["this secret does not exist"]);
|
||||
// should be able to use expect(...).toThrow() but mocha still fails
|
||||
// the test even when it throws for reasons I have no inclination to debug
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
});
|
||||
|
||||
it("should refuse to encrypt with zero keys", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
);
|
||||
|
||||
try {
|
||||
await alice.storeSecret("foo", "bar", []);
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
});
|
||||
|
||||
it("should encrypt with default key if keys is null", async function() {
|
||||
let keys = {};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => keys[t],
|
||||
saveCrossSigningKeys: k => keys = k,
|
||||
},
|
||||
},
|
||||
);
|
||||
alice.setAccountData = async function(eventType, contents, callback) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
}),
|
||||
]);
|
||||
};
|
||||
alice.resetCrossSigningKeys();
|
||||
|
||||
const newKeyId = await alice.addSecretKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1,
|
||||
);
|
||||
// we don't await on this because it waits for the event to come down the sync
|
||||
// which won't happen in the test setup
|
||||
alice.setDefaultSecretStorageKeyId(newKeyId);
|
||||
await alice.storeSecret("foo", "bar");
|
||||
|
||||
const accountData = alice.getAccountData('foo');
|
||||
expect(accountData.getContent().encrypted).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should refuse to encrypt if no keys given and no default key", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
);
|
||||
|
||||
try {
|
||||
await alice.storeSecret("foo", "bar");
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
}
|
||||
});
|
||||
|
||||
it("should request secrets from other clients", async function() {
|
||||
const [osborne2, vax] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@alice:example.com", deviceId: "VAX"},
|
||||
],
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
onSecretRequested: e => {
|
||||
expect(e.name).toBe("foo");
|
||||
return "bar";
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const vaxDevice = vax.client._crypto._olmDevice;
|
||||
const osborne2Device = osborne2.client._crypto._olmDevice;
|
||||
const secretStorage = osborne2.client._crypto._secretStorage;
|
||||
|
||||
osborne2.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"VAX": {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "VAX",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:VAX": vaxDevice.deviceEd25519Key,
|
||||
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
|
||||
},
|
||||
},
|
||||
});
|
||||
vax.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"Osborne2": {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Osborne2": osborne2Device.deviceEd25519Key,
|
||||
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await osborne2Device.generateOneTimeKeys(1);
|
||||
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
|
||||
await osborne2Device.markKeysAsPublished();
|
||||
|
||||
await vax.client._crypto._olmDevice.createOutboundSession(
|
||||
osborne2Device.deviceCurve25519Key,
|
||||
Object.values(otks)[0],
|
||||
);
|
||||
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
const secret = await request.promise;
|
||||
|
||||
expect(secret).toBe("bar");
|
||||
});
|
||||
});
|
||||
@@ -13,15 +13,14 @@ 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) {
|
||||
console.warn("unable to run device verification tests: libolm not available");
|
||||
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';
|
||||
@@ -30,12 +29,12 @@ const Olm = global.Olm;
|
||||
|
||||
describe("QR code verification", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running device verification tests: libolm not present');
|
||||
logger.warn('Not running device verification tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
describe("showing", function() {
|
||||
@@ -47,7 +46,7 @@ describe("QR code verification", function() {
|
||||
return "device+ed25519+key";
|
||||
},
|
||||
});
|
||||
const spy = expect.createSpy().andCall((e) => {
|
||||
const spy = jest.fn((e) => {
|
||||
qrCode.done();
|
||||
});
|
||||
qrCode.on("show_qr_code", spy);
|
||||
@@ -77,8 +76,8 @@ describe("QR code verification", function() {
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy().andReturn(device),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
getStoredDevice: jest.fn().mockReturnValue(device),
|
||||
setDeviceVerified: jest.fn(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client);
|
||||
qrCode.on("confirm_user_id", ({userId, confirm}) => {
|
||||
@@ -100,18 +99,18 @@ describe("QR code verification", function() {
|
||||
|
||||
it("should error when the user ID doesn't match", async function() {
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy(),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
getStoredDevice: jest.fn(),
|
||||
setDeviceVerified: jest.fn(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client, "@bob:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = expect.createSpy();
|
||||
const spy = jest.fn();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).toNotHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).toNotHaveBeenCalled();
|
||||
expect(client.getStoredDevice).not.toHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should error if the key doesn't match", async function() {
|
||||
@@ -129,18 +128,18 @@ describe("QR code verification", function() {
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy().andReturn(device),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
getStoredDevice: jest.fn().mockReturnValue(device),
|
||||
setDeviceVerified: jest.fn(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client, "@alice:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = expect.createSpy();
|
||||
const spy = jest.fn();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).toHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).toNotHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,15 +13,14 @@ 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) {
|
||||
console.warn("unable to run device verification tests: libolm not available");
|
||||
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';
|
||||
@@ -32,12 +31,12 @@ import {makeTestClients} from './util';
|
||||
|
||||
describe("verification request", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running device verification unit tests: libolm not present');
|
||||
logger.warn('Not running device verification unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
it("should request and accept a verification", async function() {
|
||||
@@ -50,7 +49,7 @@ describe("verification request", function() {
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice._crypto._deviceList.getRawStoredDevicesForUser = function() {
|
||||
alice.client._crypto._deviceList.getRawStoredDevicesForUser = function() {
|
||||
return {
|
||||
Dynabook: {
|
||||
keys: {
|
||||
@@ -59,17 +58,23 @@ describe("verification request", function() {
|
||||
},
|
||||
};
|
||||
};
|
||||
alice.downloadKeys = () => {
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.downloadKeys = () => {
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.on("crypto.verification.request", (request) => {
|
||||
bob.client.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);
|
||||
const aliceVerifier = await alice.client.requestVerification("@bob:example.com");
|
||||
expect(aliceVerifier).toBeInstanceOf(SAS);
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
aliceVerifier._endTimer();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,14 +13,15 @@ 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) {
|
||||
console.warn("unable to run device verification tests: libolm not available");
|
||||
logger.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
import olmlib from '../../../../lib/crypto/olmlib';
|
||||
|
||||
import sdk from '../../../..';
|
||||
|
||||
@@ -35,14 +36,17 @@ const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
import {makeTestClients} from './util';
|
||||
|
||||
let ALICE_DEVICES;
|
||||
let BOB_DEVICES;
|
||||
|
||||
describe("SAS verification", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running device verification unit tests: libolm not present');
|
||||
logger.warn('Not running device verification unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
it("should error on an unexpected event", async function() {
|
||||
@@ -52,13 +56,15 @@ describe("SAS verification", function() {
|
||||
type: "es.inquisition",
|
||||
content: {},
|
||||
}));
|
||||
const spy = expect.createSpy();
|
||||
await sas.verify()
|
||||
.catch(spy);
|
||||
const spy = jest.fn();
|
||||
await sas.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
// Cancel the SAS for cleanup (we started a verification, so abort)
|
||||
sas.cancel();
|
||||
});
|
||||
|
||||
describe("verification", function() {
|
||||
describe("verification", () => {
|
||||
let alice;
|
||||
let bob;
|
||||
let aliceSasEvent;
|
||||
@@ -66,7 +72,7 @@ describe("SAS verification", function() {
|
||||
let aliceVerifier;
|
||||
let bobPromise;
|
||||
|
||||
beforeEach(async function() {
|
||||
beforeEach(async () => {
|
||||
[alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
@@ -77,39 +83,44 @@ describe("SAS verification", function() {
|
||||
},
|
||||
);
|
||||
|
||||
alice.setDeviceVerified = expect.createSpy();
|
||||
alice.getDeviceEd25519Key = () => {
|
||||
return "alice+base64+ed25519+key";
|
||||
};
|
||||
alice.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||
},
|
||||
const aliceDevice = alice.client._crypto._olmDevice;
|
||||
const bobDevice = bob.client._crypto._olmDevice;
|
||||
|
||||
ALICE_DEVICES = {
|
||||
Osborne2: {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Osborne2": aliceDevice.deviceEd25519Key,
|
||||
"curve25519:Osborne2": aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
"Dynabook",
|
||||
);
|
||||
},
|
||||
};
|
||||
alice.downloadKeys = () => {
|
||||
|
||||
BOB_DEVICES = {
|
||||
Dynabook: {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "Dynabook",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobDevice.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bobDevice.deviceCurve25519Key,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
alice.client._crypto._deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
alice.client.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 = () => {
|
||||
bob.client._crypto._deviceList.storeDevicesForUser(
|
||||
"@alice:example.com", ALICE_DEVICES,
|
||||
);
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
@@ -117,7 +128,7 @@ describe("SAS verification", function() {
|
||||
bobSasEvent = null;
|
||||
|
||||
bobPromise = new Promise((resolve, reject) => {
|
||||
bob.on("crypto.verification.start", (verifier) => {
|
||||
bob.client.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
@@ -138,8 +149,318 @@ describe("SAS verification", function() {
|
||||
});
|
||||
});
|
||||
|
||||
aliceVerifier = alice.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
|
||||
aliceVerifier = alice.client.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.client.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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
afterEach(async () => {
|
||||
await Promise.all([
|
||||
alice.stop(),
|
||||
bob.stop(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should verify a key", async () => {
|
||||
let macMethod;
|
||||
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return origSendToDevice(type, map);
|
||||
};
|
||||
|
||||
alice.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@bob:example.com": BOB_DEVICES,
|
||||
},
|
||||
});
|
||||
bob.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@alice:example.com": ALICE_DEVICES,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
alice.httpBackend.flush(),
|
||||
bob.httpBackend.flush(),
|
||||
]);
|
||||
|
||||
// make sure that it uses the preferred method
|
||||
expect(macMethod).toBe("hkdf-hmac-sha256");
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
const bobDevice
|
||||
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
expect(bobDevice.isVerified()).toBeTruthy();
|
||||
const aliceDevice
|
||||
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
expect(aliceDevice.isVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should be able to verify using the old MAC", async () => {
|
||||
// pretend that Alice can only understand the old (incorrect) MAC,
|
||||
// and make sure that she can still verify with Bob
|
||||
let macMethod;
|
||||
const aliceOrigSendToDevice = alice.client.sendToDevice.bind(alice.client);
|
||||
alice.client.sendToDevice = (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.client.getUserId()][bob.client.deviceId]
|
||||
.message_authentication_codes = ['hmac-sha256'];
|
||||
}
|
||||
return aliceOrigSendToDevice(type, map);
|
||||
};
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
};
|
||||
|
||||
alice.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@bob:example.com": BOB_DEVICES,
|
||||
},
|
||||
});
|
||||
bob.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
failures: {},
|
||||
device_keys: {
|
||||
"@alice:example.com": ALICE_DEVICES,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
alice.httpBackend.flush(),
|
||||
bob.httpBackend.flush(),
|
||||
]);
|
||||
|
||||
expect(macMethod).toBe("hmac-sha256");
|
||||
|
||||
const bobDevice
|
||||
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
|
||||
expect(bobDevice.isVerified()).toBeTruthy();
|
||||
const aliceDevice
|
||||
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
|
||||
expect(aliceDevice.isVerified()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should verify a cross-signing key", async () => {
|
||||
alice.httpBackend.when('POST', '/keys/device_signing/upload').respond(
|
||||
200, {},
|
||||
);
|
||||
alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
|
||||
alice.httpBackend.flush(undefined, 2);
|
||||
await alice.client.resetCrossSigningKeys();
|
||||
bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {});
|
||||
bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
|
||||
bob.httpBackend.flush(undefined, 2);
|
||||
|
||||
await bob.client.resetCrossSigningKeys();
|
||||
|
||||
bob.client._crypto._deviceList.storeCrossSigningForUser(
|
||||
"@alice:example.com", {
|
||||
keys: alice.client._crypto._crossSigningInfo.keys,
|
||||
},
|
||||
);
|
||||
|
||||
const verifyProm = Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => {
|
||||
bob.httpBackend.when(
|
||||
'POST', '/keys/signatures/upload',
|
||||
).respond(200, {});
|
||||
bob.httpBackend.flush(undefined, 1, 2000);
|
||||
return verifier.verify();
|
||||
}),
|
||||
]);
|
||||
|
||||
await verifyProm;
|
||||
|
||||
const bobDeviceTrust = alice.client.checkDeviceTrust(
|
||||
"@bob:example.com", "Dynabook",
|
||||
);
|
||||
expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy();
|
||||
|
||||
const aliceTrust = bob.client.checkUserTrust("@alice:example.com");
|
||||
expect(aliceTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(aliceTrust.isTofu()).toBeTruthy();
|
||||
|
||||
const aliceDeviceTrust = bob.client.checkDeviceTrust(
|
||||
"@alice:example.com", "Osborne2",
|
||||
);
|
||||
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
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.client.setDeviceVerified = jest.fn();
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.client.setDeviceVerified = jest.fn();
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const bobPromise = new Promise((resolve, reject) => {
|
||||
bob.client.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
resolve(verifier);
|
||||
});
|
||||
});
|
||||
|
||||
const aliceVerifier = alice.client.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.client.getUserId(), bob.client.deviceId,
|
||||
);
|
||||
|
||||
const aliceSpy = jest.fn();
|
||||
const bobSpy = jest.fn();
|
||||
await Promise.all([
|
||||
aliceVerifier.verify().catch(aliceSpy),
|
||||
bobPromise.then((verifier) => verifier.verify()).catch(bobSpy),
|
||||
]);
|
||||
expect(aliceSpy).toHaveBeenCalled();
|
||||
expect(bobSpy).toHaveBeenCalled();
|
||||
expect(alice.client.setDeviceVerified)
|
||||
.not.toHaveBeenCalled();
|
||||
expect(bob.client.setDeviceVerified)
|
||||
.not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("verification in DM", 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.client.setDeviceVerified = jest.fn();
|
||||
alice.client.getDeviceEd25519Key = () => {
|
||||
return "alice+base64+ed25519+key";
|
||||
};
|
||||
alice.client.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
"Dynabook",
|
||||
);
|
||||
};
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.client.setDeviceVerified = jest.fn();
|
||||
bob.client.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Osborne2": "alice+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
"Osborne2",
|
||||
);
|
||||
};
|
||||
bob.client.getDeviceEd25519Key = () => {
|
||||
return "bob+base64+ed25519+key";
|
||||
};
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
aliceSasEvent = null;
|
||||
bobSasEvent = null;
|
||||
|
||||
bobPromise = new Promise((resolve, reject) => {
|
||||
bob.client.on("event", async (event) => {
|
||||
const content = event.getContent();
|
||||
if (event.getType() === "m.room.message"
|
||||
&& content.msgtype === "m.key.verification.request") {
|
||||
expect(content.methods).toContain(SAS.NAME);
|
||||
expect(content.to).toBe(bob.client.getUserId());
|
||||
const verifier = bob.client.acceptVerificationDM(event, SAS.NAME);
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
await verifier.verify();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
aliceVerifier = await alice.client.requestVerificationDM(
|
||||
bob.client.getUserId(), "!room_id", [verificationMethods.SAS],
|
||||
);
|
||||
aliceVerifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
@@ -160,113 +481,16 @@ describe("SAS verification", function() {
|
||||
});
|
||||
|
||||
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()),
|
||||
bobPromise,
|
||||
]);
|
||||
|
||||
// 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);
|
||||
expect(alice.client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
|
||||
expect(bob.client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.client.getUserId(), alice.client.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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,31 +33,63 @@ export async function makeTestClients(userInfos, options) {
|
||||
type: type,
|
||||
content: msg,
|
||||
});
|
||||
setTimeout(
|
||||
() => clientMap[userId][deviceId]
|
||||
.emit("toDeviceEvent", event),
|
||||
0,
|
||||
);
|
||||
const client = clientMap[userId][deviceId];
|
||||
if (event.isEncrypted()) {
|
||||
event.attemptDecryption(client._crypto)
|
||||
.then(() => client.emit("toDeviceEvent", event));
|
||||
} else {
|
||||
setTimeout(
|
||||
() => client.emit("toDeviceEvent", event),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const sendEvent = function(room, type, content) {
|
||||
// make up a unique ID as the event ID
|
||||
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
|
||||
const event = new MatrixEvent({
|
||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||
type: type,
|
||||
content: content,
|
||||
room_id: room,
|
||||
event_id: eventId,
|
||||
});
|
||||
for (const tc of clients) {
|
||||
setTimeout(
|
||||
() => tc.client.emit("event", event),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
return {event_id: eventId};
|
||||
};
|
||||
|
||||
for (const userInfo of userInfos) {
|
||||
const client = (new TestClient(
|
||||
let keys = {};
|
||||
if (!options) options = {};
|
||||
if (!options.cryptoCallbacks) options.cryptoCallbacks = {};
|
||||
if (!options.cryptoCallbacks.saveCrossSigningKeys) {
|
||||
options.cryptoCallbacks.saveCrossSigningKeys = k => { keys = k; };
|
||||
options.cryptoCallbacks.getCrossSigningKey = typ => keys[typ];
|
||||
}
|
||||
const testClient = 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);
|
||||
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
|
||||
testClient.client.sendToDevice = sendToDevice;
|
||||
testClient.client.sendEvent = sendEvent;
|
||||
clients.push(testClient);
|
||||
}
|
||||
|
||||
await Promise.all(clients.map((client) => client.initCrypto()));
|
||||
await Promise.all(clients.map((testClient) => testClient.client.initCrypto()));
|
||||
|
||||
return clients;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ function mockRoomStates(timeline) {
|
||||
timeline._endState = utils.mock(sdk.RoomState, "endState");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("EventTimeline", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
@@ -18,8 +16,6 @@ describe("EventTimeline", function() {
|
||||
let timeline;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
// XXX: this is a horrid hack; should use sinon or something instead to mock
|
||||
const timelineSet = { room: { roomId: roomId }};
|
||||
timelineSet.room.getUnfilteredTimelineSet = function() {
|
||||
@@ -78,7 +74,7 @@ describe("EventTimeline", function() {
|
||||
|
||||
expect(function() {
|
||||
timeline.initialiseState(state);
|
||||
}).toNotThrow();
|
||||
}).not.toThrow();
|
||||
timeline.addEvent(event, false);
|
||||
expect(function() {
|
||||
timeline.initialiseState(state);
|
||||
@@ -121,7 +117,7 @@ describe("EventTimeline", function() {
|
||||
const next = {b: "b"};
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
}).toNotThrow();
|
||||
}).not.toThrow();
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
|
||||
.toBe(prev);
|
||||
expect(function() {
|
||||
@@ -130,7 +126,7 @@ describe("EventTimeline", function() {
|
||||
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
||||
}).toNotThrow();
|
||||
}).not.toThrow();
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
|
||||
.toBe(next);
|
||||
expect(function() {
|
||||
@@ -187,14 +183,14 @@ describe("EventTimeline", function() {
|
||||
name: "Old Alice",
|
||||
};
|
||||
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
|
||||
.andCall(function(uid) {
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
|
||||
.andCall(function(uid) {
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
@@ -229,14 +225,14 @@ describe("EventTimeline", function() {
|
||||
name: "Old Alice",
|
||||
};
|
||||
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
|
||||
.andCall(function(uid) {
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
|
||||
.andCall(function(uid) {
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
@@ -281,7 +277,7 @@ describe("EventTimeline", function() {
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
toNotHaveBeenCalled();
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -311,7 +307,7 @@ describe("EventTimeline", function() {
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
toNotHaveBeenCalled();
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,16 +17,10 @@ limitations under the License.
|
||||
import sdk from '../..';
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
describe(".attemptDecryption", () => {
|
||||
let encryptedEvent;
|
||||
|
||||
@@ -44,21 +38,23 @@ describe("MatrixEvent", () => {
|
||||
let callCount = 0;
|
||||
|
||||
let prom2;
|
||||
let prom2Fulfilled = false;
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: function() {
|
||||
++callCount;
|
||||
console.log(`decrypt: ${callCount}`);
|
||||
logger.log(`decrypt: ${callCount}`);
|
||||
if (callCount == 1) {
|
||||
// schedule a second decryption attempt while
|
||||
// the first one is still running.
|
||||
prom2 = encryptedEvent.attemptDecryption(crypto);
|
||||
prom2.then(() => prom2Fulfilled = true);
|
||||
|
||||
const error = new Error("nope");
|
||||
error.name = 'DecryptionError';
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
expect(prom2.isFulfilled()).toBe(
|
||||
expect(prom2Fulfilled).toBe(
|
||||
false, 'second attemptDecryption resolved too soon');
|
||||
|
||||
return Promise.resolve({
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const Filter = sdk.Filter;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("Filter", function() {
|
||||
const filterId = "f1lt3ring15g00d4ursoul";
|
||||
@@ -12,7 +9,6 @@ describe("Filter", function() {
|
||||
let filter;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
filter = new Filter(userId);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,12 +18,11 @@ limitations under the License.
|
||||
import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
const sdk = require("../..");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
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)
|
||||
@@ -34,13 +33,9 @@ class FakeClient {
|
||||
}
|
||||
|
||||
describe("InteractiveAuth", function() {
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
it("should start an auth stage and complete it", function(done) {
|
||||
const doRequest = expect.createSpy();
|
||||
const stateUpdated = expect.createSpy();
|
||||
it("should start an auth stage and complete it", function() {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
@@ -63,8 +58,8 @@ describe("InteractiveAuth", function() {
|
||||
});
|
||||
|
||||
// first we expect a call here
|
||||
stateUpdated.andCall(function(stage) {
|
||||
console.log('aaaa');
|
||||
stateUpdated.mockImplementation(function(stage) {
|
||||
logger.log('aaaa');
|
||||
expect(stage).toEqual("logintype");
|
||||
ia.submitAuthDict({
|
||||
type: "logintype",
|
||||
@@ -74,8 +69,8 @@ describe("InteractiveAuth", function() {
|
||||
|
||||
// .. which should trigger a call here
|
||||
const requestRes = {"a": "b"};
|
||||
doRequest.andCall(function(authData) {
|
||||
console.log('cccc');
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log('cccc');
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: "logintype",
|
||||
@@ -84,16 +79,16 @@ describe("InteractiveAuth", function() {
|
||||
return Promise.resolve(requestRes);
|
||||
});
|
||||
|
||||
ia.attemptAuth().then(function(res) {
|
||||
return ia.attemptAuth().then(function(res) {
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest.calls.length).toEqual(1);
|
||||
expect(stateUpdated.calls.length).toEqual(1);
|
||||
}).nodeify(done);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should make a request if no authdata is provided", function(done) {
|
||||
const doRequest = expect.createSpy();
|
||||
const stateUpdated = expect.createSpy();
|
||||
it("should make a request if no authdata is provided", function() {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
@@ -105,8 +100,8 @@ describe("InteractiveAuth", function() {
|
||||
expect(ia.getStageParams("logintype")).toBe(undefined);
|
||||
|
||||
// first we expect a call to doRequest
|
||||
doRequest.andCall(function(authData) {
|
||||
console.log("request1", authData);
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual({});
|
||||
const err = new MatrixError({
|
||||
session: "sessionId",
|
||||
@@ -123,7 +118,7 @@ describe("InteractiveAuth", function() {
|
||||
|
||||
// .. which should be followed by a call to stateUpdated
|
||||
const requestRes = {"a": "b"};
|
||||
stateUpdated.andCall(function(stage) {
|
||||
stateUpdated.mockImplementation(function(stage) {
|
||||
expect(stage).toEqual("logintype");
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams("logintype")).toEqual({
|
||||
@@ -131,8 +126,8 @@ describe("InteractiveAuth", function() {
|
||||
});
|
||||
|
||||
// submitAuthDict should trigger another call to doRequest
|
||||
doRequest.andCall(function(authData) {
|
||||
console.log("request2", authData);
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log("request2", authData);
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: "logintype",
|
||||
@@ -147,10 +142,10 @@ describe("InteractiveAuth", function() {
|
||||
});
|
||||
});
|
||||
|
||||
ia.attemptAuth().then(function(res) {
|
||||
return ia.attemptAuth().then(function(res) {
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest.calls.length).toEqual(2);
|
||||
expect(stateUpdated.calls.length).toEqual(1);
|
||||
}).nodeify(done);
|
||||
expect(doRequest).toBeCalledTimes(2);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import expect from 'expect';
|
||||
import TestClient from '../TestClient';
|
||||
|
||||
describe('Login request', function() {
|
||||
|
||||
@@ -3,10 +3,10 @@ import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
const sdk = require("../..");
|
||||
const MatrixClient = sdk.MatrixClient;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
import lolex from 'lolex';
|
||||
import logger from '../../src/logger';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
const userId = "@alice:bar";
|
||||
@@ -15,7 +15,6 @@ describe("MatrixClient", function() {
|
||||
let client;
|
||||
let store;
|
||||
let scheduler;
|
||||
let clock;
|
||||
|
||||
const KEEP_ALIVE_PATH = "/_matrix/client/versions";
|
||||
|
||||
@@ -69,7 +68,7 @@ describe("MatrixClient", function() {
|
||||
"MatrixClient[UT] RECV " + method + " " + path + " " +
|
||||
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
|
||||
);
|
||||
console.log(logLine);
|
||||
logger.log(logLine);
|
||||
|
||||
if (!next) { // no more things to return
|
||||
if (pendingLookup) {
|
||||
@@ -91,7 +90,7 @@ describe("MatrixClient", function() {
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
if (next.path === path && next.method === method) {
|
||||
console.log(
|
||||
logger.log(
|
||||
"MatrixClient[UT] Matched. Returning " +
|
||||
(next.error ? "BAD" : "GOOD") + " response",
|
||||
);
|
||||
@@ -124,24 +123,22 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
clock = lolex.install();
|
||||
scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
|
||||
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
|
||||
"getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
|
||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.storeClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.isNewlyCreated = expect.createSpy().andReturn(Promise.resolve(true));
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.storeClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.isNewlyCreated = jest.fn().mockReturnValue(Promise.resolve(true));
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
@@ -153,13 +150,10 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
// FIXME: We shouldn't be yanking _http like this.
|
||||
client._http = [
|
||||
"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);
|
||||
"authedRequest", "getContentUri", "request", "uploadContent",
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
client._http.authedRequest.mockImplementation(httpReq);
|
||||
client._http.request.mockImplementation(httpReq);
|
||||
|
||||
// set reasonable working defaults
|
||||
acceptKeepalives = true;
|
||||
@@ -171,16 +165,12 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
clock.uninstall();
|
||||
// need to re-stub the requests with NOPs because there are no guarantees
|
||||
// clients from previous tests will be GC'd before the next test. This
|
||||
// means they may call /events and then fail an expect() which will fail
|
||||
// a DIFFERENT test (pollution between tests!) - we return unresolved
|
||||
// promises to stop the client from continuing to run.
|
||||
client._http.authedRequest.andCall(function() {
|
||||
return Promise.defer().promise;
|
||||
});
|
||||
client._http.authedRequestWithPrefix.andCall(function() {
|
||||
client._http.authedRequest.mockImplementation(function() {
|
||||
return Promise.defer().promise;
|
||||
});
|
||||
});
|
||||
@@ -190,10 +180,10 @@ describe("MatrixClient", function() {
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
const filterId = "ehfewf";
|
||||
store.getFilterIdByName.andReturn(filterId);
|
||||
store.getFilterIdByName.mockReturnValue(filterId);
|
||||
const filter = new sdk.Filter(0, filterId);
|
||||
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
|
||||
store.getFilter.andReturn(filter);
|
||||
store.getFilter.mockReturnValue(filter);
|
||||
const syncPromise = new Promise((resolve, reject) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "SYNCING") {
|
||||
@@ -254,7 +244,7 @@ describe("MatrixClient", function() {
|
||||
},
|
||||
});
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
store.getFilterIdByName.andReturn(invalidFilterId);
|
||||
store.getFilterIdByName.mockReturnValue(invalidFilterId);
|
||||
|
||||
const filterName = getFilterName(client.credentials.userId);
|
||||
client.store.setFilterIdByName(filterName, invalidFilterId);
|
||||
@@ -286,7 +276,7 @@ describe("MatrixClient", function() {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(2);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
clock.tick(1);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -312,9 +302,9 @@ describe("MatrixClient", function() {
|
||||
expect(client.retryImmediately()).toBe(
|
||||
true, "retryImmediately returned false",
|
||||
);
|
||||
clock.tick(1);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
clock.tick(10000);
|
||||
jest.advanceTimersByTime(10000);
|
||||
} else if (state === "SYNCING" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -336,7 +326,7 @@ describe("MatrixClient", function() {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(3);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
clock.tick(1);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -353,7 +343,7 @@ describe("MatrixClient", function() {
|
||||
function syncChecker(expectedStates, done) {
|
||||
return function syncListener(state, old) {
|
||||
const expected = expectedStates.shift();
|
||||
console.log(
|
||||
logger.log(
|
||||
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected,
|
||||
);
|
||||
if (!expected) {
|
||||
@@ -367,7 +357,7 @@ describe("MatrixClient", function() {
|
||||
done();
|
||||
}
|
||||
// standard retry time is 5 to 10 seconds
|
||||
clock.tick(10000);
|
||||
jest.advanceTimersByTime(10000);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ import 'source-map-support/register';
|
||||
const PushProcessor = require("../../lib/pushprocessor");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe('NotificationService', function() {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
const testDisplayName = "Alice M";
|
||||
|
||||
@@ -1,53 +1,46 @@
|
||||
"use strict";
|
||||
|
||||
import 'source-map-support/register';
|
||||
const callbacks = require("../../lib/realtime-callbacks");
|
||||
const testUtils = require("../test-utils.js");
|
||||
const callbacks = require("../../src/realtime-callbacks");
|
||||
|
||||
import expect from 'expect';
|
||||
import lolex from 'lolex';
|
||||
let wallTime = 1234567890;
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("realtime-callbacks", function() {
|
||||
let clock;
|
||||
|
||||
function tick(millis) {
|
||||
clock.tick(millis);
|
||||
wallTime += millis;
|
||||
jest.advanceTimersByTime(millis);
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
clock = lolex.install();
|
||||
const fakeDate = clock.Date;
|
||||
callbacks.setNow(fakeDate.now.bind(fakeDate));
|
||||
callbacks.setNow(() => wallTime);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
callbacks.setNow();
|
||||
clock.uninstall();
|
||||
});
|
||||
|
||||
describe("setTimeout", function() {
|
||||
it("should call the callback after the timeout", function() {
|
||||
const callback = expect.createSpy();
|
||||
const callback = jest.fn();
|
||||
callbacks.setTimeout(callback, 100);
|
||||
|
||||
expect(callback).toNotHaveBeenCalled();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("should default to a zero timeout", function() {
|
||||
const callback = expect.createSpy();
|
||||
const callback = jest.fn();
|
||||
callbacks.setTimeout(callback);
|
||||
|
||||
expect(callback).toNotHaveBeenCalled();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should pass any parameters to the callback", function() {
|
||||
const callback = expect.createSpy();
|
||||
const callback = jest.fn();
|
||||
callbacks.setTimeout(callback, 0, "a", "b", "c");
|
||||
tick(0);
|
||||
expect(callback).toHaveBeenCalledWith("a", "b", "c");
|
||||
@@ -66,10 +59,10 @@ describe("realtime-callbacks", function() {
|
||||
});
|
||||
|
||||
it("should handle timeouts of several seconds", function() {
|
||||
const callback = expect.createSpy();
|
||||
const callback = jest.fn();
|
||||
callbacks.setTimeout(callback, 2000);
|
||||
|
||||
expect(callback).toNotHaveBeenCalled();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
tick(500);
|
||||
}
|
||||
@@ -77,24 +70,24 @@ describe("realtime-callbacks", function() {
|
||||
});
|
||||
|
||||
it("should call multiple callbacks in the right order", function() {
|
||||
const callback1 = expect.createSpy();
|
||||
const callback2 = expect.createSpy();
|
||||
const callback3 = expect.createSpy();
|
||||
const callback1 = jest.fn();
|
||||
const callback2 = jest.fn();
|
||||
const callback3 = jest.fn();
|
||||
callbacks.setTimeout(callback2, 200);
|
||||
callbacks.setTimeout(callback1, 100);
|
||||
callbacks.setTimeout(callback3, 300);
|
||||
|
||||
expect(callback1).toNotHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback3).toNotHaveBeenCalled();
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback3).toNotHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
expect(callback3).toNotHaveBeenCalled();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
@@ -102,35 +95,34 @@ describe("realtime-callbacks", function() {
|
||||
});
|
||||
|
||||
it("should treat -ve timeouts the same as a zero timeout", function() {
|
||||
const callback1 = expect.createSpy();
|
||||
const callback2 = expect.createSpy();
|
||||
const callback1 = jest.fn();
|
||||
const callback2 = jest.fn();
|
||||
|
||||
// check that cb1 is called before cb2
|
||||
callback1.andCall(function() {
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
callback1.mockImplementation(function() {
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
callbacks.setTimeout(callback1);
|
||||
callbacks.setTimeout(callback2, -100);
|
||||
|
||||
expect(callback1).toNotHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not get confused by chained calls", function() {
|
||||
const callback2 = expect.createSpy();
|
||||
const callback1 = expect.createSpy();
|
||||
callback1.andCall(function() {
|
||||
const callback2 = jest.fn();
|
||||
const callback1 = jest.fn(function() {
|
||||
callbacks.setTimeout(callback2, 0);
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
callbacks.setTimeout(callback1);
|
||||
expect(callback1).toNotHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
// the fake timer won't actually run callbacks registered during
|
||||
@@ -140,16 +132,15 @@ describe("realtime-callbacks", function() {
|
||||
});
|
||||
|
||||
it("should be immune to exceptions", function() {
|
||||
const callback1 = expect.createSpy();
|
||||
callback1.andCall(function() {
|
||||
const callback1 = jest.fn(function() {
|
||||
throw new Error("prepare to die");
|
||||
});
|
||||
const callback2 = expect.createSpy();
|
||||
const callback2 = jest.fn();
|
||||
callbacks.setTimeout(callback1, 0);
|
||||
callbacks.setTimeout(callback2, 0);
|
||||
|
||||
expect(callback1).toNotHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
@@ -158,16 +149,16 @@ describe("realtime-callbacks", function() {
|
||||
|
||||
describe("cancelTimeout", function() {
|
||||
it("should cancel a pending timeout", function() {
|
||||
const callback = expect.createSpy();
|
||||
const callback = jest.fn();
|
||||
const k = callbacks.setTimeout(callback);
|
||||
callbacks.clearTimeout(k);
|
||||
tick(0);
|
||||
expect(callback).toNotHaveBeenCalled();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not affect sooner timeouts", function() {
|
||||
const callback1 = expect.createSpy();
|
||||
const callback2 = expect.createSpy();
|
||||
const callback1 = jest.fn();
|
||||
const callback2 = jest.fn();
|
||||
|
||||
callbacks.setTimeout(callback1, 100);
|
||||
const k = callbacks.setTimeout(callback2, 200);
|
||||
@@ -175,10 +166,10 @@ describe("realtime-callbacks", function() {
|
||||
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
|
||||
tick(150);
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,8 +4,6 @@ const sdk = require("../..");
|
||||
const RoomMember = sdk.RoomMember;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("RoomMember", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
@@ -14,7 +12,6 @@ describe("RoomMember", function() {
|
||||
let member;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
member = new RoomMember(roomId, userA);
|
||||
});
|
||||
|
||||
@@ -36,7 +33,7 @@ describe("RoomMember", function() {
|
||||
const url = member.getAvatarUrl(hsUrl);
|
||||
// we don't care about how the mxc->http conversion is done, other
|
||||
// than it contains the mxc body.
|
||||
expect(url.indexOf("flibble/wibble")).toNotEqual(-1);
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
@@ -255,9 +252,9 @@ describe("RoomMember", function() {
|
||||
member.setMembershipEvent(joinEvent);
|
||||
expect(member.name).toEqual("Alice"); // prefer displayname
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
expect(member.name).toNotEqual("Alice"); // it should disambig.
|
||||
expect(member.name).not.toEqual("Alice"); // it should disambig.
|
||||
// user_id should be there somewhere
|
||||
expect(member.name.indexOf(userA)).toNotEqual(-1);
|
||||
expect(member.name.indexOf(userA)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should emit 'RoomMember.membership' if the membership changes", function() {
|
||||
@@ -328,9 +325,9 @@ describe("RoomMember", function() {
|
||||
};
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
expect(member.name).toNotEqual("Alíce"); // it should disambig.
|
||||
expect(member.name).not.toEqual("Alíce"); // it should disambig.
|
||||
// user_id should be there somewhere
|
||||
expect(member.name.indexOf(userA)).toNotEqual(-1);
|
||||
expect(member.name.indexOf(userA)).not.toEqual(-1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,6 @@ const RoomState = sdk.RoomState;
|
||||
const RoomMember = sdk.RoomMember;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("RoomState", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
@@ -17,7 +15,6 @@ describe("RoomState", function() {
|
||||
let state;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
state = new RoomState(roomId);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ // userA joined
|
||||
@@ -49,8 +46,8 @@ describe("RoomState", function() {
|
||||
const members = state.getMembers();
|
||||
expect(members.length).toEqual(2);
|
||||
// ordering unimportant
|
||||
expect([userA, userB].indexOf(members[0].userId)).toNotEqual(-1);
|
||||
expect([userA, userB].indexOf(members[1].userId)).toNotEqual(-1);
|
||||
expect([userA, userB].indexOf(members[0].userId)).not.toEqual(-1);
|
||||
expect([userA, userB].indexOf(members[1].userId)).not.toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,8 +117,8 @@ describe("RoomState", function() {
|
||||
const events = state.getStateEvents("m.room.member");
|
||||
expect(events.length).toEqual(2);
|
||||
// ordering unimportant
|
||||
expect([userA, userB].indexOf(events[0].getStateKey())).toNotEqual(-1);
|
||||
expect([userA, userB].indexOf(events[1].getStateKey())).toNotEqual(-1);
|
||||
expect([userA, userB].indexOf(events[0].getStateKey())).not.toEqual(-1);
|
||||
expect([userA, userB].indexOf(events[1].getStateKey())).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return a single MatrixEvent if a state_key was specified",
|
||||
@@ -258,7 +255,7 @@ describe("RoomState", function() {
|
||||
});
|
||||
state.setStateEvents([memberEvent]);
|
||||
|
||||
expect(state.members[userA].setMembershipEvent).toNotHaveBeenCalled();
|
||||
expect(state.members[userA].setMembershipEvent).not.toHaveBeenCalled();
|
||||
expect(state.members[userB].setMembershipEvent).toHaveBeenCalledWith(
|
||||
memberEvent, state,
|
||||
);
|
||||
@@ -306,7 +303,7 @@ describe("RoomState", function() {
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
const memberA = state.getMember(userA);
|
||||
expect(memberA.events.member.getId()).toNotEqual(oobMemberEvent.getId());
|
||||
expect(memberA.events.member.getId()).not.toEqual(oobMemberEvent.getId());
|
||||
expect(memberA.isOutOfBand()).toEqual(false);
|
||||
});
|
||||
|
||||
|
||||
+33
-34
@@ -8,8 +8,6 @@ const EventStatus = sdk.EventStatus;
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("Room", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
@@ -19,7 +17,6 @@ describe("Room", function() {
|
||||
let room;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
room = new Room(roomId);
|
||||
// mock RoomStates
|
||||
room.oldState = room.getLiveTimeline()._startState =
|
||||
@@ -32,7 +29,7 @@ describe("Room", function() {
|
||||
const hsUrl = "https://my.home.server";
|
||||
|
||||
it("should return the URL from m.room.avatar preferentially", function() {
|
||||
room.currentState.getStateEvents.andCall(function(type, key) {
|
||||
room.currentState.getStateEvents.mockImplementation(function(type, key) {
|
||||
if (type === "m.room.avatar" && key === "") {
|
||||
return utils.mkEvent({
|
||||
event: true,
|
||||
@@ -49,7 +46,7 @@ describe("Room", function() {
|
||||
const url = room.getAvatarUrl(hsUrl);
|
||||
// we don't care about how the mxc->http conversion is done, other
|
||||
// than it contains the mxc body.
|
||||
expect(url.indexOf("flibble/wibble")).toNotEqual(-1);
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
@@ -67,13 +64,13 @@ describe("Room", function() {
|
||||
|
||||
describe("getMember", function() {
|
||||
beforeEach(function() {
|
||||
room.currentState.getMember.andCall(function(userId) {
|
||||
room.currentState.getMember.mockImplementation(function(userId) {
|
||||
return {
|
||||
"@alice:bar": {
|
||||
userId: userA,
|
||||
roomId: roomId,
|
||||
},
|
||||
}[userId];
|
||||
}[userId] || null;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,7 +79,7 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
it("should return the member from current state", function() {
|
||||
expect(room.getMember(userA)).toNotEqual(null);
|
||||
expect(room.getMember(userA)).not.toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,7 +101,7 @@ describe("Room", function() {
|
||||
user_ids: [userA],
|
||||
},
|
||||
});
|
||||
room.addLiveEvents([typing]);
|
||||
room.addEphemeralEvents([typing]);
|
||||
expect(room.currentState.setTypingEvent).toHaveBeenCalledWith(typing);
|
||||
});
|
||||
|
||||
@@ -174,7 +171,7 @@ describe("Room", function() {
|
||||
);
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
expect(room.oldState.setStateEvents).toNotHaveBeenCalled();
|
||||
expect(room.oldState.setStateEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should synthesize read receipts for the senders of events", function() {
|
||||
@@ -183,7 +180,7 @@ describe("Room", function() {
|
||||
membership: "join",
|
||||
name: "Alice",
|
||||
};
|
||||
room.currentState.getSentinelMember.andCall(function(uid) {
|
||||
room.currentState.getSentinelMember.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
@@ -292,13 +289,13 @@ describe("Room", function() {
|
||||
membership: "join",
|
||||
name: "Old Alice",
|
||||
};
|
||||
room.currentState.getSentinelMember.andCall(function(uid) {
|
||||
room.currentState.getSentinelMember.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
room.oldState.getSentinelMember.andCall(function(uid) {
|
||||
room.oldState.getSentinelMember.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
@@ -331,13 +328,13 @@ describe("Room", function() {
|
||||
membership: "join",
|
||||
name: "Old Alice",
|
||||
};
|
||||
room.currentState.getSentinelMember.andCall(function(uid) {
|
||||
room.currentState.getSentinelMember.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
room.oldState.getSentinelMember.andCall(function(uid) {
|
||||
room.oldState.getSentinelMember.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
@@ -379,7 +376,7 @@ describe("Room", function() {
|
||||
);
|
||||
expect(events[0].forwardLooking).toBe(false);
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
expect(room.currentState.setStateEvents).toNotHaveBeenCalled();
|
||||
expect(room.currentState.setStateEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -545,7 +542,7 @@ describe("Room", function() {
|
||||
|
||||
describe("getJoinedMembers", function() {
|
||||
it("should return members whose membership is 'join'", function() {
|
||||
room.currentState.getMembers.andCall(function() {
|
||||
room.currentState.getMembers.mockImplementation(function() {
|
||||
return [
|
||||
{ userId: "@alice:bar", membership: "join" },
|
||||
{ userId: "@bob:bar", membership: "invite" },
|
||||
@@ -558,7 +555,7 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
it("should return an empty list if no membership is 'join'", function() {
|
||||
room.currentState.getMembers.andCall(function() {
|
||||
room.currentState.getMembers.mockImplementation(function() {
|
||||
return [
|
||||
{ userId: "@bob:bar", membership: "invite" },
|
||||
];
|
||||
@@ -571,7 +568,7 @@ describe("Room", function() {
|
||||
describe("hasMembershipState", function() {
|
||||
it("should return true for a matching userId and membership",
|
||||
function() {
|
||||
room.currentState.getMember.andCall(function(userId) {
|
||||
room.currentState.getMember.mockImplementation(function(userId) {
|
||||
return {
|
||||
"@alice:bar": { userId: "@alice:bar", membership: "join" },
|
||||
"@bob:bar": { userId: "@bob:bar", membership: "invite" },
|
||||
@@ -582,7 +579,7 @@ describe("Room", function() {
|
||||
|
||||
it("should return false if match membership but no match userId",
|
||||
function() {
|
||||
room.currentState.getMember.andCall(function(userId) {
|
||||
room.currentState.getMember.mockImplementation(function(userId) {
|
||||
return {
|
||||
"@alice:bar": { userId: "@alice:bar", membership: "join" },
|
||||
}[userId];
|
||||
@@ -592,7 +589,7 @@ describe("Room", function() {
|
||||
|
||||
it("should return false if match userId but no match membership",
|
||||
function() {
|
||||
room.currentState.getMember.andCall(function(userId) {
|
||||
room.currentState.getMember.mockImplementation(function(userId) {
|
||||
return {
|
||||
"@alice:bar": { userId: "@alice:bar", membership: "join" },
|
||||
}[userId];
|
||||
@@ -602,7 +599,7 @@ describe("Room", function() {
|
||||
|
||||
it("should return false if no match membership or userId",
|
||||
function() {
|
||||
room.currentState.getMember.andCall(function(userId) {
|
||||
room.currentState.getMember.mockImplementation(function(userId) {
|
||||
return {
|
||||
"@alice:bar": { userId: "@alice:bar", membership: "join" },
|
||||
}[userId];
|
||||
@@ -626,7 +623,9 @@ describe("Room", function() {
|
||||
};
|
||||
const setAliases = function(aliases, stateKey) {
|
||||
if (!stateKey) {
|
||||
stateKey = "flibble";
|
||||
stateKey = aliases.length
|
||||
? aliases[0].split(':').splice(1).join(':') // domain+port
|
||||
: 'fibble';
|
||||
}
|
||||
room.addLiveEvents([utils.mkEvent({
|
||||
type: "m.room.aliases", room: roomId, skey: stateKey, content: {
|
||||
@@ -814,8 +813,8 @@ describe("Room", function() {
|
||||
addMember(userC);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name.indexOf(userB)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userC)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
expect(name.indexOf(userC)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should return the names of members in a public (public join_rules)" +
|
||||
@@ -827,8 +826,8 @@ describe("Room", function() {
|
||||
addMember(userC);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name.indexOf(userB)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userC)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
expect(name.indexOf(userC)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should show the other user's name for public (public join_rules)" +
|
||||
@@ -839,7 +838,7 @@ describe("Room", function() {
|
||||
addMember(userB);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name.indexOf(userB)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should show the other user's name for private " +
|
||||
@@ -850,7 +849,7 @@ describe("Room", function() {
|
||||
addMember(userB);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name.indexOf(userB)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should show the other user's name for private" +
|
||||
@@ -860,14 +859,14 @@ describe("Room", function() {
|
||||
addMember(userB);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name.indexOf(userB)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should show the room alias if one exists for private " +
|
||||
"(invite join_rules) rooms if a room name doesn't exist.", function() {
|
||||
const alias = "#room_alias:here";
|
||||
setJoinRule("invite");
|
||||
setAliases([alias, "#another:one"]);
|
||||
setAliases([alias, "#another:here"]);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).toEqual(alias);
|
||||
@@ -877,7 +876,7 @@ describe("Room", function() {
|
||||
"(public join_rules) rooms if a room name doesn't exist.", function() {
|
||||
const alias = "#room_alias:here";
|
||||
setJoinRule("public");
|
||||
setAliases([alias, "#another:one"]);
|
||||
setAliases([alias, "#another:here"]);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).toEqual(alias);
|
||||
@@ -1004,7 +1003,7 @@ describe("Room", function() {
|
||||
|
||||
it("should emit an event when a receipt is added",
|
||||
function() {
|
||||
const listener = expect.createSpy();
|
||||
const listener = jest.fn();
|
||||
room.on("Room.receipt", listener);
|
||||
|
||||
const ts = 13787898424;
|
||||
@@ -1175,7 +1174,7 @@ describe("Room", function() {
|
||||
it("should emit Room.tags event when new tags are " +
|
||||
"received on the event stream",
|
||||
function() {
|
||||
const listener = expect.createSpy();
|
||||
const listener = jest.fn();
|
||||
room.on("Room.tags", listener);
|
||||
|
||||
const tags = { "m.foo": { "order": 0.5 } };
|
||||
|
||||
+41
-37
@@ -8,11 +8,9 @@ const MatrixScheduler = sdk.MatrixScheduler;
|
||||
const MatrixError = sdk.MatrixError;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
import lolex from 'lolex';
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("MatrixScheduler", function() {
|
||||
let clock;
|
||||
let scheduler;
|
||||
let retryFn;
|
||||
let queueFn;
|
||||
@@ -26,8 +24,6 @@ describe("MatrixScheduler", function() {
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
clock = lolex.install();
|
||||
scheduler = new MatrixScheduler(function(ev, attempts, err) {
|
||||
if (retryFn) {
|
||||
return retryFn(ev, attempts, err);
|
||||
@@ -44,11 +40,7 @@ describe("MatrixScheduler", function() {
|
||||
defer = Promise.defer();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
clock.uninstall();
|
||||
});
|
||||
|
||||
it("should process events in a queue in a FIFO manner", function(done) {
|
||||
it("should process events in a queue in a FIFO manner", async function() {
|
||||
retryFn = function() {
|
||||
return 0;
|
||||
};
|
||||
@@ -57,28 +49,30 @@ describe("MatrixScheduler", function() {
|
||||
};
|
||||
const deferA = Promise.defer();
|
||||
const deferB = Promise.defer();
|
||||
let resolvedA = false;
|
||||
let yieldedA = false;
|
||||
scheduler.setProcessFunction(function(event) {
|
||||
if (resolvedA) {
|
||||
if (yieldedA) {
|
||||
expect(event).toEqual(eventB);
|
||||
return deferB.promise;
|
||||
} else {
|
||||
yieldedA = true;
|
||||
expect(event).toEqual(eventA);
|
||||
return deferA.promise;
|
||||
}
|
||||
});
|
||||
scheduler.queueEvent(eventA);
|
||||
scheduler.queueEvent(eventB).done(function() {
|
||||
expect(resolvedA).toBe(true);
|
||||
done();
|
||||
});
|
||||
deferA.resolve({});
|
||||
resolvedA = true;
|
||||
deferB.resolve({});
|
||||
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);
|
||||
});
|
||||
|
||||
it("should invoke the retryFn on failure and wait the amount of time specified",
|
||||
function(done) {
|
||||
async function() {
|
||||
const waitTimeMs = 1500;
|
||||
const retryDefer = Promise.defer();
|
||||
retryFn = function() {
|
||||
@@ -97,24 +91,26 @@ describe("MatrixScheduler", function() {
|
||||
return defer.promise;
|
||||
} else if (procCount === 2) {
|
||||
// don't care about this defer
|
||||
return Promise.defer().promise;
|
||||
return new 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({});
|
||||
retryDefer.promise.done(function() {
|
||||
expect(procCount).toEqual(1);
|
||||
clock.tick(waitTimeMs);
|
||||
expect(procCount).toEqual(2);
|
||||
done();
|
||||
});
|
||||
await retryDefer.promise;
|
||||
expect(procCount).toEqual(1);
|
||||
jest.advanceTimersByTime(waitTimeMs);
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(2);
|
||||
});
|
||||
|
||||
it("should give up if the retryFn on failure returns -1 and try the next event",
|
||||
function(done) {
|
||||
async function() {
|
||||
// Queue A & B.
|
||||
// Reject A and return -1 on retry.
|
||||
// Expect B to be tried next and the promise for A to be rejected.
|
||||
@@ -122,8 +118,8 @@ describe("MatrixScheduler", function() {
|
||||
return -1;
|
||||
};
|
||||
queueFn = function() {
|
||||
return "yep";
|
||||
};
|
||||
return "yep";
|
||||
};
|
||||
|
||||
const deferA = Promise.defer();
|
||||
const deferB = Promise.defer();
|
||||
@@ -142,13 +138,17 @@ 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({});
|
||||
globalA.catch(function() {
|
||||
try {
|
||||
await globalA;
|
||||
} catch(err) {
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(2);
|
||||
done();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("should treat each queue separately", function(done) {
|
||||
@@ -195,7 +195,7 @@ describe("MatrixScheduler", function() {
|
||||
setTimeout(function() {
|
||||
deferA.resolve({});
|
||||
}, 1000);
|
||||
clock.tick(1000);
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
describe("queueEvent", function() {
|
||||
@@ -300,7 +300,11 @@ describe("MatrixScheduler", function() {
|
||||
expect(ev).toEqual(eventA);
|
||||
return defer.promise;
|
||||
});
|
||||
expect(procCount).toEqual(1);
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call the processFn if there are no queued events", function() {
|
||||
|
||||
@@ -16,9 +16,7 @@ limitations under the License.
|
||||
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
import utils from "../test-utils";
|
||||
import sdk from "../..";
|
||||
import expect from 'expect';
|
||||
|
||||
const SyncAccumulator = sdk.SyncAccumulator;
|
||||
|
||||
@@ -26,7 +24,6 @@ describe("SyncAccumulator", function() {
|
||||
let sa;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
sa = new SyncAccumulator({
|
||||
maxTimelineEntries: 10,
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ const TimelineWindow = sdk.TimelineWindow;
|
||||
const TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
|
||||
|
||||
const utils = require("../test-utils");
|
||||
import expect from 'expect';
|
||||
|
||||
const ROOM_ID = "roomId";
|
||||
const USER_ID = "userId";
|
||||
@@ -67,10 +66,6 @@ function createLinkedTimelines() {
|
||||
|
||||
|
||||
describe("TimelineIndex", function() {
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("minIndex", function() {
|
||||
it("should return the min index relative to BaseIndex", function() {
|
||||
const timelineIndex = new TimelineIndex(createTimeline(), 0);
|
||||
@@ -153,7 +148,7 @@ describe("TimelineWindow", function() {
|
||||
let timelineSet;
|
||||
let client;
|
||||
function createWindow(timeline, opts) {
|
||||
timelineSet = {};
|
||||
timelineSet = {getTimelineForEvent: () => null};
|
||||
client = {};
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
@@ -163,12 +158,8 @@ describe("TimelineWindow", function() {
|
||||
return new TimelineWindow(client, timelineSet, opts);
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("load", function() {
|
||||
it("should initialise from the live timeline", function(done) {
|
||||
it("should initialise from the live timeline", function() {
|
||||
const liveTimeline = createTimeline();
|
||||
const room = {};
|
||||
room.getLiveTimeline = function() {
|
||||
@@ -176,17 +167,17 @@ describe("TimelineWindow", function() {
|
||||
};
|
||||
|
||||
const timelineWindow = new TimelineWindow(undefined, room);
|
||||
timelineWindow.load(undefined, 2).then(function() {
|
||||
return timelineWindow.load(undefined, 2).then(function() {
|
||||
const expectedEvents = liveTimeline.getEvents().slice(1);
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should initialise from a specific event", function(done) {
|
||||
it("should initialise from a specific event", function() {
|
||||
const timeline = createTimeline();
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
const timelineSet = {};
|
||||
const timelineSet = {getTimelineForEvent: () => null};
|
||||
const client = {};
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
@@ -195,21 +186,20 @@ describe("TimelineWindow", function() {
|
||||
};
|
||||
|
||||
const timelineWindow = new TimelineWindow(client, timelineSet);
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("canPaginate should return false until load has returned",
|
||||
function(done) {
|
||||
it("canPaginate should return false until load has returned", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok1", EventTimeline.BACKWARDS);
|
||||
timeline.setPaginationToken("toktok2", EventTimeline.FORWARDS);
|
||||
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
const timelineSet = {};
|
||||
const timelineSet = {getTimelineForEvent: () => null};
|
||||
const client = {};
|
||||
|
||||
const timelineWindow = new TimelineWindow(client, timelineSet);
|
||||
@@ -222,25 +212,24 @@ describe("TimelineWindow", function() {
|
||||
return Promise.resolve(timeline);
|
||||
};
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(true);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(true);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pagination", function() {
|
||||
it("should be able to advance across the initial timeline",
|
||||
function(done) {
|
||||
it("should be able to advance across the initial timeline", function() {
|
||||
const timeline = createTimeline();
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
const timelineWindow = createWindow(timeline);
|
||||
|
||||
timelineWindow.load(eventId, 1).then(function() {
|
||||
return timelineWindow.load(eventId, 1).then(function() {
|
||||
const expectedEvents = [timeline.getEvents()[1]];
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
@@ -277,15 +266,15 @@ describe("TimelineWindow", function() {
|
||||
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(false);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should advance into next timeline", function(done) {
|
||||
it("should advance into next timeline", function() {
|
||||
const tls = createLinkedTimelines();
|
||||
const eventId = tls[0].getEvents()[1].getId();
|
||||
const timelineWindow = createWindow(tls[0], {windowLimit: 5});
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = tls[0].getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
@@ -322,15 +311,15 @@ describe("TimelineWindow", function() {
|
||||
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(false);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should retreat into previous timeline", function(done) {
|
||||
it("should retreat into previous timeline", function() {
|
||||
const tls = createLinkedTimelines();
|
||||
const eventId = tls[1].getEvents()[1].getId();
|
||||
const timelineWindow = createWindow(tls[1], {windowLimit: 5});
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = tls[1].getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
@@ -367,10 +356,10 @@ describe("TimelineWindow", function() {
|
||||
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(false);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should make forward pagination requests", function(done) {
|
||||
it("should make forward pagination requests", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
|
||||
|
||||
@@ -386,7 +375,7 @@ describe("TimelineWindow", function() {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
@@ -399,11 +388,11 @@ describe("TimelineWindow", function() {
|
||||
expect(success).toBe(true);
|
||||
const expectedEvents = timeline.getEvents().slice(0, 5);
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should make backward pagination requests", function(done) {
|
||||
it("should make backward pagination requests", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS);
|
||||
|
||||
@@ -419,7 +408,7 @@ describe("TimelineWindow", function() {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
@@ -432,11 +421,10 @@ describe("TimelineWindow", function() {
|
||||
expect(success).toBe(true);
|
||||
const expectedEvents = timeline.getEvents().slice(1, 6);
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should limit the number of unsuccessful pagination requests",
|
||||
function(done) {
|
||||
it("should limit the number of unsuccessful pagination requests", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
|
||||
|
||||
@@ -452,7 +440,7 @@ describe("TimelineWindow", function() {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
@@ -471,7 +459,7 @@ describe("TimelineWindow", function() {
|
||||
.toBe(false);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(true);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,14 +4,11 @@ const sdk = require("../..");
|
||||
const User = sdk.User;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("User", function() {
|
||||
const userId = "@alice:bar";
|
||||
let user;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
user = new User(userId);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const utils = require("../../lib/utils");
|
||||
const testUtils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("utils", function() {
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("encodeParams", function() {
|
||||
it("should url encode and concat with &s", function() {
|
||||
const params = {
|
||||
@@ -135,7 +128,7 @@ describe("utils", function() {
|
||||
utils.checkObjectHasKeys({
|
||||
foo: "bar",
|
||||
}, ["foo"]);
|
||||
}).toNotThrow();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,7 +145,7 @@ describe("utils", function() {
|
||||
utils.checkObjectHasNoAdditionalKeys({
|
||||
foo: "bar",
|
||||
}, ["foo"]);
|
||||
}).toNotThrow();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+7
-1
@@ -34,12 +34,18 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
+35
-14
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 New Vector 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.
|
||||
@@ -17,7 +18,7 @@ limitations under the License.
|
||||
/** @module auto-discovery */
|
||||
|
||||
import Promise from 'bluebird';
|
||||
const logger = require("./logger");
|
||||
import logger from './logger';
|
||||
import { URL as NodeURL } from "url";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
@@ -256,6 +257,11 @@ export class AutoDiscovery {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -270,21 +276,11 @@ export class AutoDiscovery {
|
||||
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.
|
||||
// in this branch.
|
||||
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.homeserver": clientConfig["m.homeserver"],
|
||||
"m.identity_server": {
|
||||
state: AutoDiscovery.FAIL_ERROR,
|
||||
state: AutoDiscovery.FAIL_PROMPT,
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
base_url: null,
|
||||
},
|
||||
@@ -311,6 +307,11 @@ export class AutoDiscovery {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -419,6 +420,26 @@ export class AutoDiscovery {
|
||||
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");
|
||||
}
|
||||
|
||||
const response = await this._fetchWellKnownObject(
|
||||
`https://${domain}/.well-known/matrix/client`,
|
||||
);
|
||||
if (!response) return {};
|
||||
return response.raw || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
|
||||
* is suitable for the requirements laid out by .well-known auto discovery.
|
||||
|
||||
+660
-105
File diff suppressed because it is too large
Load Diff
+1016
-233
File diff suppressed because it is too large
Load Diff
+6
-5
@@ -47,14 +47,14 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
let serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
let prefix = "/_matrix/media/v1/download/";
|
||||
let prefix = "/_matrix/media/r0/download/";
|
||||
const params = {};
|
||||
|
||||
if (width) {
|
||||
params.width = width;
|
||||
params.width = Math.round(width);
|
||||
}
|
||||
if (height) {
|
||||
params.height = height;
|
||||
params.height = Math.round(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/v1/thumbnail/";
|
||||
prefix = "/_matrix/media/r0/thumbnail/";
|
||||
}
|
||||
|
||||
const fragmentOffset = serverAndMediaId.indexOf("#");
|
||||
@@ -83,6 +83,7 @@ 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) {
|
||||
@@ -99,7 +100,7 @@ module.exports = {
|
||||
height: height,
|
||||
};
|
||||
|
||||
const path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
|
||||
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
|
||||
$ident: identiconString,
|
||||
});
|
||||
return baseUrl + path +
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
/*
|
||||
Copyright 2019 New Vector 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.
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Cross signing methods
|
||||
* @module crypto/CrossSigning
|
||||
*/
|
||||
|
||||
import {pkSign, pkVerify} from './olmlib';
|
||||
import {EventEmitter} from 'events';
|
||||
import logger from '../logger';
|
||||
|
||||
function publicKeyFromKeyInfo(keyInfo) {
|
||||
return Object.entries(keyInfo.keys)[0];
|
||||
}
|
||||
|
||||
export class CrossSigningInfo extends EventEmitter {
|
||||
/**
|
||||
* Information about a user's cross-signing keys
|
||||
*
|
||||
* @class
|
||||
*
|
||||
* @param {string} userId the user that the information is about
|
||||
* @param {object} callbacks Callbacks used to interact with the app
|
||||
* Requires getCrossSigningKey and saveCrossSigningKeys
|
||||
*/
|
||||
constructor(userId, callbacks) {
|
||||
super();
|
||||
|
||||
// you can't change the userId
|
||||
Object.defineProperty(this, 'userId', {
|
||||
enumerable: true,
|
||||
value: userId,
|
||||
});
|
||||
this._callbacks = callbacks || {};
|
||||
this.keys = {};
|
||||
this.firstUse = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the app callback to ask for a private key
|
||||
* @param {string} type The key type ("master", "self_signing", or "user_signing")
|
||||
* @param {Uint8Array} expectedPubkey The matching public key or undefined to use
|
||||
* the stored public key for the given key type.
|
||||
*/
|
||||
async getCrossSigningKey(type, expectedPubkey) {
|
||||
if (!this._callbacks.getCrossSigningKey) {
|
||||
throw new Error("No getCrossSigningKey callback supplied");
|
||||
}
|
||||
|
||||
if (expectedPubkey === undefined) {
|
||||
expectedPubkey = this.getId(type);
|
||||
}
|
||||
|
||||
const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
|
||||
if (!privkey) {
|
||||
throw new Error(
|
||||
"getCrossSigningKey callback for " + type + " returned falsey",
|
||||
);
|
||||
}
|
||||
const signing = new global.Olm.PkSigning();
|
||||
const gotPubkey = signing.init_with_seed(privkey);
|
||||
if (gotPubkey !== expectedPubkey) {
|
||||
signing.free();
|
||||
throw new Error(
|
||||
"Key type " + type + " from getCrossSigningKey callback did not match",
|
||||
);
|
||||
} else {
|
||||
return [gotPubkey, signing];
|
||||
}
|
||||
}
|
||||
|
||||
static fromStorage(obj, userId) {
|
||||
const res = new CrossSigningInfo(userId);
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
res[prop] = obj[prop];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
toStorage() {
|
||||
return {
|
||||
keys: this.keys,
|
||||
firstUse: this.firstUse,
|
||||
};
|
||||
}
|
||||
|
||||
hasKeys() {
|
||||
return Object.keys(this.keys).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID used to identify the user
|
||||
*
|
||||
* @param {string} type The type of key to get the ID of. One of "master",
|
||||
* "self_signing", or "user_signing". Defaults to "master".
|
||||
*
|
||||
* @return {string} the ID
|
||||
*/
|
||||
getId(type) {
|
||||
type = type || "master";
|
||||
if (!this.keys[type]) return null;
|
||||
const keyInfo = this.keys[type];
|
||||
return publicKeyFromKeyInfo(keyInfo)[1];
|
||||
}
|
||||
|
||||
|
||||
async resetKeys(level) {
|
||||
if (!this._callbacks.saveCrossSigningKeys) {
|
||||
throw new Error("No saveCrossSigningKeys callback supplied");
|
||||
}
|
||||
|
||||
// If we're resetting the master key, we reset all keys
|
||||
if (
|
||||
level === undefined ||
|
||||
level & CrossSigningLevel.MASTER ||
|
||||
!this.keys.master
|
||||
) {
|
||||
level = (
|
||||
CrossSigningLevel.MASTER |
|
||||
CrossSigningLevel.USER_SIGNING |
|
||||
CrossSigningLevel.SELF_SIGNING
|
||||
);
|
||||
} else if (level === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const privateKeys = {};
|
||||
const keys = {};
|
||||
let masterSigning;
|
||||
let masterPub;
|
||||
|
||||
try {
|
||||
if (level & CrossSigningLevel.MASTER) {
|
||||
masterSigning = new global.Olm.PkSigning();
|
||||
privateKeys.master = masterSigning.generate_seed();
|
||||
masterPub = masterSigning.init_with_seed(privateKeys.master);
|
||||
keys.master = {
|
||||
user_id: this.userId,
|
||||
usage: ['master'],
|
||||
keys: {
|
||||
['ed25519:' + masterPub]: masterPub,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
[masterPub, masterSigning] = await this.getCrossSigningyKey("master");
|
||||
}
|
||||
|
||||
if (level & CrossSigningLevel.SELF_SIGNING) {
|
||||
const sskSigning = new global.Olm.PkSigning();
|
||||
try {
|
||||
privateKeys.self_signing = sskSigning.generate_seed();
|
||||
const sskPub = sskSigning.init_with_seed(privateKeys.self_signing);
|
||||
keys.self_signing = {
|
||||
user_id: this.userId,
|
||||
usage: ['self_signing'],
|
||||
keys: {
|
||||
['ed25519:' + sskPub]: sskPub,
|
||||
},
|
||||
};
|
||||
pkSign(keys.self_signing, masterSigning, this.userId, masterPub);
|
||||
} finally {
|
||||
sskSigning.free();
|
||||
}
|
||||
}
|
||||
|
||||
if (level & CrossSigningLevel.USER_SIGNING) {
|
||||
const uskSigning = new global.Olm.PkSigning();
|
||||
try {
|
||||
privateKeys.user_signing = uskSigning.generate_seed();
|
||||
const uskPub = uskSigning.init_with_seed(privateKeys.user_signing);
|
||||
keys.user_signing = {
|
||||
user_id: this.userId,
|
||||
usage: ['user_signing'],
|
||||
keys: {
|
||||
['ed25519:' + uskPub]: uskPub,
|
||||
},
|
||||
};
|
||||
pkSign(keys.user_signing, masterSigning, this.userId, masterPub);
|
||||
} finally {
|
||||
uskSigning.free();
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(this.keys, keys);
|
||||
this._callbacks.saveCrossSigningKeys(privateKeys);
|
||||
} finally {
|
||||
if (masterSigning) {
|
||||
masterSigning.free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setKeys(keys) {
|
||||
const signingKeys = {};
|
||||
if (keys.master) {
|
||||
if (keys.master.user_id !== this.userId) {
|
||||
const error = "Mismatched user ID " + keys.master.user_id +
|
||||
" in master key from " + this.userId;
|
||||
logger.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
if (!this.keys.master) {
|
||||
// this is the first key we've seen, so first-use is true
|
||||
this.firstUse = true;
|
||||
} else if (publicKeyFromKeyInfo(keys.master)[1] !== this.getId()) {
|
||||
// this is a different key, so first-use is false
|
||||
this.firstUse = false;
|
||||
} // otherwise, same key, so no change
|
||||
signingKeys.master = keys.master;
|
||||
} else if (this.keys.master) {
|
||||
signingKeys.master = this.keys.master;
|
||||
} else {
|
||||
throw new Error("Tried to set cross-signing keys without a master key");
|
||||
}
|
||||
const masterKey = publicKeyFromKeyInfo(signingKeys.master)[1];
|
||||
|
||||
// verify signatures
|
||||
if (keys.user_signing) {
|
||||
if (keys.user_signing.user_id !== this.userId) {
|
||||
const error = "Mismatched user ID " + keys.master.user_id +
|
||||
" in user_signing key from " + this.userId;
|
||||
logger.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
try {
|
||||
pkVerify(keys.user_signing, masterKey, this.userId);
|
||||
} catch (e) {
|
||||
logger.error("invalid signature on user-signing key");
|
||||
// FIXME: what do we want to do here?
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (keys.self_signing) {
|
||||
if (keys.self_signing.user_id !== this.userId) {
|
||||
const error = "Mismatched user ID " + keys.master.user_id +
|
||||
" in self_signing key from " + this.userId;
|
||||
logger.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
try {
|
||||
pkVerify(keys.self_signing, masterKey, this.userId);
|
||||
} catch (e) {
|
||||
logger.error("invalid signature on self-signing key");
|
||||
// FIXME: what do we want to do here?
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// if everything checks out, then save the keys
|
||||
if (keys.master) {
|
||||
this.keys.master = keys.master;
|
||||
// if the master key is set, then the old self-signing and
|
||||
// user-signing keys are obsolete
|
||||
delete this.keys.self_signing;
|
||||
delete this.keys.user_signing;
|
||||
}
|
||||
if (keys.self_signing) {
|
||||
this.keys.self_signing = keys.self_signing;
|
||||
}
|
||||
if (keys.user_signing) {
|
||||
this.keys.user_signing = keys.user_signing;
|
||||
}
|
||||
}
|
||||
|
||||
async signObject(data, type) {
|
||||
if (!this.keys[type]) {
|
||||
throw new Error(
|
||||
"Attempted to sign with " + type + " key but no such key present",
|
||||
);
|
||||
}
|
||||
const [pubkey, signing] = await this.getCrossSigningKey(type);
|
||||
try {
|
||||
pkSign(data, signing, this.userId, pubkey);
|
||||
return data;
|
||||
} finally {
|
||||
signing.free();
|
||||
}
|
||||
}
|
||||
|
||||
async signUser(key) {
|
||||
if (!this.keys.user_signing) {
|
||||
return;
|
||||
}
|
||||
return this.signObject(key.keys.master, "user_signing");
|
||||
}
|
||||
|
||||
async signDevice(userId, device) {
|
||||
if (userId !== this.userId) {
|
||||
throw new Error(
|
||||
`Trying to sign ${userId}'s device; can only sign our own device`,
|
||||
);
|
||||
}
|
||||
if (!this.keys.self_signing) {
|
||||
return;
|
||||
}
|
||||
return this.signObject(
|
||||
{
|
||||
algorithms: device.algorithms,
|
||||
keys: device.keys,
|
||||
device_id: device.deviceId,
|
||||
user_id: userId,
|
||||
}, "self_signing",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given user is trusted.
|
||||
*
|
||||
* @param {CrossSigningInfo} userCrossSigning Cross signing info for user
|
||||
*
|
||||
* @returns {UserTrustLevel}
|
||||
*/
|
||||
checkUserTrust(userCrossSigning) {
|
||||
// if we're checking our own key, then it's trusted if the master key
|
||||
// and self-signing key match
|
||||
if (this.userId === userCrossSigning.userId
|
||||
&& this.getId() && this.getId() === userCrossSigning.getId()
|
||||
&& this.getId("self_signing")
|
||||
&& this.getId("self_signing") === userCrossSigning.getId("self_signing")
|
||||
) {
|
||||
return new UserTrustLevel(true, this.firstUse);
|
||||
}
|
||||
|
||||
if (!this.keys.user_signing) {
|
||||
// If there's no user signing key, they can't possibly be verified.
|
||||
// They may be TOFU trusted though.
|
||||
return new UserTrustLevel(false, userCrossSigning.firstUse);
|
||||
}
|
||||
|
||||
let userTrusted;
|
||||
const userMaster = userCrossSigning.keys.master;
|
||||
const uskId = this.getId('user_signing');
|
||||
try {
|
||||
pkVerify(userMaster, uskId, this.userId);
|
||||
userTrusted = true;
|
||||
} catch (e) {
|
||||
userTrusted = false;
|
||||
}
|
||||
return new UserTrustLevel(userTrusted, userCrossSigning.firstUse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given device is trusted.
|
||||
*
|
||||
* @param {CrossSigningInfo} userCrossSigning Cross signing info for user
|
||||
* @param {module:crypto/deviceinfo} device The device to check
|
||||
* @param {bool} localTrust Whether the device is trusted locally
|
||||
*
|
||||
* @returns {DeviceTrustLevel}
|
||||
*/
|
||||
checkDeviceTrust(userCrossSigning, device, localTrust) {
|
||||
const userTrust = this.checkUserTrust(userCrossSigning);
|
||||
|
||||
const userSSK = userCrossSigning.keys.self_signing;
|
||||
if (!userSSK) {
|
||||
// if the user has no self-signing key then we cannot make any
|
||||
// trust assertions about this device from cross-signing
|
||||
return new DeviceTrustLevel(false, false, localTrust);
|
||||
}
|
||||
|
||||
const deviceObj = deviceToObject(device, userCrossSigning.userId);
|
||||
try {
|
||||
// if we can verify the user's SSK from their master key...
|
||||
pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId);
|
||||
// ...and this device's key from their SSK...
|
||||
pkVerify(
|
||||
deviceObj, publicKeyFromKeyInfo(userSSK)[1], userCrossSigning.userId,
|
||||
);
|
||||
// ...then we trust this device as much as far as we trust the user
|
||||
return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust);
|
||||
} catch (e) {
|
||||
return new DeviceTrustLevel(false, false, localTrust);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deviceToObject(device, userId) {
|
||||
return {
|
||||
algorithms: device.algorithms,
|
||||
keys: device.keys,
|
||||
device_id: device.deviceId,
|
||||
user_id: userId,
|
||||
signatures: device.signatures,
|
||||
};
|
||||
}
|
||||
|
||||
export const CrossSigningLevel = {
|
||||
MASTER: 4,
|
||||
USER_SIGNING: 2,
|
||||
SELF_SIGNING: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents the ways in which we trust a user
|
||||
*/
|
||||
export class UserTrustLevel {
|
||||
constructor(crossSigningVerified, tofu) {
|
||||
this._crossSigningVerified = crossSigningVerified;
|
||||
this._tofu = tofu;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this user is verified via any means
|
||||
*/
|
||||
isVerified() {
|
||||
return this.isCrossSigningVerified();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this user is verified via cross signing
|
||||
*/
|
||||
isCrossSigningVerified() {
|
||||
return this._crossSigningVerified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this user's key is trusted on first use
|
||||
*/
|
||||
isTofu() {
|
||||
return this._tofu;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the ways in which we trust a device
|
||||
*/
|
||||
export class DeviceTrustLevel {
|
||||
constructor(crossSigningVerified, tofu, localVerified) {
|
||||
this._crossSigningVerified = crossSigningVerified;
|
||||
this._tofu = tofu;
|
||||
this._localVerified = localVerified;
|
||||
}
|
||||
|
||||
static fromUserTrustLevel(userTrustLevel, localVerified) {
|
||||
return new DeviceTrustLevel(
|
||||
userTrustLevel._crossSigningVerified,
|
||||
userTrustLevel._tofu,
|
||||
localVerified,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this device is verified via any means
|
||||
*/
|
||||
isVerified() {
|
||||
return this.isCrossSigningVerified() || this.isLocallyVerified();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this device is verified via cross signing
|
||||
*/
|
||||
isCrossSigningVerified() {
|
||||
return this._crossSigningVerified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this device is verified locally
|
||||
*/
|
||||
isLocallyVerified() {
|
||||
return this._localVerified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this device is trusted from a user's key
|
||||
* that is trusted on first use
|
||||
*/
|
||||
isTofu() {
|
||||
return this._tofu;
|
||||
}
|
||||
}
|
||||
+89
-21
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector 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,11 +24,14 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
import logger from '../logger';
|
||||
import DeviceInfo from './deviceinfo';
|
||||
import {CrossSigningInfo} from './CrossSigning';
|
||||
import olmlib from './olmlib';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
import {sleep} from '../utils';
|
||||
|
||||
|
||||
/* State transition diagram for DeviceList._deviceTrackingStatus
|
||||
@@ -60,8 +64,10 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
|
||||
/**
|
||||
* @alias module:crypto/DeviceList
|
||||
*/
|
||||
export default class DeviceList {
|
||||
export default class DeviceList extends EventEmitter {
|
||||
constructor(baseApis, cryptoStore, olmDevice) {
|
||||
super();
|
||||
|
||||
this._cryptoStore = cryptoStore;
|
||||
|
||||
// userId -> {
|
||||
@@ -71,6 +77,11 @@ export default class DeviceList {
|
||||
// }
|
||||
this._devices = {};
|
||||
|
||||
// userId -> {
|
||||
// [key info]
|
||||
// }
|
||||
this._crossSigningInfo = {};
|
||||
|
||||
// map of identity keys to the user who owns it
|
||||
this._userByIdentityKey = {};
|
||||
|
||||
@@ -111,6 +122,7 @@ export default class DeviceList {
|
||||
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
|
||||
this._devices = deviceData ? deviceData.devices : {},
|
||||
this._ssks = deviceData ? deviceData.self_signing_keys || {} : {};
|
||||
this._deviceTrackingStatus = deviceData ?
|
||||
deviceData.trackingStatus : {};
|
||||
this._syncToken = deviceData ? deviceData.syncToken : null;
|
||||
@@ -201,6 +213,7 @@ export default class DeviceList {
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||
this._cryptoStore.storeEndToEndDeviceData({
|
||||
devices: this._devices,
|
||||
self_signing_keys: this._ssks,
|
||||
trackingStatus: this._deviceTrackingStatus,
|
||||
syncToken: this._syncToken,
|
||||
}, txn);
|
||||
@@ -334,6 +347,17 @@ export default class DeviceList {
|
||||
return this._devices[userId];
|
||||
}
|
||||
|
||||
getStoredCrossSigningForUser(userId) {
|
||||
if (!this._crossSigningInfo[userId]) return null;
|
||||
|
||||
return CrossSigningInfo.fromStorage(this._crossSigningInfo[userId], userId);
|
||||
}
|
||||
|
||||
storeCrossSigningForUser(userId, info) {
|
||||
this._crossSigningInfo[userId] = info;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored keys for a single device
|
||||
*
|
||||
@@ -561,6 +585,10 @@ export default class DeviceList {
|
||||
}
|
||||
}
|
||||
|
||||
setRawStoredCrossSigningForUser(userId, info) {
|
||||
this._crossSigningInfo[userId] = info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire off download update requests for the given users, and update the
|
||||
* device list tracking status for them, and the
|
||||
@@ -624,6 +652,7 @@ export default class DeviceList {
|
||||
}
|
||||
});
|
||||
this.saveIfDirty();
|
||||
this.emit("crypto.devicesUpdated", users);
|
||||
};
|
||||
|
||||
return prom;
|
||||
@@ -724,6 +753,9 @@ class DeviceListUpdateSerialiser {
|
||||
downloadUsers, opts,
|
||||
).then((res) => {
|
||||
const dk = res.device_keys || {};
|
||||
const masterKeys = res.master_keys || {};
|
||||
const ssks = res.self_signing_keys || {};
|
||||
const usks = res.user_signing_keys || {};
|
||||
|
||||
// do each user in a separate promise, to avoid wedging the CPU
|
||||
// (https://github.com/vector-im/riot-web/issues/3158)
|
||||
@@ -732,8 +764,14 @@ class DeviceListUpdateSerialiser {
|
||||
// this serves as an easy solution for now.
|
||||
let prom = Promise.resolve();
|
||||
for (const userId of downloadUsers) {
|
||||
prom = prom.delay(5).then(() => {
|
||||
return this._processQueryResponseForUser(userId, dk[userId]);
|
||||
prom = prom.then(sleep(5)).then(() => {
|
||||
return this._processQueryResponseForUser(
|
||||
userId, dk[userId], {
|
||||
master: masterKeys[userId],
|
||||
self_signing: ssks[userId],
|
||||
user_signing: usks[userId],
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -757,30 +795,58 @@ class DeviceListUpdateSerialiser {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
async _processQueryResponseForUser(userId, response) {
|
||||
logger.log('got keys for ' + userId + ':', response);
|
||||
async _processQueryResponseForUser(
|
||||
userId, dkResponse, crossSigningResponse, sskResponse,
|
||||
) {
|
||||
logger.log('got device keys for ' + userId + ':', dkResponse);
|
||||
logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse);
|
||||
|
||||
// map from deviceid -> deviceinfo for this user
|
||||
const userStore = {};
|
||||
const devs = this._deviceList.getRawStoredDevicesForUser(userId);
|
||||
if (devs) {
|
||||
Object.keys(devs).forEach((deviceId) => {
|
||||
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
||||
userStore[deviceId] = d;
|
||||
{
|
||||
// map from deviceid -> deviceinfo for this user
|
||||
const userStore = {};
|
||||
const devs = this._deviceList.getRawStoredDevicesForUser(userId);
|
||||
if (devs) {
|
||||
Object.keys(devs).forEach((deviceId) => {
|
||||
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
||||
userStore[deviceId] = d;
|
||||
});
|
||||
}
|
||||
|
||||
await _updateStoredDeviceKeysForUser(
|
||||
this._olmDevice, userId, userStore, dkResponse || {},
|
||||
);
|
||||
|
||||
// put the updates into the object that will be returned as our results
|
||||
const storage = {};
|
||||
Object.keys(userStore).forEach((deviceId) => {
|
||||
storage[deviceId] = userStore[deviceId].toStorage();
|
||||
});
|
||||
|
||||
this._deviceList._setRawStoredDevicesForUser(userId, storage);
|
||||
}
|
||||
|
||||
await _updateStoredDeviceKeysForUser(
|
||||
this._olmDevice, userId, userStore, response || {},
|
||||
);
|
||||
// now do the same for the cross-signing keys
|
||||
{
|
||||
// FIXME: should we be ignoring empty cross-signing responses, or
|
||||
// should we be dropping the keys?
|
||||
if (crossSigningResponse
|
||||
&& (crossSigningResponse.master || crossSigningResponse.self_signing
|
||||
|| crossSigningResponse.user_signing)) {
|
||||
const crossSigning
|
||||
= this._deviceList.getStoredCrossSigningForUser(userId)
|
||||
|| new CrossSigningInfo(userId);
|
||||
|
||||
// put the updates into thr object that will be returned as our results
|
||||
const storage = {};
|
||||
Object.keys(userStore).forEach((deviceId) => {
|
||||
storage[deviceId] = userStore[deviceId].toStorage();
|
||||
});
|
||||
crossSigning.setKeys(crossSigningResponse);
|
||||
|
||||
this._deviceList._setRawStoredDevicesForUser(userId, storage);
|
||||
this._deviceList.setRawStoredCrossSigningForUser(
|
||||
userId, crossSigning.toStorage(),
|
||||
);
|
||||
|
||||
// NB. Unlike most events in the js-sdk, this one is internal to the
|
||||
// js-sdk and is not re-emitted
|
||||
this._deviceList.emit('userCrossSigningUpdated', userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,6 +920,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
||||
}
|
||||
|
||||
const unsigned = deviceResult.unsigned || {};
|
||||
const signatures = deviceResult.signatures || {};
|
||||
|
||||
try {
|
||||
await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
|
||||
@@ -886,5 +953,6 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
||||
deviceStore.keys = deviceResult.keys || {};
|
||||
deviceStore.algorithms = deviceResult.algorithms || [];
|
||||
deviceStore.unsigned = unsigned;
|
||||
deviceStore.signatures = signatures;
|
||||
return true;
|
||||
}
|
||||
|
||||
+17
-3
@@ -462,7 +462,7 @@ OlmDevice.prototype.createInboundSession = async function(
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
|
||||
if (this._sessionsInProgress[theirDeviceIdentityKey]) {
|
||||
console.log("waiting for session to be created");
|
||||
logger.log("waiting for olm session to be created");
|
||||
try {
|
||||
await this._sessionsInProgress[theirDeviceIdentityKey];
|
||||
} catch (e) {
|
||||
@@ -543,7 +543,7 @@ OlmDevice.prototype.getSessionIdForDevice = async function(
|
||||
*/
|
||||
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey, nowait) {
|
||||
if (this._sessionsInProgress[deviceIdentityKey] && !nowait) {
|
||||
logger.log("waiting for session to be created");
|
||||
logger.log("waiting for olm session to be created");
|
||||
try {
|
||||
await this._sessionsInProgress[deviceIdentityKey];
|
||||
} catch (e) {
|
||||
@@ -594,6 +594,11 @@ OlmDevice.prototype.encryptMessage = async function(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
|
||||
(txn) => {
|
||||
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
|
||||
const sessionDesc = sessionInfo.session.describe();
|
||||
logger.log(
|
||||
"encryptMessage: Olm Session ID " + sessionId + " to " +
|
||||
theirDeviceIdentityKey + ": " + sessionDesc,
|
||||
);
|
||||
res = sessionInfo.session.encrypt(payloadString);
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
});
|
||||
@@ -621,6 +626,11 @@ OlmDevice.prototype.decryptMessage = async function(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
|
||||
(txn) => {
|
||||
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
|
||||
const sessionDesc = sessionInfo.session.describe();
|
||||
logger.log(
|
||||
"decryptMessage: Olm Session ID " + sessionId + " from " +
|
||||
theirDeviceIdentityKey + ": " + sessionDesc,
|
||||
);
|
||||
payloadString = sessionInfo.session.decrypt(messageType, ciphertext);
|
||||
sessionInfo.lastReceivedMessageTs = Date.now();
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
@@ -730,6 +740,8 @@ OlmDevice.prototype.createOutboundGroupSession = function() {
|
||||
OlmDevice.prototype.encryptGroupMessage = function(sessionId, payloadString) {
|
||||
const self = this;
|
||||
|
||||
logger.log(`encrypting msg with megolm session ${sessionId}`);
|
||||
|
||||
checkPayloadLength(payloadString);
|
||||
|
||||
return this._getOutboundGroupSession(sessionId, function(session) {
|
||||
@@ -876,7 +888,9 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
<= session.first_known_index()) {
|
||||
// existing session has lower index (i.e. can
|
||||
// decrypt more), so keep it
|
||||
logger.log("Keeping existing session");
|
||||
logger.log(
|
||||
`Keeping existing megolm session ${sessionId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,529 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
import logger from '../logger';
|
||||
import olmlib from './olmlib';
|
||||
import { randomString } from '../randomstring';
|
||||
import { keyFromPassphrase } from './key_passphrase';
|
||||
import { encodeRecoveryKey } from './recoverykey';
|
||||
import { pkVerify } from './olmlib';
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1 = "m.secret_storage.v1.curve25519-aes-sha2";
|
||||
|
||||
/**
|
||||
* Implements Secure Secret Storage and Sharing (MSC1946)
|
||||
* @module crypto/SecretStorage
|
||||
*/
|
||||
export default class SecretStorage extends EventEmitter {
|
||||
constructor(baseApis, cryptoCallbacks, crossSigningInfo) {
|
||||
super();
|
||||
this._baseApis = baseApis;
|
||||
this._cryptoCallbacks = cryptoCallbacks;
|
||||
this._crossSigningInfo = crossSigningInfo;
|
||||
this._requests = {};
|
||||
this._incomingRequests = {};
|
||||
}
|
||||
|
||||
getDefaultKeyId() {
|
||||
const defaultKeyEvent = this._baseApis.getAccountData(
|
||||
'm.secret_storage.default_key',
|
||||
);
|
||||
if (!defaultKeyEvent) return null;
|
||||
return defaultKeyEvent.getContent().key;
|
||||
}
|
||||
|
||||
setDefaultKeyId(keyId) {
|
||||
return new Promise((resolve) => {
|
||||
const listener = (ev) => {
|
||||
if (
|
||||
ev.getType() === 'm.secret_storage.default_key' &&
|
||||
ev.getContent().key === keyId
|
||||
) {
|
||||
this._baseApis.removeListener('accountData', listener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
this._baseApis.on('accountData', listener);
|
||||
|
||||
this._baseApis.setAccountData(
|
||||
'm.secret_storage.default_key',
|
||||
{ key: keyId },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a key for encrypting secrets.
|
||||
*
|
||||
* @param {string} algorithm the algorithm used by the key.
|
||||
* @param {object} opts the options for the algorithm. The properties used
|
||||
* depend on the algorithm given. This object may be modified to pass
|
||||
* information back about the key.
|
||||
* @param {string} [keyId] the ID of the key. If not given, a random
|
||||
* ID will be generated.
|
||||
*
|
||||
* @return {string} the ID of the key
|
||||
*/
|
||||
async addKey(algorithm, opts, keyId) {
|
||||
const keyData = {algorithm};
|
||||
|
||||
if (!opts) opts = {};
|
||||
|
||||
if (opts.name) {
|
||||
keyData.name = opts.name;
|
||||
}
|
||||
|
||||
switch (algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
{
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
if (opts.passphrase) {
|
||||
const key = await keyFromPassphrase(opts.passphrase);
|
||||
keyData.passphrase = {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: key.iterations,
|
||||
salt: key.salt,
|
||||
};
|
||||
opts.encodedkey = encodeRecoveryKey(key.key);
|
||||
keyData.pubkey = decryption.init_with_private_key(key.key);
|
||||
} else if (opts.privkey) {
|
||||
keyData.pubkey = decryption.init_with_private_key(opts.privkey);
|
||||
opts.encodedkey = encodeRecoveryKey(opts.privkey);
|
||||
} else {
|
||||
keyData.pubkey = decryption.generate_key();
|
||||
opts.encodedkey = encodeRecoveryKey(decryption.get_private_key());
|
||||
}
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
|
||||
}
|
||||
|
||||
if (!keyId) {
|
||||
do {
|
||||
keyId = randomString(32);
|
||||
} while (this._baseApis.getAccountData(`m.secret_storage.key.${keyId}`));
|
||||
}
|
||||
|
||||
await this._crossSigningInfo.signObject(keyData, 'master');
|
||||
|
||||
await this._baseApis.setAccountData(
|
||||
`m.secret_storage.key.${keyId}`, keyData,
|
||||
);
|
||||
|
||||
return keyId;
|
||||
}
|
||||
|
||||
// TODO: need a function to get all the secret keys
|
||||
|
||||
/**
|
||||
* Store an encrypted secret on the server
|
||||
*
|
||||
* @param {string} name The name of the secret
|
||||
* @param {string} secret The secret contents.
|
||||
* @param {Array} keys The IDs of the keys to use to encrypt the secret
|
||||
* or null/undefined to use the default key.
|
||||
*/
|
||||
async store(name, secret, keys) {
|
||||
const encrypted = {};
|
||||
|
||||
if (!keys) {
|
||||
const defaultKeyId = this.getDefaultKeyId();
|
||||
if (!defaultKeyId) {
|
||||
throw new Error("No keys specified and no default key present");
|
||||
}
|
||||
keys = [defaultKeyId];
|
||||
}
|
||||
|
||||
if (keys.length === 0) {
|
||||
throw new Error("Zero keys given to encrypt with!");
|
||||
}
|
||||
|
||||
for (const keyId of keys) {
|
||||
// get key information from key storage
|
||||
const keyInfo = this._baseApis.getAccountData(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
if (!keyInfo) {
|
||||
throw new Error("Unknown key: " + keyId);
|
||||
}
|
||||
const keyInfoContent = keyInfo.getContent();
|
||||
|
||||
// check signature of key info
|
||||
pkVerify(
|
||||
keyInfoContent,
|
||||
this._crossSigningInfo.getId('master'),
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
|
||||
// encrypt secret, based on the algorithm
|
||||
switch (keyInfoContent.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
{
|
||||
const encryption = new global.Olm.PkEncryption();
|
||||
try {
|
||||
encryption.set_recipient_key(keyInfoContent.pubkey);
|
||||
encrypted[keyId] = encryption.encrypt(secret);
|
||||
} finally {
|
||||
encryption.free();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
logger.warn("unknown algorithm for secret storage key " + keyId
|
||||
+ ": " + keyInfoContent.algorithm);
|
||||
// do nothing if we don't understand the encryption algorithm
|
||||
}
|
||||
}
|
||||
|
||||
// save encrypted secret
|
||||
await this._baseApis.setAccountData(name, {encrypted});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a secret from storage.
|
||||
*
|
||||
* @param {string} name the name of the secret
|
||||
*
|
||||
* @return {string} the contents of the secret
|
||||
*/
|
||||
async get(name) {
|
||||
const secretInfo = this._baseApis.getAccountData(name);
|
||||
if (!secretInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const secretContent = secretInfo.getContent();
|
||||
|
||||
if (!secretContent.encrypted) {
|
||||
throw new Error("Content is not encrypted!");
|
||||
}
|
||||
|
||||
// get possible keys to decrypt
|
||||
const keys = {};
|
||||
for (const keyId of Object.keys(secretContent.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyInfo = this._baseApis.getAccountData(
|
||||
"m.secret_storage.key." + keyId,
|
||||
).getContent();
|
||||
const encInfo = secretContent.encrypted[keyId];
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
|
||||
&& encInfo.ephemeral) {
|
||||
keys[keyId] = keyInfo;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// do nothing if we don't understand the encryption algorithm
|
||||
}
|
||||
}
|
||||
|
||||
let keyId;
|
||||
let decryption;
|
||||
try {
|
||||
// fetch private key from app
|
||||
[keyId, decryption] = await this._getSecretStorageKey(keys);
|
||||
|
||||
// decrypt secret
|
||||
const encInfo = secretContent.encrypted[keyId];
|
||||
switch (keys[keyId].algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
return decryption.decrypt(
|
||||
encInfo.ephemeral, encInfo.mac, encInfo.ciphertext,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (decryption) decryption.free();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a secret is stored on the server.
|
||||
*
|
||||
* @param {string} name the name of the secret
|
||||
* @param {boolean} checkKey check if the secret is encrypted by a trusted key
|
||||
*
|
||||
* @return {boolean} whether or not the secret is stored
|
||||
*/
|
||||
isStored(name, checkKey) {
|
||||
// check if secret exists
|
||||
const secretInfo = this._baseApis.getAccountData(name);
|
||||
if (!secretInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checkKey === undefined) checkKey = true;
|
||||
|
||||
const secretContent = secretInfo.getContent();
|
||||
|
||||
if (!secretContent.encrypted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if secret is encrypted by a known/trusted secret and
|
||||
// encryption looks sane
|
||||
for (const keyId of Object.keys(secretContent.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyInfo = this._baseApis.getAccountData(
|
||||
"m.secret_storage.key." + keyId,
|
||||
).getContent();
|
||||
const encInfo = secretContent.encrypted[keyId];
|
||||
if (checkKey) {
|
||||
pkVerify(
|
||||
keyInfo,
|
||||
this._crossSigningInfo.getId('master'),
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
}
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
|
||||
&& encInfo.ephemeral) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// do nothing if we don't understand the encryption algorithm
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a secret from another device
|
||||
*
|
||||
* @param {string} name the name of the secret to request
|
||||
* @param {string[]} devices the devices to request the secret from
|
||||
*
|
||||
* @return {string} the contents of the secret
|
||||
*/
|
||||
request(name, devices) {
|
||||
const requestId = this._baseApis.makeTxnId();
|
||||
|
||||
const requestControl = this._requests[requestId] = {
|
||||
devices,
|
||||
};
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
requestControl.resolve = resolve;
|
||||
requestControl.reject = reject;
|
||||
});
|
||||
const cancel = (reason) => {
|
||||
// send cancellation event
|
||||
const cancelData = {
|
||||
action: "request_cancellation",
|
||||
requesting_device_id: this._baseApis.deviceId,
|
||||
request_id: requestId,
|
||||
};
|
||||
const toDevice = {};
|
||||
for (const device of devices) {
|
||||
toDevice[device] = cancelData;
|
||||
}
|
||||
this._baseApis.sendToDevice("m.secret.request", {
|
||||
[this._baseApis.getUserId()]: toDevice,
|
||||
});
|
||||
|
||||
// and reject the promise so that anyone waiting on it will be
|
||||
// notified
|
||||
requestControl.reject(new Error(reason || "Cancelled"));
|
||||
};
|
||||
|
||||
// send request to devices
|
||||
const requestData = {
|
||||
name,
|
||||
action: "request",
|
||||
requesting_device_id: this._baseApis.deviceId,
|
||||
request_id: requestId,
|
||||
};
|
||||
const toDevice = {};
|
||||
for (const device of devices) {
|
||||
toDevice[device] = requestData;
|
||||
}
|
||||
this._baseApis.sendToDevice("m.secret.request", {
|
||||
[this._baseApis.getUserId()]: toDevice,
|
||||
});
|
||||
|
||||
return {
|
||||
request_id: requestId,
|
||||
promise,
|
||||
cancel,
|
||||
};
|
||||
}
|
||||
|
||||
async _onRequestReceived(event) {
|
||||
const sender = event.getSender();
|
||||
const content = event.getContent();
|
||||
if (sender !== this._baseApis.getUserId()
|
||||
|| !(content.name && content.action
|
||||
&& content.requesting_device_id && content.request_id)) {
|
||||
// ignore requests from anyone else, for now
|
||||
return;
|
||||
}
|
||||
const deviceId = content.requesting_device_id;
|
||||
// check if it's a cancel
|
||||
if (content.action === "request_cancellation") {
|
||||
if (this._incomingRequests[deviceId]
|
||||
&& this._incomingRequests[deviceId][content.request_id]) {
|
||||
logger.info("received request cancellation for secret (" + sender
|
||||
+ ", " + deviceId + ", " + content.request_id + ")");
|
||||
this.baseApis.emit("crypto.secrets.requestCancelled", {
|
||||
user_id: sender,
|
||||
device_id: deviceId,
|
||||
request_id: content.request_id,
|
||||
});
|
||||
}
|
||||
} else if (content.action === "request") {
|
||||
if (deviceId === this._baseApis.deviceId) {
|
||||
// no point in trying to send ourself the secret
|
||||
return;
|
||||
}
|
||||
|
||||
// check if we have the secret
|
||||
logger.info("received request for secret (" + sender
|
||||
+ ", " + deviceId + ", " + content.request_id + ")");
|
||||
if (!this._cryptoCallbacks.onSecretRequested) {
|
||||
return;
|
||||
}
|
||||
const secret = await this._cryptoCallbacks.onSecretRequested({
|
||||
user_id: sender,
|
||||
device_id: deviceId,
|
||||
request_id: content.request_id,
|
||||
name: content.name,
|
||||
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
|
||||
});
|
||||
if (secret) {
|
||||
const payload = {
|
||||
type: "m.secret.send",
|
||||
content: {
|
||||
request_id: content.request_id,
|
||||
secret: secret,
|
||||
},
|
||||
};
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this._baseApis._crypto._olmDevice,
|
||||
this._baseApis,
|
||||
{
|
||||
[sender]: [
|
||||
await this._baseApis.getStoredDevice(sender, deviceId),
|
||||
],
|
||||
},
|
||||
);
|
||||
await olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
this._baseApis.getUserId(),
|
||||
this._baseApis.deviceId,
|
||||
this._baseApis._crypto._olmDevice,
|
||||
sender,
|
||||
this._baseApis._crypto.getStoredDevice(sender, deviceId),
|
||||
payload,
|
||||
);
|
||||
const contentMap = {
|
||||
[sender]: {
|
||||
[deviceId]: encryptedContent,
|
||||
},
|
||||
};
|
||||
|
||||
this._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onSecretReceived(event) {
|
||||
if (event.getSender() !== this._baseApis.getUserId()) {
|
||||
// we shouldn't be receiving secrets from anyone else, so ignore
|
||||
// because someone could be trying to send us bogus data
|
||||
return;
|
||||
}
|
||||
const content = event.getContent();
|
||||
logger.log("got secret share for request ", content.request_id);
|
||||
const requestControl = this._requests[content.request_id];
|
||||
if (requestControl) {
|
||||
// make sure that the device that sent it is one of the devices that
|
||||
// we requested from
|
||||
const deviceInfo = this._baseApis._crypto._deviceList.getDeviceByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
event.getSenderKey(),
|
||||
);
|
||||
if (!deviceInfo) {
|
||||
logger.log(
|
||||
"secret share from unknown device with key", event.getSenderKey(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!requestControl.devices.includes(deviceInfo.deviceId)) {
|
||||
logger.log("unsolicited secret share from device", deviceInfo.deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
requestControl.resolve(content.secret);
|
||||
}
|
||||
}
|
||||
|
||||
async _getSecretStorageKey(keys) {
|
||||
if (!this._cryptoCallbacks.getSecretStorageKey) {
|
||||
throw new Error("No getSecretStorageKey callback supplied");
|
||||
}
|
||||
|
||||
const returned = await Promise.resolve(
|
||||
this._cryptoCallbacks.getSecretStorageKey({keys}),
|
||||
);
|
||||
|
||||
if (!returned) {
|
||||
throw new Error("getSecretStorageKey callback returned falsey");
|
||||
}
|
||||
if (returned.length < 2) {
|
||||
throw new Error("getSecretStorageKey callback returned invalid data");
|
||||
}
|
||||
|
||||
const [keyId, privateKey] = returned;
|
||||
if (!keys[keyId]) {
|
||||
throw new Error("App returned unknown key from getSecretStorageKey!");
|
||||
}
|
||||
|
||||
switch (keys[keyId].algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
{
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
let pubkey;
|
||||
try {
|
||||
pubkey = decryption.init_with_private_key(privateKey);
|
||||
} catch (e) {
|
||||
decryption.free();
|
||||
throw new Error("getSecretStorageKey callback returned invalid key");
|
||||
}
|
||||
if (pubkey !== keys[keyId].pubkey) {
|
||||
decryption.free();
|
||||
throw new Error(
|
||||
"getSecretStorageKey callback returned incorrect key",
|
||||
);
|
||||
}
|
||||
return [keyId, decryption];
|
||||
}
|
||||
default:
|
||||
throw new Error("Unknown key type: " + keys[keyId].algorithm);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,8 @@ 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 base = require("./base");
|
||||
@@ -104,7 +104,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
|
||||
}
|
||||
|
||||
if (!devicesInRoom.hasOwnProperty(userId)) {
|
||||
logger.log("Starting new session because we shared with " + userId);
|
||||
logger.log("Starting new megolm session because we shared with " + userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
|
||||
|
||||
if (!devicesInRoom[userId].hasOwnProperty(deviceId)) {
|
||||
logger.log(
|
||||
"Starting new session because we shared with " +
|
||||
"Starting new megolm session because we shared with " +
|
||||
userId + ":" + deviceId,
|
||||
);
|
||||
return true;
|
||||
@@ -200,6 +200,8 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
|
||||
if (!session) {
|
||||
logger.log(`Starting new megolm session for room ${self._roomId}`);
|
||||
session = await self._prepareNewSession();
|
||||
logger.log(`Started new megolm session ${session.sessionId} ` +
|
||||
`for room ${self._roomId}`);
|
||||
self._outboundSessions[session.sessionId] = session;
|
||||
}
|
||||
|
||||
@@ -278,7 +280,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.
|
||||
console.log("Failed to back up group session", e);
|
||||
logger.log("Failed to back up megolm session", e);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -440,19 +442,19 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
|
||||
) {
|
||||
const obSessionInfo = this._outboundSessions[sessionId];
|
||||
if (!obSessionInfo) {
|
||||
logger.debug("Session ID " + sessionId + " not found: not re-sharing keys");
|
||||
logger.debug(`megolm session ${sessionId} not found: not re-sharing keys`);
|
||||
return;
|
||||
}
|
||||
|
||||
// The chain index of the key we previously sent this device
|
||||
if (obSessionInfo.sharedWithDevices[userId] === undefined) {
|
||||
logger.debug("Session ID " + sessionId + " never shared with user " + userId);
|
||||
logger.debug(`megolm session ${sessionId} never shared with user ${userId}`);
|
||||
return;
|
||||
}
|
||||
const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId];
|
||||
if (sentChainIndex === undefined) {
|
||||
logger.debug(
|
||||
"Session ID " + sessionId + " never shared with device " +
|
||||
"megolm session ID " + sessionId + " never shared with device " +
|
||||
userId + ":" + device.deviceId,
|
||||
);
|
||||
return;
|
||||
@@ -466,7 +468,7 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
|
||||
|
||||
if (!key) {
|
||||
logger.warn(
|
||||
"No outbound session key found for " + sessionId + ": not re-sharing keys",
|
||||
`No inbound session key found for megolm ${sessionId}: not re-sharing keys`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -513,9 +515,8 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
|
||||
[device.deviceId]: encryptedContent,
|
||||
},
|
||||
});
|
||||
logger.debug(
|
||||
`Re-shared key for session ${sessionId} with ${userId}:${device.deviceId}`,
|
||||
);
|
||||
logger.debug(`Re-shared key for megolm session ${sessionId} ` +
|
||||
`with ${userId}:${device.deviceId}`);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -550,10 +551,10 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
|
||||
await this._encryptAndSendKeysToDevices(
|
||||
session, key.chain_index, userDeviceMaps[i], payload,
|
||||
);
|
||||
logger.log(`Completed megolm keyshare in ${this._roomId} `
|
||||
+ `(slice ${i + 1}/${userDeviceMaps.length})`);
|
||||
logger.log(`Completed megolm keyshare for ${session.sessionId} `
|
||||
+ `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`);
|
||||
} catch (e) {
|
||||
logger.log(`megolm keyshare in ${this._roomId} `
|
||||
logger.log(`megolm keyshare for ${session.sessionId} in ${this._roomId} `
|
||||
+ `(slice ${i + 1}/${userDeviceMaps.length}) failed`);
|
||||
|
||||
throw e;
|
||||
@@ -922,7 +923,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
keysClaimed = event.getKeysClaimed();
|
||||
}
|
||||
|
||||
logger.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
|
||||
logger.log(`Received and adding key for megolm session ${senderKey}|${sessionId}`);
|
||||
return this._olmDevice.addInboundGroupSession(
|
||||
content.room_id, senderKey, forwardingKeyChain, sessionId,
|
||||
content.session_key, keysClaimed,
|
||||
@@ -955,7 +956,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.
|
||||
console.log("Failed to back up group session", e);
|
||||
logger.log("Failed to back up megolm session", e);
|
||||
});
|
||||
}
|
||||
}).catch((e) => {
|
||||
@@ -1088,7 +1089,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.
|
||||
console.log("Failed to back up group session", e);
|
||||
logger.log("Failed to back up megolm session", e);
|
||||
});
|
||||
}
|
||||
// have another go at decrypting events sent with this session.
|
||||
|
||||
@@ -22,7 +22,7 @@ limitations under the License.
|
||||
*/
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const logger = require("../../logger");
|
||||
import logger from '../../logger';
|
||||
const utils = require("../../utils");
|
||||
const olmlib = require("../olmlib");
|
||||
const DeviceInfo = require("../deviceinfo");
|
||||
@@ -139,7 +139,7 @@ OlmEncryption.prototype.encryptMessage = async function(room, eventType, content
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.all(promises).return(encryptedContent);
|
||||
return await Promise.all(promises).then(() => encryptedContent);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,6 +56,7 @@ function DeviceInfo(deviceId) {
|
||||
this.verified = DeviceVerification.UNVERIFIED;
|
||||
this.known = false;
|
||||
this.unsigned = {};
|
||||
this.signatures = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,6 +89,7 @@ DeviceInfo.prototype.toStorage = function() {
|
||||
verified: this.verified,
|
||||
known: this.known,
|
||||
unsigned: this.unsigned,
|
||||
signatures: this.signatures,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+868
-86
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 New Vector 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.
|
||||
@@ -18,13 +19,11 @@ import { randomString } from '../randomstring';
|
||||
|
||||
const DEFAULT_ITERATIONS = 500000;
|
||||
|
||||
export async function keyForExistingBackup(backupData, password) {
|
||||
export async function keyFromAuthData(authData, password) {
|
||||
if (!global.Olm) {
|
||||
throw new Error("Olm is not available");
|
||||
}
|
||||
|
||||
const authData = backupData.auth_data;
|
||||
|
||||
if (!authData.private_key_salt || !authData.private_key_iterations) {
|
||||
throw new Error(
|
||||
"Salt and/or iterations not found: " +
|
||||
@@ -33,12 +32,12 @@ export async function keyForExistingBackup(backupData, password) {
|
||||
}
|
||||
|
||||
return await deriveKey(
|
||||
password, backupData.auth_data.private_key_salt,
|
||||
backupData.auth_data.private_key_iterations,
|
||||
password, authData.private_key_salt,
|
||||
authData.private_key_iterations,
|
||||
);
|
||||
}
|
||||
|
||||
export async function keyForNewBackup(password) {
|
||||
export async function keyFromPassphrase(password) {
|
||||
if (!global.Olm) {
|
||||
throw new Error("Olm is not available");
|
||||
}
|
||||
+69
-6
@@ -24,7 +24,7 @@ limitations under the License.
|
||||
import Promise from 'bluebird';
|
||||
const anotherjson = require('another-json');
|
||||
|
||||
const logger = require("../logger");
|
||||
import logger from '../logger';
|
||||
const utils = require("../utils");
|
||||
|
||||
/**
|
||||
@@ -287,12 +287,12 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
|
||||
);
|
||||
} catch (e) {
|
||||
// possibly a bad key
|
||||
logger.error("Error starting session with device " +
|
||||
logger.error("Error starting olm session with device " +
|
||||
userId + ":" + deviceId + ": " + e);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.log("Started new sessionid " + sid +
|
||||
logger.log("Started new olm sessionid " + sid +
|
||||
" for device " + userId + ":" + deviceId);
|
||||
return sid;
|
||||
}
|
||||
@@ -328,11 +328,74 @@ const _verifySignature = module.exports.verifySignature = async function(
|
||||
|
||||
// prepare the canonical json: remove unsigned and signatures, and stringify with
|
||||
// anotherjson
|
||||
delete obj.unsigned;
|
||||
delete obj.signatures;
|
||||
const json = anotherjson.stringify(obj);
|
||||
const mangledObj = Object.assign({}, obj);
|
||||
delete mangledObj.unsigned;
|
||||
delete mangledObj.signatures;
|
||||
const json = anotherjson.stringify(mangledObj);
|
||||
|
||||
olmDevice.verifySignature(
|
||||
signingKey, json, signature,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sign a JSON object using public key cryptography
|
||||
* @param {Object} obj Object to sign. The object will be modified to include
|
||||
* the new signature
|
||||
* @param {Olm.PkSigning|Uint8Array} key the signing object or the private key
|
||||
* seed
|
||||
* @param {string} userId The user ID who owns the signing key
|
||||
* @param {string} pubkey The public key (ignored if key is a seed)
|
||||
* @returns {string} the signature for the object
|
||||
*/
|
||||
module.exports.pkSign = function(obj, key, userId, pubkey) {
|
||||
let createdKey = false;
|
||||
if (key instanceof Uint8Array) {
|
||||
const keyObj = new global.Olm.PkSigning();
|
||||
pubkey = keyObj.init_with_seed(key);
|
||||
key = keyObj;
|
||||
createdKey = true;
|
||||
}
|
||||
const sigs = obj.signatures || {};
|
||||
delete obj.signatures;
|
||||
const unsigned = obj.unsigned;
|
||||
if (obj.unsigned) delete obj.unsigned;
|
||||
try {
|
||||
const mysigs = sigs[userId] || {};
|
||||
sigs[userId] = mysigs;
|
||||
|
||||
return mysigs['ed25519:' + pubkey] = key.sign(anotherjson.stringify(obj));
|
||||
} finally {
|
||||
obj.signatures = sigs;
|
||||
if (unsigned) obj.unsigned = unsigned;
|
||||
if (createdKey) {
|
||||
key.free();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify a signed JSON object
|
||||
* @param {Object} obj Object to verify
|
||||
* @param {string} pubkey The public key to use to verify
|
||||
* @param {string} userId The user ID who signed the object
|
||||
*/
|
||||
module.exports.pkVerify = function(obj, pubkey, userId) {
|
||||
const keyId = "ed25519:" + pubkey;
|
||||
if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) {
|
||||
throw new Error("No signature");
|
||||
}
|
||||
const signature = obj.signatures[userId][keyId];
|
||||
const util = new global.Olm.Utility();
|
||||
const sigs = obj.signatures;
|
||||
delete obj.signatures;
|
||||
const unsigned = obj.unsigned;
|
||||
if (obj.unsigned) delete obj.unsigned;
|
||||
try {
|
||||
util.ed25519_verify(pubkey, anotherjson.stringify(obj), signature);
|
||||
} finally {
|
||||
obj.signatures = sigs;
|
||||
if (unsigned) obj.unsigned = unsigned;
|
||||
util.free();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,35 +58,34 @@ export class Backend {
|
||||
getOrAddOutgoingRoomKeyRequest(request) {
|
||||
const requestBody = request.requestBody;
|
||||
|
||||
const deferred = Promise.defer();
|
||||
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
|
||||
txn.onerror = deferred.reject;
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
|
||||
txn.onerror = reject;
|
||||
|
||||
// first see if we already have an entry for this request.
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
if (existing) {
|
||||
// this entry matches the request - return it.
|
||||
// first see if we already have an entry for this request.
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
if (existing) {
|
||||
// this entry matches the request - return it.
|
||||
logger.log(
|
||||
`already have key request outstanding for ` +
|
||||
`${requestBody.room_id} / ${requestBody.session_id}: ` +
|
||||
`not sending another`,
|
||||
);
|
||||
resolve(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
// we got to the end of the list without finding a match
|
||||
// - add the new request.
|
||||
logger.log(
|
||||
`already have key request outstanding for ` +
|
||||
`${requestBody.room_id} / ${requestBody.session_id}: ` +
|
||||
`not sending another`,
|
||||
`enqueueing key request for ${requestBody.room_id} / ` +
|
||||
requestBody.session_id,
|
||||
);
|
||||
deferred.resolve(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
// we got to the end of the list without finding a match
|
||||
// - add the new request.
|
||||
logger.log(
|
||||
`enqueueing key request for ${requestBody.room_id} / ` +
|
||||
requestBody.session_id,
|
||||
);
|
||||
txn.oncomplete = () => { deferred.resolve(request); };
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
store.add(request);
|
||||
txn.oncomplete = () => { resolve(request); };
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
store.add(request);
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,15 +99,14 @@ export class Backend {
|
||||
* not found
|
||||
*/
|
||||
getOutgoingRoomKeyRequest(requestBody) {
|
||||
const deferred = Promise.defer();
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
txn.onerror = reject;
|
||||
|
||||
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
txn.onerror = deferred.reject;
|
||||
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
deferred.resolve(existing);
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
resolve(existing);
|
||||
});
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,6 +330,23 @@ export class Backend {
|
||||
objectStore.put(newData, "-");
|
||||
}
|
||||
|
||||
getCrossSigningKeys(txn, func) {
|
||||
const objectStore = txn.objectStore("account");
|
||||
const getReq = objectStore.get("crossSigningKeys");
|
||||
getReq.onsuccess = function() {
|
||||
try {
|
||||
func(getReq.result || null);
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
storeCrossSigningKeys(txn, keys) {
|
||||
const objectStore = txn.objectStore("account");
|
||||
objectStore.put(keys, "crossSigningKeys");
|
||||
}
|
||||
|
||||
// Olm Sessions
|
||||
|
||||
countEndToEndSessions(txn, func) {
|
||||
@@ -694,7 +709,7 @@ function promiseifyTxn(txn) {
|
||||
if (txn._mx_abortexception !== undefined) {
|
||||
reject(txn._mx_abortexception);
|
||||
} else {
|
||||
console.log("Error performing indexeddb txn", event);
|
||||
logger.log("Error performing indexeddb txn", event);
|
||||
reject(event.target.error);
|
||||
}
|
||||
};
|
||||
@@ -702,7 +717,7 @@ function promiseifyTxn(txn) {
|
||||
if (txn._mx_abortexception !== undefined) {
|
||||
reject(txn._mx_abortexception);
|
||||
} else {
|
||||
console.log("Error performing indexeddb txn", event);
|
||||
logger.log("Error performing indexeddb txn", event);
|
||||
reject(event.target.error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,7 +90,7 @@ export default class IndexedDBCryptoStore {
|
||||
};
|
||||
|
||||
req.onerror = (ev) => {
|
||||
console.log("Error connecting to indexeddb", ev);
|
||||
logger.log("Error connecting to indexeddb", ev);
|
||||
reject(ev.target.error);
|
||||
};
|
||||
|
||||
@@ -160,7 +160,7 @@ export default class IndexedDBCryptoStore {
|
||||
};
|
||||
|
||||
req.onerror = (ev) => {
|
||||
console.log("Error deleting data from indexeddb", ev);
|
||||
logger.log("Error deleting data from indexeddb", ev);
|
||||
reject(ev.target.error);
|
||||
};
|
||||
|
||||
@@ -287,10 +287,12 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {function(string)} func Called with the account pickle
|
||||
*/
|
||||
getAccount(txn, func) {
|
||||
this._backendPromise.value().getAccount(txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getAccount(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Write the account pickle to the store.
|
||||
* This requires an active transaction. See doTxn().
|
||||
*
|
||||
@@ -298,7 +300,35 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {string} newData The new account pickle to store.
|
||||
*/
|
||||
storeAccount(txn, newData) {
|
||||
this._backendPromise.value().storeAccount(txn, newData);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeAccount(txn, newData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public part of the cross-signing keys (eg. self-signing key,
|
||||
* user signing key).
|
||||
*
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
* @param {function(string)} func Called with the account keys object:
|
||||
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
|
||||
*/
|
||||
getCrossSigningKeys(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getCrossSigningKeys(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the cross-signing keys back to the store
|
||||
*
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
* @param {string} keys keys object as getCrossSigningKeys()
|
||||
*/
|
||||
storeCrossSigningKeys(txn, keys) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeCrossSigningKeys(txn, keys);
|
||||
});
|
||||
}
|
||||
|
||||
// Olm sessions
|
||||
@@ -309,7 +339,9 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {function(int)} func Called with the count of sessions
|
||||
*/
|
||||
countEndToEndSessions(txn, func) {
|
||||
this._backendPromise.value().countEndToEndSessions(txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.countEndToEndSessions(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,7 +357,9 @@ export default class IndexedDBCryptoStore {
|
||||
* a message.
|
||||
*/
|
||||
getEndToEndSession(deviceKey, sessionId, txn, func) {
|
||||
this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndSession(deviceKey, sessionId, txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -340,7 +374,9 @@ export default class IndexedDBCryptoStore {
|
||||
* a message.
|
||||
*/
|
||||
getEndToEndSessions(deviceKey, txn, func) {
|
||||
this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndSessions(deviceKey, txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,7 +387,9 @@ export default class IndexedDBCryptoStore {
|
||||
* and session keys.
|
||||
*/
|
||||
getAllEndToEndSessions(txn, func) {
|
||||
this._backendPromise.value().getAllEndToEndSessions(txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getAllEndToEndSessions(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -362,12 +400,14 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
|
||||
this._backendPromise.value().storeEndToEndSession(
|
||||
deviceKey, sessionId, sessionInfo, txn,
|
||||
);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndSession(
|
||||
deviceKey, sessionId, sessionInfo, txn,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Inbound group saessions
|
||||
// Inbound group sessions
|
||||
|
||||
/**
|
||||
* Retrieve the end-to-end inbound group session for a given
|
||||
@@ -379,9 +419,11 @@ export default class IndexedDBCryptoStore {
|
||||
* to Base64 end-to-end session.
|
||||
*/
|
||||
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
|
||||
this._backendPromise.value().getEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, txn, func,
|
||||
);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, txn, func,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -392,7 +434,9 @@ export default class IndexedDBCryptoStore {
|
||||
* sessionData}, then once with null to indicate the end of the list.
|
||||
*/
|
||||
getAllEndToEndInboundGroupSessions(txn, func) {
|
||||
this._backendPromise.value().getAllEndToEndInboundGroupSessions(txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getAllEndToEndInboundGroupSessions(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -405,9 +449,11 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
|
||||
this._backendPromise.value().addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -420,9 +466,11 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
|
||||
this._backendPromise.value().storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// End-to-end device tracking
|
||||
@@ -438,7 +486,9 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndDeviceData(deviceData, txn) {
|
||||
this._backendPromise.value().storeEndToEndDeviceData(deviceData, txn);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndDeviceData(deviceData, txn);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,7 +499,9 @@ export default class IndexedDBCryptoStore {
|
||||
* device data
|
||||
*/
|
||||
getEndToEndDeviceData(txn, func) {
|
||||
this._backendPromise.value().getEndToEndDeviceData(txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndDeviceData(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
// End to End Rooms
|
||||
@@ -461,7 +513,9 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndRoom(roomId, roomInfo, txn) {
|
||||
this._backendPromise.value().storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,7 +524,9 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {function(Object)} func Function called with the end to end encrypted rooms
|
||||
*/
|
||||
getEndToEndRooms(txn, func) {
|
||||
this._backendPromise.value().getEndToEndRooms(txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndRooms(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
// session backups
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import logger from '../../logger';
|
||||
import MemoryCryptoStore from './memory-crypto-store.js';
|
||||
import MemoryCryptoStore from './memory-crypto-store';
|
||||
|
||||
/**
|
||||
* Internal module. Partial localStorage backed storage for e2e.
|
||||
@@ -31,6 +31,7 @@ import MemoryCryptoStore from './memory-crypto-store.js';
|
||||
|
||||
const E2E_PREFIX = "crypto.";
|
||||
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys";
|
||||
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
|
||||
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
|
||||
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
|
||||
@@ -284,6 +285,17 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
);
|
||||
}
|
||||
|
||||
getCrossSigningKeys(txn, func) {
|
||||
const keys = getJsonItem(this.store, KEY_CROSS_SIGNING_KEYS);
|
||||
func(keys);
|
||||
}
|
||||
|
||||
storeCrossSigningKeys(txn, keys) {
|
||||
setJsonItem(
|
||||
this.store, KEY_CROSS_SIGNING_KEYS, keys,
|
||||
);
|
||||
}
|
||||
|
||||
doTxn(mode, stores, func) {
|
||||
return Promise.resolve(func(null));
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export default class MemoryCryptoStore {
|
||||
constructor() {
|
||||
this._outgoingRoomKeyRequests = [];
|
||||
this._account = null;
|
||||
this._crossSigningKeys = null;
|
||||
|
||||
// Map of {devicekey -> {sessionId -> session pickle}}
|
||||
this._sessions = {};
|
||||
@@ -234,6 +235,14 @@ export default class MemoryCryptoStore {
|
||||
this._account = newData;
|
||||
}
|
||||
|
||||
getCrossSigningKeys(txn, func) {
|
||||
func(this._crossSigningKeys);
|
||||
}
|
||||
|
||||
storeCrossSigningKeys(txn, keys) {
|
||||
this._crossSigningKeys = keys;
|
||||
}
|
||||
|
||||
// Olm Sessions
|
||||
|
||||
countEndToEndSessions(txn, func) {
|
||||
|
||||
+171
-20
@@ -21,6 +21,11 @@ limitations under the License.
|
||||
|
||||
import {MatrixEvent} from '../../models/event';
|
||||
import {EventEmitter} from 'events';
|
||||
import logger from '../../logger';
|
||||
import DeviceInfo from '../deviceinfo';
|
||||
import {newTimeoutError} from "./Error";
|
||||
|
||||
const timeoutException = new Error("Verification timed out");
|
||||
|
||||
export default class VerificationBase extends EventEmitter {
|
||||
/**
|
||||
@@ -43,37 +48,116 @@ export default class VerificationBase extends EventEmitter {
|
||||
*
|
||||
* @param {string} transactionId the transaction ID to be used when sending events
|
||||
*
|
||||
* @param {object} startEvent the m.key.verification.start event that
|
||||
* @param {string} [roomId] the room to use for verification
|
||||
*
|
||||
* @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
|
||||
* @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) {
|
||||
constructor(baseApis, userId, deviceId, transactionId, roomId, startEvent, request) {
|
||||
super();
|
||||
this._baseApis = baseApis;
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
this.transactionId = transactionId;
|
||||
this.startEvent = startEvent;
|
||||
this.request = request;
|
||||
this._parent = parent;
|
||||
if (typeof(roomId) === "string" || roomId instanceof String) {
|
||||
this.roomId = roomId;
|
||||
this.startEvent = startEvent;
|
||||
this.request = request;
|
||||
} else {
|
||||
// if room ID was omitted, but start event and request were not
|
||||
this.startEvent= roomId;
|
||||
this.request = startEvent;
|
||||
}
|
||||
this.cancelled = false;
|
||||
this._done = false;
|
||||
this._promise = null;
|
||||
this._transactionTimeoutTimer = null;
|
||||
this._eventsSubscription = null;
|
||||
|
||||
// At this point, the verification request was received so start the timeout timer.
|
||||
this._resetTimer();
|
||||
|
||||
if (this.roomId) {
|
||||
this._sendWithTxnId = this._sendMessage;
|
||||
} else {
|
||||
this._sendWithTxnId = this._sendToDevice;
|
||||
}
|
||||
}
|
||||
|
||||
_resetTimer() {
|
||||
logger.info("Refreshing/starting the verification transaction timeout timer");
|
||||
if (this._transactionTimeoutTimer !== null) {
|
||||
clearTimeout(this._transactionTimeoutTimer);
|
||||
}
|
||||
this._transactionTimeoutTimer = setTimeout(() => {
|
||||
if (!this._done && !this.cancelled) {
|
||||
logger.info("Triggering verification timeout");
|
||||
this.cancel(timeoutException);
|
||||
}
|
||||
}, 10 * 60 * 1000); // 10 minutes
|
||||
}
|
||||
|
||||
_endTimer() {
|
||||
if (this._transactionTimeoutTimer !== null) {
|
||||
clearTimeout(this._transactionTimeoutTimer);
|
||||
this._transactionTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_contentFromEventWithTxnId(event) {
|
||||
if (this.roomId) { // verification as timeline event
|
||||
// ensure m.related_to is included in e2ee rooms
|
||||
// as the field is excluded from encryption
|
||||
const content = Object.assign({}, event.getContent());
|
||||
content["m.relates_to"] = event.getRelation();
|
||||
return content;
|
||||
} else { // verification as to_device event
|
||||
return event.getContent();
|
||||
}
|
||||
}
|
||||
|
||||
/* creates a content object with the transaction id added to it */
|
||||
_contentWithTxnId(content) {
|
||||
const copy = Object.assign({}, content);
|
||||
if (this.roomId) { // verification as timeline event
|
||||
copy["m.relates_to"] = {
|
||||
rel_type: "m.reference",
|
||||
event_id: this.transactionId,
|
||||
};
|
||||
} else { // verification as to_device event
|
||||
copy.transaction_id = this.transactionId;
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
_send(type, contentWithoutTxnId) {
|
||||
const content = this._contentWithTxnId(contentWithoutTxnId);
|
||||
return this._sendWithTxnId(type, content);
|
||||
}
|
||||
|
||||
/* send a message to the other participant, using to-device messages
|
||||
*/
|
||||
_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 },
|
||||
});
|
||||
}
|
||||
|
||||
/* send a message to the other participant, using in-roomm messages
|
||||
*/
|
||||
_sendMessage(type, content) {
|
||||
if (this._done) {
|
||||
return Promise.reject(new Error("Verification is already done"));
|
||||
}
|
||||
return this._baseApis.sendEvent(this.roomId, type, content);
|
||||
}
|
||||
|
||||
_waitForEvent(type) {
|
||||
if (this._done) {
|
||||
return Promise.reject(new Error("Verification is already done"));
|
||||
@@ -91,13 +175,18 @@ export default class VerificationBase extends EventEmitter {
|
||||
} else if (e.getType() === this._expectedEvent) {
|
||||
this._expectedEvent = undefined;
|
||||
this._rejectEvent = undefined;
|
||||
this._resetTimer();
|
||||
this._resolveEvent(e);
|
||||
} else if (e.getType() === "m.key.verification.cancel") {
|
||||
const reject = this._reject;
|
||||
this._reject = undefined;
|
||||
reject(new Error("Other side cancelled verification"));
|
||||
} else {
|
||||
this._expectedEvent = undefined;
|
||||
const exception = new Error(
|
||||
"Unexpected message: expecting " + this._expectedEvent
|
||||
+ " but got " + e.getType(),
|
||||
);
|
||||
this._expectedEvent = undefined;
|
||||
if (this._rejectEvent) {
|
||||
const reject = this._rejectEvent;
|
||||
this._rejectEvent = undefined;
|
||||
@@ -108,17 +197,27 @@ export default class VerificationBase extends EventEmitter {
|
||||
}
|
||||
|
||||
done() {
|
||||
this._endTimer(); // always kill the activity timer
|
||||
if (!this._done) {
|
||||
if (this.roomId) {
|
||||
// verification in DM requires a done message
|
||||
this._send("m.key.verification.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 instanceof MatrixEvent) {
|
||||
if (e === timeoutException) {
|
||||
const timeoutEvent = newTimeoutError();
|
||||
this._send(timeoutEvent.getType(), timeoutEvent.getContent());
|
||||
} else if (e instanceof MatrixEvent) {
|
||||
const sender = e.getSender();
|
||||
if (sender !== this.userId) {
|
||||
const content = e.getContent();
|
||||
@@ -127,9 +226,9 @@ export default class VerificationBase extends EventEmitter {
|
||||
content.reason = content.reason || content.body
|
||||
|| "Unknown reason";
|
||||
content.transaction_id = this.transactionId;
|
||||
this._sendToDevice("m.key.verification.cancel", content);
|
||||
this._send("m.key.verification.cancel", content);
|
||||
} else {
|
||||
this._sendToDevice("m.key.verification.cancel", {
|
||||
this._send("m.key.verification.cancel", {
|
||||
code: "m.unknown",
|
||||
reason: content.body || "Unknown reason",
|
||||
transaction_id: this.transactionId,
|
||||
@@ -137,7 +236,7 @@ export default class VerificationBase extends EventEmitter {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._sendToDevice("m.key.verification.cancel", {
|
||||
this._send("m.key.verification.cancel", {
|
||||
code: "m.unknown",
|
||||
reason: e.toString(),
|
||||
transaction_id: this.transactionId,
|
||||
@@ -145,8 +244,16 @@ export default class VerificationBase extends EventEmitter {
|
||||
}
|
||||
}
|
||||
if (this._promise !== null) {
|
||||
this._reject(e);
|
||||
// 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 {
|
||||
// unsubscribe from events, this happens in _reject usually but we don't have one here
|
||||
if (this._eventsSubscription) {
|
||||
this._eventsSubscription = this._eventsSubscription();
|
||||
}
|
||||
// FIXME: this causes an "Uncaught promise" console message
|
||||
// if nothing ends up chaining this promise.
|
||||
this._promise = Promise.reject(e);
|
||||
}
|
||||
// Also emit a 'cancel' event that the app can listen for to detect cancellation
|
||||
@@ -167,15 +274,30 @@ export default class VerificationBase extends EventEmitter {
|
||||
this._promise = new Promise((resolve, reject) => {
|
||||
this._resolve = (...args) => {
|
||||
this._done = true;
|
||||
this._endTimer();
|
||||
if (this.handler) {
|
||||
// these listeners are attached in Crypto.acceptVerificationDM
|
||||
if (this._eventsSubscription) {
|
||||
this._eventsSubscription = this._eventsSubscription();
|
||||
}
|
||||
}
|
||||
resolve(...args);
|
||||
};
|
||||
this._reject = (...args) => {
|
||||
this._done = true;
|
||||
this._endTimer();
|
||||
if (this.handler) {
|
||||
// these listeners are attached in Crypto.acceptVerificationDM
|
||||
if (this._eventsSubscription) {
|
||||
this._eventsSubscription = this._eventsSubscription();
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
@@ -183,18 +305,47 @@ export default class VerificationBase extends EventEmitter {
|
||||
}
|
||||
|
||||
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) {
|
||||
throw new Error(`Could not find device ${deviceId}`);
|
||||
} else {
|
||||
if (device) {
|
||||
await verifier(keyId, device, keyInfo);
|
||||
verifiedDevices.push(deviceId);
|
||||
} else {
|
||||
const crossSigningInfo = this._baseApis._crypto._deviceList
|
||||
.getStoredCrossSigningForUser(userId);
|
||||
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
|
||||
await verifier(keyId, DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
[keyId]: deviceId,
|
||||
},
|
||||
}, deviceId), keyInfo);
|
||||
verifiedDevices.push(deviceId);
|
||||
} else {
|
||||
logger.warn(
|
||||
`verification: Could not find device ${deviceId} to verify`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
const deviceId = keyId.split(':', 2)[1];
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
setEventsSubscription(subscription) {
|
||||
this._eventsSubscription = subscription;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ export default class SAS extends Base {
|
||||
}
|
||||
|
||||
async _doSendVerification() {
|
||||
const initialMessage = {
|
||||
const initialMessage = this._contentWithTxnId({
|
||||
method: SAS.NAME,
|
||||
from_device: this._baseApis.deviceId,
|
||||
key_agreement_protocols: KEY_AGREEMENT_LIST,
|
||||
@@ -213,9 +213,10 @@ export default class SAS extends Base {
|
||||
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);
|
||||
});
|
||||
// add the transaction id to the message beforehand because
|
||||
// it needs to be included in the commitment hash later on
|
||||
this._sendWithTxnId("m.key.verification.start", initialMessage);
|
||||
|
||||
|
||||
let e = await this._waitForEvent("m.key.verification.accept");
|
||||
@@ -235,7 +236,7 @@ export default class SAS extends Base {
|
||||
const hashCommitment = content.commitment;
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
this._sendToDevice("m.key.verification.key", {
|
||||
this._send("m.key.verification.key", {
|
||||
key: olmSAS.get_pubkey(),
|
||||
});
|
||||
|
||||
@@ -280,7 +281,10 @@ export default class SAS extends Base {
|
||||
}
|
||||
|
||||
async _doRespondVerification() {
|
||||
let content = this.startEvent.getContent();
|
||||
// as m.related_to is not included in the encrypted content in e2e rooms,
|
||||
// we need to make sure it is added
|
||||
let content = this._contentFromEventWithTxnId(this.startEvent);
|
||||
|
||||
// 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
|
||||
@@ -306,7 +310,7 @@ export default class SAS extends Base {
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
|
||||
this._sendToDevice("m.key.verification.accept", {
|
||||
this._send("m.key.verification.accept", {
|
||||
key_agreement_protocol: keyAgreement,
|
||||
hash: hashMethod,
|
||||
message_authentication_code: macMethod,
|
||||
@@ -320,7 +324,7 @@ export default class SAS extends Base {
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
olmSAS.set_their_key(content.key);
|
||||
this._sendToDevice("m.key.verification.key", {
|
||||
this._send("m.key.verification.key", {
|
||||
key: olmSAS.get_pubkey(),
|
||||
});
|
||||
|
||||
@@ -354,22 +358,35 @@ export default class SAS extends Base {
|
||||
}
|
||||
|
||||
_sendMAC(olmSAS, method) {
|
||||
const keyId = `ed25519:${this._baseApis.deviceId}`;
|
||||
const mac = {};
|
||||
const keyList = [];
|
||||
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.userId + this.deviceId
|
||||
+ this.transactionId;
|
||||
|
||||
mac[keyId] = olmSAS[macMethods[method]](
|
||||
const deviceKeyId = `ed25519:${this._baseApis.deviceId}`;
|
||||
mac[deviceKeyId] = olmSAS[macMethods[method]](
|
||||
this._baseApis.getDeviceEd25519Key(),
|
||||
baseInfo + keyId,
|
||||
baseInfo + deviceKeyId,
|
||||
);
|
||||
keyList.push(deviceKeyId);
|
||||
|
||||
const crossSigningId = this._baseApis.getCrossSigningId();
|
||||
if (crossSigningId) {
|
||||
const crossSigningKeyId = `ed25519:${crossSigningId}`;
|
||||
mac[crossSigningKeyId] = olmSAS[macMethods[method]](
|
||||
crossSigningId,
|
||||
baseInfo + crossSigningKeyId,
|
||||
);
|
||||
keyList.push(crossSigningKeyId);
|
||||
}
|
||||
|
||||
const keys = olmSAS[macMethods[method]](
|
||||
keyId,
|
||||
keyList.sort().join(","),
|
||||
baseInfo + "KEY_IDS",
|
||||
);
|
||||
this._sendToDevice("m.key.verification.mac", { mac, keys });
|
||||
this._send("m.key.verification.mac", { mac, keys });
|
||||
}
|
||||
|
||||
async _checkMAC(olmSAS, content, method) {
|
||||
|
||||
+59
-92
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
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.
|
||||
@@ -22,6 +23,7 @@ import Promise from 'bluebird';
|
||||
const parseContentType = require('content-type').parse;
|
||||
|
||||
const utils = require("./utils");
|
||||
import logger from './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
|
||||
@@ -45,10 +47,15 @@ module.exports.PREFIX_R0 = "/_matrix/client/r0";
|
||||
module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
|
||||
|
||||
/**
|
||||
* URI path for the identity API
|
||||
* URI path for v1 of the 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
|
||||
*/
|
||||
@@ -89,6 +96,13 @@ 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.
|
||||
@@ -101,7 +115,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
};
|
||||
return {
|
||||
base: this.opts.baseUrl,
|
||||
path: "/_matrix/media/v1/upload",
|
||||
path: "/_matrix/media/r0/upload",
|
||||
params: params,
|
||||
};
|
||||
},
|
||||
@@ -164,9 +178,21 @@ 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
|
||||
// nodejs.
|
||||
const body = file.stream ? file.stream : file;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// backwards-compatibility hacks where we used to do different things
|
||||
// between browser and node.
|
||||
@@ -175,7 +201,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
if (global.XMLHttpRequest) {
|
||||
rawResponse = false;
|
||||
} else {
|
||||
console.warn(
|
||||
logger.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 " +
|
||||
@@ -188,7 +214,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
let onlyContentUri = opts.onlyContentUri;
|
||||
if (!rawResponse && onlyContentUri === undefined) {
|
||||
if (global.XMLHttpRequest) {
|
||||
console.warn(
|
||||
logger.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 " +
|
||||
@@ -278,7 +304,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
});
|
||||
}
|
||||
});
|
||||
let url = this.opts.baseUrl + "/_matrix/media/v1/upload";
|
||||
let url = this.opts.baseUrl + "/_matrix/media/r0/upload";
|
||||
|
||||
const queryArgs = [];
|
||||
|
||||
@@ -314,7 +340,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
|
||||
promise = this.authedRequest(
|
||||
opts.callback, "POST", "/upload", queryParams, body, {
|
||||
prefix: "/_matrix/media/v1",
|
||||
prefix: "/_matrix/media/r0",
|
||||
headers: {"Content-Type": contentType},
|
||||
json: false,
|
||||
bodyParser: bodyParser,
|
||||
@@ -355,7 +381,18 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
return this.uploads;
|
||||
},
|
||||
|
||||
idServerRequest: function(callback, method, path, params, prefix) {
|
||||
idServerRequest: function(
|
||||
callback,
|
||||
method,
|
||||
path,
|
||||
params,
|
||||
prefix,
|
||||
accessToken,
|
||||
) {
|
||||
if (!this.opts.idBaseUrl) {
|
||||
throw new Error("No Identity Server base URL set");
|
||||
}
|
||||
|
||||
const fullUri = this.opts.idBaseUrl + prefix + path;
|
||||
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
@@ -368,13 +405,17 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
uri: fullUri,
|
||||
method: method,
|
||||
withCredentials: false,
|
||||
json: false,
|
||||
json: true, // we want a JSON response if we can
|
||||
_matrix_opts: this.opts,
|
||||
headers: {},
|
||||
};
|
||||
if (method == 'GET') {
|
||||
if (method === 'GET') {
|
||||
opts.qs = params;
|
||||
} else {
|
||||
opts.form = params;
|
||||
} else if (typeof params === "object") {
|
||||
opts.json = params;
|
||||
}
|
||||
if (accessToken) {
|
||||
opts.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
const defer = Promise.defer();
|
||||
@@ -382,12 +423,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
opts,
|
||||
requestCallback(defer, callback, this.opts.onlyData),
|
||||
);
|
||||
// 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);
|
||||
});
|
||||
return defer.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -457,7 +493,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");
|
||||
self.event_emitter.emit("Session.logged_out", err);
|
||||
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
|
||||
self.event_emitter.emit(
|
||||
"no_consent",
|
||||
@@ -512,76 +548,6 @@ 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
|
||||
@@ -870,7 +836,8 @@ function parseErrorResponse(response, body) {
|
||||
let err;
|
||||
if (contentType) {
|
||||
if (contentType.type === 'application/json') {
|
||||
err = new module.exports.MatrixError(JSON.parse(body));
|
||||
const jsonBody = typeof(body) === 'object' ? body : JSON.parse(body);
|
||||
err = new module.exports.MatrixError(jsonBody);
|
||||
} else if (contentType.type === 'text/plain') {
|
||||
err = new Error(`Server returned ${httpStatus} error: ${body}`);
|
||||
}
|
||||
|
||||
+73
-20
@@ -22,6 +22,7 @@ import Promise from 'bluebird';
|
||||
const url = require("url");
|
||||
|
||||
const utils = require("./utils");
|
||||
import logger from './logger';
|
||||
|
||||
const EMAIL_STAGE_TYPE = "m.login.email.identity";
|
||||
const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
@@ -48,11 +49,18 @@ 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?, 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(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(string, object?)} opts.stateUpdated
|
||||
* called when the status of the UI auth changes, ie. when the state of
|
||||
@@ -100,6 +108,7 @@ 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;
|
||||
@@ -111,9 +120,14 @@ function InteractiveAuth(opts) {
|
||||
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 = {
|
||||
@@ -134,7 +148,10 @@ InteractiveAuth.prototype = {
|
||||
// if we have no flows, try a request (we'll have
|
||||
// just a session ID in _data if resuming)
|
||||
if (!this._data.flows) {
|
||||
this._doRequest(this._data);
|
||||
if (this._busyChangedCallback) this._busyChangedCallback(true);
|
||||
this._doRequest(this._data).finally(() => {
|
||||
if (this._busyChangedCallback) this._busyChangedCallback(false);
|
||||
});
|
||||
} else {
|
||||
this._startNextAuthStage();
|
||||
}
|
||||
@@ -146,24 +163,30 @@ InteractiveAuth.prototype = {
|
||||
* completed out-of-band. If so, the attemptAuth promise will
|
||||
* be resolved.
|
||||
*/
|
||||
poll: function() {
|
||||
poll: async 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 idServerParsedUrl = url.parse(
|
||||
this._matrixClient.getIdentityServerUrl(),
|
||||
);
|
||||
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;
|
||||
}
|
||||
authDict = {
|
||||
type: EMAIL_STAGE_TYPE,
|
||||
threepid_creds: {
|
||||
sid: this._emailSid,
|
||||
client_secret: this._clientSecret,
|
||||
id_server: idServerParsedUrl.host,
|
||||
},
|
||||
threepid_creds: creds,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -220,18 +243,44 @@ 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: function(authData, background) {
|
||||
submitAuthDict: async function(authData, background) {
|
||||
if (!this._resolveFunc) {
|
||||
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);
|
||||
|
||||
this._doRequest(auth, background);
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -283,7 +332,7 @@ InteractiveAuth.prototype = {
|
||||
// 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
|
||||
console.log(
|
||||
logger.log(
|
||||
"Background poll request failed doing UI auth: ignoring",
|
||||
error,
|
||||
);
|
||||
@@ -304,12 +353,14 @@ InteractiveAuth.prototype = {
|
||||
|
||||
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,
|
||||
@@ -332,6 +383,8 @@ InteractiveAuth.prototype = {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,9 +436,9 @@ InteractiveAuth.prototype = {
|
||||
if (this._chosenFlow === null) {
|
||||
this._chosenFlow = this._chooseFlow();
|
||||
}
|
||||
console.log("Active flow => %s", JSON.stringify(this._chosenFlow));
|
||||
logger.log("Active flow => %s", JSON.stringify(this._chosenFlow));
|
||||
const nextStage = this._firstUncompletedStage(this._chosenFlow);
|
||||
console.log("Next stage: %s", nextStage);
|
||||
logger.log("Next stage: %s", nextStage);
|
||||
return nextStage;
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
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.
|
||||
@@ -76,6 +77,7 @@ 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;
|
||||
|
||||
@@ -20,6 +20,8 @@ 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 '../logger';
|
||||
import Relations from './relations';
|
||||
|
||||
// var DEBUG = false;
|
||||
@@ -28,7 +30,7 @@ const DEBUG = true;
|
||||
let debuglog;
|
||||
if (DEBUG) {
|
||||
// using bind means that we get to keep useful line numbers in the console
|
||||
debuglog = console.log.bind(console);
|
||||
debuglog = logger.log.bind(logger);
|
||||
} else {
|
||||
debuglog = function() {};
|
||||
}
|
||||
@@ -90,6 +92,13 @@ function EventTimelineSet(room, opts) {
|
||||
}
|
||||
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
|
||||
@@ -422,7 +431,7 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
|
||||
}
|
||||
|
||||
// time to join the timelines.
|
||||
console.info("Already have timeline for " + eventId +
|
||||
logger.info("Already have timeline for " + eventId +
|
||||
" - joining timeline " + timeline + " to " +
|
||||
existingTimeline);
|
||||
|
||||
@@ -436,15 +445,14 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
|
||||
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.
|
||||
console.warn({backwardsIsLive, forwardsIsLive}); // debugging
|
||||
if (backwardsIsLive) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Refusing to set a preceding existingTimeLine on our " +
|
||||
"timeline as the existingTimeLine is live (" + existingTimeline + ")",
|
||||
);
|
||||
}
|
||||
if (forwardsIsLive) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Refusing to set our preceding timeline on a existingTimeLine " +
|
||||
"as our timeline is live (" + timeline + ")",
|
||||
);
|
||||
@@ -464,8 +472,8 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
|
||||
// timeline we ended up on.
|
||||
if (lastEventWasNew || !didUpdate) {
|
||||
if (direction === EventTimeline.FORWARDS && timeline === this._liveTimeline) {
|
||||
console.warn({lastEventWasNew, didUpdate}); // for debugging
|
||||
console.warn(
|
||||
logger.warn({lastEventWasNew, didUpdate}); // for debugging
|
||||
logger.warn(
|
||||
`Refusing to set forwards pagination token of live timeline ` +
|
||||
`${timeline} to ${paginationToken}`,
|
||||
);
|
||||
@@ -687,9 +695,12 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
|
||||
* 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.
|
||||
* @throws If <code>eventId</code>, <code>relationType</code> or <code>eventType</code>
|
||||
* are not valid.
|
||||
*
|
||||
* @returns {Relations}
|
||||
* A container for relation events.
|
||||
* @returns {?Relations}
|
||||
* A container for relation events or undefined if there are no relation events for
|
||||
* the relationType.
|
||||
*/
|
||||
EventTimelineSet.prototype.getRelationsForEvent = function(
|
||||
eventId, relationType, eventType,
|
||||
@@ -748,6 +759,10 @@ EventTimelineSet.prototype.aggregateRelations = function(event) {
|
||||
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", () => {
|
||||
@@ -777,20 +792,27 @@ EventTimelineSet.prototype.aggregateRelations = function(event) {
|
||||
}
|
||||
let relationsWithEventType = relationsWithRelType[eventType];
|
||||
|
||||
let isNewRelations = false;
|
||||
let relatesToEvent;
|
||||
if (!relationsWithEventType) {
|
||||
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
||||
relationType,
|
||||
eventType,
|
||||
this.room,
|
||||
);
|
||||
const relatesToEvent = this.findEventById(relatesToEventId);
|
||||
isNewRelations = true;
|
||||
relatesToEvent = this.findEventById(relatesToEventId);
|
||||
if (relatesToEvent) {
|
||||
relationsWithEventType.setTargetEvent(relatesToEvent);
|
||||
relatesToEvent.emit("Event.relationsCreated", relationType, eventType);
|
||||
}
|
||||
}
|
||||
|
||||
relationsWithEventType.addEvent(event);
|
||||
|
||||
// only emit once event has been added to relations
|
||||
if (isNewRelations && relatesToEvent) {
|
||||
relatesToEvent.emit("Event.relationsCreated", relationType, eventType);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+195
-13
@@ -24,13 +24,14 @@ limitations under the License.
|
||||
import Promise from 'bluebird';
|
||||
import {EventEmitter} from 'events';
|
||||
import utils from '../utils.js';
|
||||
import logger from '../logger';
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
module.exports.EventStatus = {
|
||||
const EventStatus = {
|
||||
/** The event was not sent and will no longer be retried. */
|
||||
NOT_SENT: "not_sent",
|
||||
|
||||
@@ -48,6 +49,7 @@ module.exports.EventStatus = {
|
||||
/** The event was cancelled before it was successfully sent. */
|
||||
CANCELLED: "cancelled",
|
||||
};
|
||||
module.exports.EventStatus = EventStatus;
|
||||
|
||||
const interns = {};
|
||||
function intern(str) {
|
||||
@@ -123,6 +125,8 @@ module.exports.MatrixEvent = function MatrixEvent(
|
||||
this.forwardLooking = true;
|
||||
this._pushActions = null;
|
||||
this._replacingEvent = null;
|
||||
this._localRedactionEvent = null;
|
||||
this._isCancelled = false;
|
||||
|
||||
this._clearEvent = {};
|
||||
|
||||
@@ -227,6 +231,9 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
* @return {Object} The event content JSON, or an empty object.
|
||||
*/
|
||||
getOriginalContent: function() {
|
||||
if (this._localRedactionEvent) {
|
||||
return {};
|
||||
}
|
||||
return this._clearEvent.content || this.event.content || {};
|
||||
},
|
||||
|
||||
@@ -238,7 +245,9 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
* @return {Object} The event content JSON, or an empty object.
|
||||
*/
|
||||
getContent: function() {
|
||||
if (this._replacingEvent) {
|
||||
if (this._localRedactionEvent) {
|
||||
return {};
|
||||
} else if (this._replacingEvent) {
|
||||
return this._replacingEvent.getContent()["m.new_content"] || {};
|
||||
} else {
|
||||
return this.getOriginalContent();
|
||||
@@ -288,6 +297,15 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
return this.getUnsigned().age || this.event.age; // v2 / v1
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the age of the event when this function was called.
|
||||
* Relies on the local clock being in sync with the clock of the original homeserver.
|
||||
* @return {Number} The age of this event in milliseconds.
|
||||
*/
|
||||
getLocalAge: function() {
|
||||
return Date.now() - this.getTs();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the event state_key if it has one. This will return <code>undefined
|
||||
* </code> for message events.
|
||||
@@ -395,7 +413,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
// new info.
|
||||
//
|
||||
if (this._decryptionPromise) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`Event ${this.getId()} already being decrypted; queueing a retry`,
|
||||
);
|
||||
this._retryDecryption = true;
|
||||
@@ -469,7 +487,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)
|
||||
console.error(
|
||||
logger.error(
|
||||
`Error decrypting event (id=${this.getId()}): ${e.stack || e}`,
|
||||
);
|
||||
this._decryptionPromise = null;
|
||||
@@ -495,7 +513,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
//
|
||||
if (this._retryDecryption) {
|
||||
// decryption error, but we have a retry queued.
|
||||
console.log(
|
||||
logger.log(
|
||||
`Got error decrypting event (id=${this.getId()}: ` +
|
||||
`${e}), but retrying`,
|
||||
);
|
||||
@@ -504,7 +522,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
|
||||
// decryption error, no retries queued. Warn about the error and
|
||||
// set it to m.bad.encrypted.
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Error decrypting event (id=${this.getId()}): ${e.detailedString}`,
|
||||
);
|
||||
|
||||
@@ -665,6 +683,27 @@ 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
|
||||
@@ -678,6 +717,8 @@ 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;
|
||||
@@ -723,6 +764,15 @@ 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
|
||||
*
|
||||
@@ -746,9 +796,26 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
* @param {Object} event the object to assign to the `event` property
|
||||
*/
|
||||
handleRemoteEcho: function(event) {
|
||||
const oldUnsigned = this.getUnsigned();
|
||||
const oldId = this.getId();
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -771,6 +838,11 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
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.
|
||||
@@ -806,7 +878,11 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
* @param {MatrixEvent?} newEvent the event with the replacing content, if any.
|
||||
*/
|
||||
makeReplaced(newEvent) {
|
||||
if (this.isRedacted()) {
|
||||
// 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) {
|
||||
@@ -816,15 +892,25 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the status of the event, or the replacing event in case `makeReplace` has been called.
|
||||
* 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}
|
||||
*/
|
||||
replacementOrOwnStatus() {
|
||||
getAssociatedStatus() {
|
||||
if (this._replacingEvent) {
|
||||
return this._replacingEvent.status;
|
||||
} else {
|
||||
return this.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];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -834,11 +920,18 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
* @return {string?}
|
||||
*/
|
||||
replacingEventId() {
|
||||
return this._replacingEvent && this._replacingEvent.getId();
|
||||
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?}
|
||||
*/
|
||||
@@ -846,6 +939,90 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
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
|
||||
@@ -865,6 +1042,11 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
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;
|
||||
}
|
||||
@@ -880,7 +1062,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
/* _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/unstable.html#redactions
|
||||
* http://matrix.org/speculator/spec/HEAD/client_server/latest.html#redactions
|
||||
*
|
||||
* Also:
|
||||
* - We keep 'unsigned' since that is created by the local server
|
||||
|
||||
+12
-7
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import { EventStatus } from '../../lib/models/event';
|
||||
import { EventStatus } from '../models/event';
|
||||
|
||||
/**
|
||||
* A container for relation events that supports easy access to common ways of
|
||||
@@ -242,12 +242,7 @@ export default class Relations extends EventEmitter {
|
||||
|
||||
redactedEvent.removeListener("Event.beforeRedaction", this._onBeforeRedaction);
|
||||
|
||||
// Dispatch a redaction event on this collection. `setTimeout` is used
|
||||
// to wait until the next event loop iteration by which time the event
|
||||
// has actually been marked as redacted.
|
||||
setTimeout(() => {
|
||||
this.emit("Relations.redaction");
|
||||
}, 0);
|
||||
this.emit("Relations.redaction", redactedEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,10 +301,20 @@ export default class Relations extends EventEmitter {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* will be returned. Default: true. (Deprecated)
|
||||
* @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
|
||||
|
||||
@@ -21,6 +21,7 @@ const EventEmitter = require("events").EventEmitter;
|
||||
|
||||
const utils = require("../utils");
|
||||
const RoomMember = require("./room-member");
|
||||
import logger from '../logger';
|
||||
|
||||
// possible statuses for out-of-band member loading
|
||||
const OOB_STATUS_NOTSTARTED = 1;
|
||||
@@ -447,7 +448,7 @@ RoomState.prototype.clearOutOfBandMembers = function() {
|
||||
delete this.members[userId];
|
||||
}
|
||||
});
|
||||
console.log(`LL: RoomState removed ${count} members...`);
|
||||
logger.log(`LL: RoomState removed ${count} members...`);
|
||||
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
|
||||
};
|
||||
|
||||
@@ -456,11 +457,11 @@ RoomState.prototype.clearOutOfBandMembers = function() {
|
||||
* @param {MatrixEvent[]} stateEvents array of membership state events
|
||||
*/
|
||||
RoomState.prototype.setOutOfBandMembers = function(stateEvents) {
|
||||
console.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
|
||||
logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
|
||||
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
|
||||
return;
|
||||
}
|
||||
console.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`);
|
||||
logger.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`);
|
||||
this._oobMemberFlags.status = OOB_STATUS_FINISHED;
|
||||
stateEvents.forEach((e) => this._setOutOfBandMember(e));
|
||||
};
|
||||
|
||||
+214
-70
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018, 2019 New Vector 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.
|
||||
@@ -29,6 +30,7 @@ const ContentRepo = require("../content-repo");
|
||||
const EventTimeline = require("./event-timeline");
|
||||
const EventTimelineSet = require("./event-timeline-set");
|
||||
|
||||
import logger from '../logger';
|
||||
import ReEmitter from '../ReEmitter';
|
||||
|
||||
// These constants are used as sane defaults when the homeserver doesn't support
|
||||
@@ -37,8 +39,8 @@ import ReEmitter from '../ReEmitter';
|
||||
// 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 = '1';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3'];
|
||||
const KNOWN_SAFE_ROOM_VERSION = '4';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4'];
|
||||
|
||||
function synthesizeReceipt(userId, event, receiptType) {
|
||||
// console.log("synthesizing receipt for "+event.getId());
|
||||
@@ -209,7 +211,7 @@ utils.inherits(Room, EventEmitter);
|
||||
Room.prototype.getVersion = function() {
|
||||
const createEvent = this.currentState.getStateEvents("m.room.create", "");
|
||||
if (!createEvent) {
|
||||
console.warn("Room " + this.room_id + " does not have an m.room.create event");
|
||||
logger.warn("Room " + this.room_id + " does not have an m.room.create event");
|
||||
return '1';
|
||||
}
|
||||
const ver = createEvent.getContent()['room_version'];
|
||||
@@ -262,9 +264,36 @@ Room.prototype.getRecommendedVersion = async function() {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
console.log(`[${this.roomId}] Current version: ${currentVersion}`);
|
||||
console.log(`[${this.roomId}] Version capability: `, versionCap);
|
||||
logger.log(`[${this.roomId}] Current version: ${currentVersion}`);
|
||||
logger.log(`[${this.roomId}] Version capability: `, versionCap);
|
||||
|
||||
const result = {
|
||||
version: currentVersion,
|
||||
@@ -273,7 +302,7 @@ Room.prototype.getRecommendedVersion = async function() {
|
||||
};
|
||||
|
||||
// If the room is on the default version then nothing needs to change
|
||||
if (currentVersion === versionCap.default) return Promise.resolve(result);
|
||||
if (currentVersion === versionCap.default) return result;
|
||||
|
||||
const stableVersions = Object.keys(versionCap.available)
|
||||
.filter((v) => versionCap.available[v] === 'stable');
|
||||
@@ -286,16 +315,16 @@ Room.prototype.getRecommendedVersion = async function() {
|
||||
result.needsUpgrade = true;
|
||||
result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g);
|
||||
if (result.urgent) {
|
||||
console.warn(`URGENT upgrade required on ${this.roomId}`);
|
||||
logger.warn(`URGENT upgrade required on ${this.roomId}`);
|
||||
} else {
|
||||
console.warn(`Non-urgent upgrade required on ${this.roomId}`);
|
||||
logger.warn(`Non-urgent upgrade required on ${this.roomId}`);
|
||||
}
|
||||
return Promise.resolve(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// The room is on a stable, but non-default, version by this point.
|
||||
// No upgrade needed.
|
||||
return Promise.resolve(result);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -318,13 +347,30 @@ Room.prototype.userMayUpgradeRoom = function(userId) {
|
||||
Room.prototype.getPendingEvents = function() {
|
||||
if (this._opts.pendingEventOrdering !== "detached") {
|
||||
throw new Error(
|
||||
"Cannot call getPendingEventList with pendingEventOrdering == " +
|
||||
"Cannot call getPendingEvents 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.
|
||||
*
|
||||
@@ -334,6 +380,23 @@ Room.prototype.getLiveTimeline = function() {
|
||||
return this.getUnfilteredTimelineSet().getLiveTimeline();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the timestamp of the last message in the room
|
||||
*
|
||||
* @return {number} the timestamp of the last message in the room
|
||||
*/
|
||||
Room.prototype.getLastActiveTimestamp = function() {
|
||||
const timeline = this.getLiveTimeline();
|
||||
const events = timeline.getEvents();
|
||||
if (events.length) {
|
||||
const lastEvent = events[events.length - 1];
|
||||
return lastEvent.getTs();
|
||||
} else {
|
||||
return Number.MIN_SAFE_INTEGER;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} myUserId the user id for the logged in member
|
||||
* @return {string} the membership type (join | leave | invite) for the logged in user
|
||||
@@ -471,7 +534,7 @@ Room.prototype._loadMembers = async function() {
|
||||
if (rawMembersEvents === null) {
|
||||
fromServer = true;
|
||||
rawMembersEvents = await this._loadMembersFromServer();
|
||||
console.log(`LL: got ${rawMembersEvents.length} ` +
|
||||
logger.log(`LL: got ${rawMembersEvents.length} ` +
|
||||
`members from server for room ${this.roomId}`);
|
||||
}
|
||||
const memberEvents = rawMembersEvents.map(this._client.getEventMapper());
|
||||
@@ -515,21 +578,21 @@ Room.prototype.loadMembersIfNeeded = function() {
|
||||
const oobMembers = this.currentState.getMembers()
|
||||
.filter((m) => m.isOutOfBand())
|
||||
.map((m) => m.events.member.event);
|
||||
console.log(`LL: telling store to write ${oobMembers.length}`
|
||||
logger.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) => {
|
||||
console.log("LL: storing OOB room members failed, oh well",
|
||||
logger.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
|
||||
console.error(err);
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
this._membersPromise = inMemoryUpdate;
|
||||
@@ -555,9 +618,9 @@ Room.prototype.clearLoadedMembersIfNeeded = async function() {
|
||||
*/
|
||||
Room.prototype._cleanupAfterLeaving = function() {
|
||||
this.clearLoadedMembersIfNeeded().catch((err) => {
|
||||
console.error(`error after clearing loaded members from ` +
|
||||
logger.error(`error after clearing loaded members from ` +
|
||||
`room ${this.roomId} after leaving`);
|
||||
console.dir(err);
|
||||
logger.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -735,7 +798,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.
|
||||
* avatar URL wasn't explicitly set. Default: true. (Deprecated)
|
||||
* @return {?string} the avatar URL or null.
|
||||
*/
|
||||
Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
|
||||
@@ -769,20 +832,26 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
|
||||
* @return {array} The room's alias as an array of strings
|
||||
*/
|
||||
Room.prototype.getAliases = function() {
|
||||
const alias_strings = [];
|
||||
const aliasStrings = [];
|
||||
|
||||
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(
|
||||
alias_strings, alias_event.getContent().aliases,
|
||||
);
|
||||
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 filteredAliases = aliasEvent.getContent().aliases.filter(a => {
|
||||
if (typeof(a) !== "string") return false;
|
||||
if (a[0] !== '#') return false;
|
||||
if (!a.endsWith(`:${aliasEvent.getStateKey()}`)) return false;
|
||||
|
||||
// It's probably valid by here.
|
||||
return true;
|
||||
});
|
||||
Array.prototype.push.apply(aliasStrings, filteredAliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
return alias_strings;
|
||||
return aliasStrings;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -832,7 +901,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
* @return {RoomMember} The member or <code>null</code>.
|
||||
*/
|
||||
Room.prototype.getMember = function(userId) {
|
||||
return this.currentState.getMember(userId);
|
||||
return this.currentState.getMember(userId);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -840,7 +909,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
* @return {RoomMember[]} A list of currently joined members.
|
||||
*/
|
||||
Room.prototype.getJoinedMembers = function() {
|
||||
return this.getMembersWithMembership("join");
|
||||
return this.getMembersWithMembership("join");
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1004,13 +1073,25 @@ Room.prototype.removeFilteredTimelineSet = function(filter) {
|
||||
* @private
|
||||
*/
|
||||
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
||||
if (event.getType() === "m.room.redaction") {
|
||||
if (event.isRedaction()) {
|
||||
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
|
||||
@@ -1101,7 +1182,7 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
|
||||
if (this._opts.pendingEventOrdering == "detached") {
|
||||
if (this._pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
|
||||
console.warn("Setting event as NOT_SENT due to messages in the same state");
|
||||
logger.warn("Setting event as NOT_SENT due to messages in the same state");
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
}
|
||||
this._pendingEventList.push(event);
|
||||
@@ -1110,24 +1191,26 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
// 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.)
|
||||
// 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 (this._filter.filterRoomTimeline([event]).length) {
|
||||
timelineSet.aggregateRelations(event);
|
||||
}
|
||||
} else {
|
||||
timelineSet.aggregateRelations(event);
|
||||
}
|
||||
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 (this._filter.filterRoomTimeline([event]).length) {
|
||||
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
|
||||
timelineSet.addEventToTimeline(event,
|
||||
timelineSet.getLiveTimeline(), false);
|
||||
}
|
||||
@@ -1140,6 +1223,30 @@ 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.
|
||||
@@ -1161,7 +1268,7 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
|
||||
const oldStatus = localEvent.status;
|
||||
|
||||
// no longer pending
|
||||
delete this._txnToEvent[remoteEvent.transaction_id];
|
||||
delete this._txnToEvent[remoteEvent.getUnsigned().transaction_id];
|
||||
|
||||
// if it's in the pending list, remove it
|
||||
if (this._pendingEventList) {
|
||||
@@ -1229,7 +1336,7 @@ ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
|
||||
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
|
||||
*/
|
||||
Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
||||
console.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`);
|
||||
logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`);
|
||||
|
||||
// if the message was sent, we expect an event id
|
||||
if (newStatus == EventStatus.SENT && !newEventId) {
|
||||
@@ -1265,7 +1372,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
||||
|
||||
if (newStatus == EventStatus.SENT) {
|
||||
// update the event id
|
||||
event.event.event_id = newEventId;
|
||||
event.replaceLocalEventId(newEventId);
|
||||
|
||||
// if the event was already in the timeline (which will be the case if
|
||||
// opts.pendingEventOrdering==chronological), we need to update the
|
||||
@@ -1276,12 +1383,13 @@ 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) {
|
||||
utils.removeElement(
|
||||
this._pendingEventList,
|
||||
function(ev) {
|
||||
return ev.getId() == oldEventId;
|
||||
}, false,
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.removeEvent(oldEventId);
|
||||
}
|
||||
@@ -1289,6 +1397,23 @@ 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
|
||||
@@ -1330,28 +1455,33 @@ Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
|
||||
}
|
||||
|
||||
for (i = 0; i < events.length; i++) {
|
||||
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);
|
||||
}
|
||||
// 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
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes events from this room.
|
||||
* @param {String[]} event_ids A list of event_ids to remove.
|
||||
* @param {String[]} eventIds A list of eventIds to remove.
|
||||
*/
|
||||
Room.prototype.removeEvents = function(event_ids) {
|
||||
for (let i = 0; i < event_ids.length; ++i) {
|
||||
this.removeEvent(event_ids[i]);
|
||||
Room.prototype.removeEvents = function(eventIds) {
|
||||
for (let i = 0; i < eventIds.length; ++i) {
|
||||
this.removeEvent(eventIds[i]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1367,6 +1497,9 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -1790,10 +1923,21 @@ module.exports = Room;
|
||||
* event).
|
||||
*
|
||||
* @event module:client~MatrixClient#"Room.redaction"
|
||||
* @param {MatrixEvent} event The matrix event which was redacted
|
||||
* @param {MatrixEvent} event The matrix redaction event
|
||||
* @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"
|
||||
|
||||
+60
-7
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {escapeRegExp, globToRegexp} from "./utils";
|
||||
import {escapeRegExp, globToRegexp, isNullOrUndefined} from "./utils";
|
||||
|
||||
/**
|
||||
* @module pushprocessor
|
||||
@@ -43,6 +43,11 @@ const DEFAULT_OVERRIDE_RULES = [
|
||||
key: "type",
|
||||
pattern: "m.room.tombstone",
|
||||
},
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "state_key",
|
||||
pattern: "",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
"notify",
|
||||
@@ -52,6 +57,22 @@ const DEFAULT_OVERRIDE_RULES = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// 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",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -247,7 +268,7 @@ function PushProcessor(client) {
|
||||
}
|
||||
|
||||
const val = valueForDottedKey(cond.key, ev);
|
||||
if (!val || typeof val != 'string') {
|
||||
if (typeof val !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -283,10 +304,10 @@ function PushProcessor(client) {
|
||||
|
||||
// special-case the first component to deal with encrypted messages
|
||||
const firstPart = parts[0];
|
||||
if (firstPart == 'content') {
|
||||
if (firstPart === 'content') {
|
||||
val = ev.getContent();
|
||||
parts.shift();
|
||||
} else if (firstPart == 'type') {
|
||||
} else if (firstPart === 'type') {
|
||||
val = ev.getType();
|
||||
parts.shift();
|
||||
} else {
|
||||
@@ -295,11 +316,11 @@ function PushProcessor(client) {
|
||||
}
|
||||
|
||||
while (parts.length > 0) {
|
||||
const thispart = parts.shift();
|
||||
if (!val[thispart]) {
|
||||
const thisPart = parts.shift();
|
||||
if (isNullOrUndefined(val[thisPart])) {
|
||||
return null;
|
||||
}
|
||||
val = val[thispart];
|
||||
val = val[thisPart];
|
||||
}
|
||||
return val;
|
||||
};
|
||||
@@ -439,6 +460,38 @@ 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}
|
||||
|
||||
@@ -24,6 +24,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
import logger from './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.
|
||||
@@ -39,7 +40,7 @@ let _realCallbackKey;
|
||||
// each is an object with keys [runAt, func, params, key].
|
||||
const _callbackList = [];
|
||||
|
||||
// var debuglog = console.log.bind(console);
|
||||
// var debuglog = logger.log.bind(logger);
|
||||
const debuglog = function() {};
|
||||
|
||||
/**
|
||||
@@ -47,7 +48,7 @@ const debuglog = function() {};
|
||||
*
|
||||
* Intended for use by the unit tests.
|
||||
*
|
||||
* @param {function} f function which should return a millisecond counter
|
||||
* @param {function} [f] function which should return a millisecond counter
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@@ -170,7 +171,7 @@ function _runCallbacks() {
|
||||
try {
|
||||
cb.func.apply(global, cb.params);
|
||||
} catch (e) {
|
||||
console.error("Uncaught exception in callback function",
|
||||
logger.error("Uncaught exception in callback function",
|
||||
e.stack || e);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-3
@@ -21,6 +21,7 @@ limitations under the License.
|
||||
*/
|
||||
const utils = require("./utils");
|
||||
import Promise from 'bluebird';
|
||||
import logger from './logger';
|
||||
|
||||
const DEBUG = false; // set true to enable console logging.
|
||||
|
||||
@@ -176,7 +177,8 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
|
||||
* @see module:scheduler~queueAlgorithm
|
||||
*/
|
||||
MatrixScheduler.QUEUE_MESSAGES = function(event) {
|
||||
if (event.getType() === "m.room.message") {
|
||||
// enqueue messages or events that associate with another event (redactions and relations)
|
||||
if (event.getType() === "m.room.message" || event.hasAssocation()) {
|
||||
// put these events in the 'message' queue.
|
||||
return "message";
|
||||
}
|
||||
@@ -219,7 +221,14 @@ function _processQueue(scheduler, queueName) {
|
||||
);
|
||||
// fire the process function and if it resolves, resolve the deferred. Else
|
||||
// invoke the retry algorithm.
|
||||
scheduler._procFn(obj.event).done(function(res) {
|
||||
|
||||
// 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) {
|
||||
// remove this from the queue
|
||||
_removeNextEvent(scheduler, queueName);
|
||||
debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
|
||||
@@ -269,7 +278,7 @@ function _removeNextEvent(scheduler, queueName) {
|
||||
|
||||
function debuglog() {
|
||||
if (DEBUG) {
|
||||
console.log(...arguments);
|
||||
logger.log(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import Promise from 'bluebird';
|
||||
import SyncAccumulator from "../sync-accumulator";
|
||||
import utils from "../utils";
|
||||
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
||||
import logger from '../logger';
|
||||
|
||||
const VERSION = 3;
|
||||
|
||||
@@ -146,7 +147,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
*/
|
||||
connect: function() {
|
||||
if (!this._disconnected) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend.connect: already connected or connecting`,
|
||||
);
|
||||
return Promise.resolve();
|
||||
@@ -154,14 +155,14 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
|
||||
this._disconnected = false;
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend.connect: connecting...`,
|
||||
);
|
||||
const req = this.indexedDB.open(this._dbName, VERSION);
|
||||
req.onupgradeneeded = (ev) => {
|
||||
const db = ev.target.result;
|
||||
const oldVersion = ev.oldVersion;
|
||||
console.log(
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
|
||||
);
|
||||
if (oldVersion < 1) { // The database did not previously exist.
|
||||
@@ -178,16 +179,16 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
};
|
||||
|
||||
req.onblocked = () => {
|
||||
console.log(
|
||||
logger.log(
|
||||
`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`,
|
||||
);
|
||||
};
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend.connect: awaiting connection...`,
|
||||
);
|
||||
return reqAsEventPromise(req).then((ev) => {
|
||||
console.log(
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend.connect: connected`,
|
||||
);
|
||||
this.db = ev.target.result;
|
||||
@@ -215,7 +216,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
this._loadAccountData(),
|
||||
this._loadSyncData(),
|
||||
]).then(([accountData, syncData]) => {
|
||||
console.log(
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend: loaded initial data`,
|
||||
);
|
||||
this._syncAccumulator.accumulate({
|
||||
@@ -273,7 +274,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
reject(err);
|
||||
};
|
||||
}).then((events) => {
|
||||
console.log(`LL: got ${events && events.length}` +
|
||||
logger.log(`LL: got ${events && events.length}` +
|
||||
` membershipEvents from storage for room ${roomId} ...`);
|
||||
return events;
|
||||
});
|
||||
@@ -287,7 +288,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
*/
|
||||
setOutOfBandMembers: async function(roomId, membershipEvents) {
|
||||
console.log(`LL: backend about to store ${membershipEvents.length}` +
|
||||
logger.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");
|
||||
@@ -306,7 +307,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
};
|
||||
store.put(markerObject);
|
||||
await txnAsPromise(tx);
|
||||
console.log(`LL: backend done storing for ${roomId}!`);
|
||||
logger.log(`LL: backend done storing for ${roomId}!`);
|
||||
},
|
||||
|
||||
clearOutOfBandMembers: async function(roomId) {
|
||||
@@ -341,7 +342,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
[roomId, maxStateKey],
|
||||
);
|
||||
|
||||
console.log(`LL: Deleting all users + marker in storage for ` +
|
||||
logger.log(`LL: Deleting all users + marker in storage for ` +
|
||||
`room ${roomId}, with key range:`,
|
||||
[roomId, minStateKey], [roomId, maxStateKey]);
|
||||
await reqAsPromise(writeStore.delete(membersKeyRange));
|
||||
@@ -354,11 +355,11 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
*/
|
||||
clearDatabase: function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Removing indexeddb instance: ${this._dbName}`);
|
||||
logger.log(`Removing indexeddb instance: ${this._dbName}`);
|
||||
const req = this.indexedDB.deleteDatabase(this._dbName);
|
||||
|
||||
req.onblocked = () => {
|
||||
console.log(
|
||||
logger.log(
|
||||
`can't yet delete indexeddb ${this._dbName}` +
|
||||
` because it is open elsewhere`,
|
||||
);
|
||||
@@ -368,14 +369,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.
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`unable to delete js-sdk store indexeddb: ${ev.target.error}`,
|
||||
);
|
||||
resolve();
|
||||
};
|
||||
|
||||
req.onsuccess = () => {
|
||||
console.log(`Removed indexeddb instance: ${this._dbName}`);
|
||||
logger.log(`Removed indexeddb instance: ${this._dbName}`);
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
@@ -434,7 +435,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* @return {Promise} Resolves if the data was persisted.
|
||||
*/
|
||||
_persistSyncData: function(nextBatch, roomsData, groupsData) {
|
||||
console.log("Persisting sync data up to ", nextBatch);
|
||||
logger.log("Persisting sync data up to ", nextBatch);
|
||||
return Promise.try(() => {
|
||||
const txn = this.db.transaction(["sync"], "readwrite");
|
||||
const store = txn.objectStore("sync");
|
||||
@@ -508,7 +509,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* @return {Promise<Object[]>} A list of raw global account events.
|
||||
*/
|
||||
_loadAccountData: function() {
|
||||
console.log(
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend: loading account data...`,
|
||||
);
|
||||
return Promise.try(() => {
|
||||
@@ -517,7 +518,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
return selectQuery(store, undefined, (cursor) => {
|
||||
return cursor.value;
|
||||
}).then((result) => {
|
||||
console.log(
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend: loaded account data`,
|
||||
);
|
||||
return result;
|
||||
@@ -530,7 +531,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
|
||||
*/
|
||||
_loadSyncData: function() {
|
||||
console.log(
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend: loading sync data...`,
|
||||
);
|
||||
return Promise.try(() => {
|
||||
@@ -539,11 +540,11 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
return selectQuery(store, undefined, (cursor) => {
|
||||
return cursor.value;
|
||||
}).then((results) => {
|
||||
console.log(
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend: loaded sync data`,
|
||||
);
|
||||
if (results.length > 1) {
|
||||
console.warn("loadSyncData: More than 1 sync row found.");
|
||||
logger.warn("loadSyncData: More than 1 sync row found.");
|
||||
}
|
||||
return (results.length > 0 ? results[0] : {});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import logger from '../logger';
|
||||
import {defer} from '../utils';
|
||||
|
||||
/**
|
||||
* An IndexedDB store backend where the actual backend sits in a web
|
||||
@@ -140,7 +142,7 @@ RemoteIndexedDBStoreBackend.prototype = {
|
||||
|
||||
// tell the worker the db name.
|
||||
this._startPromise = this._doCmd('_setupWorker', [this._dbName]).then(() => {
|
||||
console.log("IndexedDB worker is ready");
|
||||
logger.log("IndexedDB worker is ready");
|
||||
});
|
||||
}
|
||||
return this._startPromise;
|
||||
@@ -151,7 +153,7 @@ RemoteIndexedDBStoreBackend.prototype = {
|
||||
// the promise automatically gets rejected
|
||||
return Promise.resolve().then(() => {
|
||||
const seq = this._nextSeq++;
|
||||
const def = Promise.defer();
|
||||
const def = defer();
|
||||
|
||||
this._inFlight[seq] = def;
|
||||
|
||||
@@ -170,13 +172,13 @@ RemoteIndexedDBStoreBackend.prototype = {
|
||||
|
||||
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
|
||||
if (msg.seq === undefined) {
|
||||
console.error("Got reply from worker with no seq");
|
||||
logger.error("Got reply from worker with no seq");
|
||||
return;
|
||||
}
|
||||
|
||||
const def = this._inFlight[msg.seq];
|
||||
if (def === undefined) {
|
||||
console.error("Got reply for unknown seq " + msg.seq);
|
||||
logger.error("Got reply for unknown seq " + msg.seq);
|
||||
return;
|
||||
}
|
||||
delete this._inFlight[msg.seq];
|
||||
@@ -189,7 +191,7 @@ RemoteIndexedDBStoreBackend.prototype = {
|
||||
def.reject(error);
|
||||
}
|
||||
} else {
|
||||
console.warn("Unrecognised message from worker: " + msg);
|
||||
logger.warn("Unrecognised message from worker: " + msg);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
|
||||
import logger from '../logger';
|
||||
|
||||
/**
|
||||
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
|
||||
@@ -129,8 +130,8 @@ class IndexedDBStoreWorker {
|
||||
result: ret,
|
||||
});
|
||||
}, (err) => {
|
||||
console.error("Error running command: "+msg.command);
|
||||
console.error(err);
|
||||
logger.error("Error running command: "+msg.command);
|
||||
logger.error(err);
|
||||
this.postMessage.call(null, {
|
||||
command: 'cmd_fail',
|
||||
seq: msg.seq,
|
||||
|
||||
+15
-12
@@ -25,6 +25,7 @@ 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 '../logger';
|
||||
|
||||
/**
|
||||
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
||||
@@ -124,16 +125,16 @@ IndexedDBStore.exists = function(indexedDB, dbName) {
|
||||
*/
|
||||
IndexedDBStore.prototype.startup = function() {
|
||||
if (this.startedUp) {
|
||||
console.log(`IndexedDBStore.startup: already started`);
|
||||
logger.log(`IndexedDBStore.startup: already started`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
console.log(`IndexedDBStore.startup: connecting to backend`);
|
||||
logger.log(`IndexedDBStore.startup: connecting to backend`);
|
||||
return this.backend.connect().then(() => {
|
||||
console.log(`IndexedDBStore.startup: loading presence events`);
|
||||
logger.log(`IndexedDBStore.startup: loading presence events`);
|
||||
return this.backend.getUserPresenceEvents();
|
||||
}).then((userPresenceEvents) => {
|
||||
console.log(`IndexedDBStore.startup: processing presence events`);
|
||||
logger.log(`IndexedDBStore.startup: processing presence events`);
|
||||
userPresenceEvents.forEach(([userId, rawEvent]) => {
|
||||
const u = new User(userId);
|
||||
if (rawEvent) {
|
||||
@@ -174,9 +175,9 @@ IndexedDBStore.prototype.getSavedSyncToken = degradable(function() {
|
||||
IndexedDBStore.prototype.deleteAllData = degradable(function() {
|
||||
MemoryStore.prototype.deleteAllData.call(this);
|
||||
return this.backend.clearDatabase().then(() => {
|
||||
console.log("Deleted indexeddb data.");
|
||||
logger.log("Deleted indexeddb data.");
|
||||
}, (err) => {
|
||||
console.error(`Failed to delete indexeddb data: ${err}`);
|
||||
logger.error(`Failed to delete indexeddb data: ${err}`);
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
@@ -197,11 +198,13 @@ 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() {
|
||||
if (this.wantsSave()) {
|
||||
IndexedDBStore.prototype.save = function(force) {
|
||||
if (force || this.wantsSave()) {
|
||||
return this._reallySave();
|
||||
}
|
||||
return Promise.resolve();
|
||||
@@ -290,18 +293,18 @@ function degradable(func, fallback) {
|
||||
try {
|
||||
return await func.call(this, ...args);
|
||||
} catch (e) {
|
||||
console.error("IndexedDBStore failure, degrading to MemoryStore", 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.
|
||||
console.log("IndexedDBStore trying to delete degraded data");
|
||||
logger.log("IndexedDBStore trying to delete degraded data");
|
||||
await this.backend.clearDatabase();
|
||||
console.log("IndexedDBStore delete after degrading succeeeded");
|
||||
logger.log("IndexedDBStore delete after degrading succeeeded");
|
||||
} catch (e) {
|
||||
console.warn("IndexedDBStore delete after degrading failed", 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
|
||||
|
||||
+3
-1
@@ -335,8 +335,10 @@ 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() {},
|
||||
save: function(force) {},
|
||||
|
||||
/**
|
||||
* Startup does nothing as this store doesn't require starting up.
|
||||
|
||||
@@ -22,6 +22,7 @@ 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.";
|
||||
@@ -257,7 +258,7 @@ function removeByPrefix(store, prefix) {
|
||||
|
||||
function debuglog() {
|
||||
if (DEBUG) {
|
||||
console.log(...arguments);
|
||||
logger.log(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import utils from "./utils";
|
||||
import logger from './logger';
|
||||
|
||||
|
||||
/**
|
||||
@@ -168,7 +169,7 @@ class SyncAccumulator {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown cateogory: ", category);
|
||||
logger.error("Unknown cateogory: ", category);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+51
-22
@@ -32,6 +32,8 @@ 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 './logger';
|
||||
|
||||
import {InvalidStoreError} from './errors';
|
||||
|
||||
@@ -58,7 +60,7 @@ function debuglog(...params) {
|
||||
if (!DEBUG) {
|
||||
return;
|
||||
}
|
||||
console.log(...params);
|
||||
logger.log(...params);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +128,9 @@ SyncApi.prototype.createRoom = function(roomId) {
|
||||
timelineSupport,
|
||||
unstableClientRelationAggregation,
|
||||
});
|
||||
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction",
|
||||
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline",
|
||||
"Room.redaction",
|
||||
"Room.redactionCancelled",
|
||||
"Room.receipt", "Room.tags",
|
||||
"Room.timelineReset",
|
||||
"Room.localEchoUpdated",
|
||||
@@ -392,7 +396,7 @@ SyncApi.prototype._peekPoll = function(peekRoom, token) {
|
||||
peekRoom.addLiveEvents(events);
|
||||
self._peekPoll(peekRoom, res.end);
|
||||
}, function(err) {
|
||||
console.error("[%s] Peek poll failed: %s", peekRoom.roomId, err);
|
||||
logger.error("[%s] Peek poll failed: %s", peekRoom.roomId, err);
|
||||
setTimeout(function() {
|
||||
self._peekPoll(peekRoom, token);
|
||||
}, 30 * 1000);
|
||||
@@ -454,7 +458,7 @@ SyncApi.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) {
|
||||
SyncApi.prototype._shouldAbortSync = function(error) {
|
||||
if (error.errcode === "M_UNKNOWN_TOKEN") {
|
||||
// The logout already happened, we just need to stop.
|
||||
console.warn("Token no longer valid - assuming logout");
|
||||
logger.warn("Token no longer valid - assuming logout");
|
||||
this.stop();
|
||||
return true;
|
||||
}
|
||||
@@ -494,7 +498,7 @@ SyncApi.prototype.sync = function() {
|
||||
|
||||
client.pushRules = result;
|
||||
} catch (err) {
|
||||
console.error("Getting push rules failed", 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
|
||||
@@ -522,7 +526,7 @@ SyncApi.prototype.sync = function() {
|
||||
);
|
||||
debuglog("Created and stored lazy load sync filter");
|
||||
} catch (err) {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Creating and storing lazy load sync filter failed",
|
||||
err,
|
||||
);
|
||||
@@ -546,7 +550,7 @@ 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.
|
||||
console.warn("InvalidStoreError: store is not usable: stopping sync.");
|
||||
logger.warn("InvalidStoreError: store is not usable: stopping sync.");
|
||||
return;
|
||||
}
|
||||
if (this.opts.lazyLoadMembers && this.opts.crypto) {
|
||||
@@ -557,7 +561,7 @@ SyncApi.prototype.sync = function() {
|
||||
await this.client._storeClientOptions();
|
||||
debuglog("Stored client options");
|
||||
} catch (err) {
|
||||
console.error("Storing client options failed", err);
|
||||
logger.error("Storing client options failed", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -580,7 +584,7 @@ SyncApi.prototype.sync = function() {
|
||||
getFilterName(client.credentials.userId), filter,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Getting filter failed", 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
|
||||
@@ -627,7 +631,7 @@ SyncApi.prototype.sync = function() {
|
||||
return self._syncFromCache(savedSync);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error("Getting saved sync failed", 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
|
||||
@@ -699,7 +703,7 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
|
||||
try {
|
||||
await this._processSyncResponse(syncEventData, data);
|
||||
} catch(e) {
|
||||
console.error("Error processing cached sync", e.stack || e);
|
||||
logger.error("Error processing cached sync", e.stack || e);
|
||||
}
|
||||
|
||||
// Don't emit a prepared if we've bailed because the store is invalid:
|
||||
@@ -774,7 +778,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
|
||||
console.error("Caught /sync error", e.stack || e);
|
||||
logger.error("Caught /sync error", e.stack || e);
|
||||
|
||||
// Emit the exception for client handling
|
||||
this.client.emit("sync.unexpectedError", e);
|
||||
@@ -888,15 +892,15 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("/sync error %s", err);
|
||||
console.error(err);
|
||||
logger.error("/sync error %s", err);
|
||||
logger.error(err);
|
||||
|
||||
if(this._shouldAbortSync(err)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._failedSyncCount++;
|
||||
console.log('Number of consecutive failed sync requests:', this._failedSyncCount);
|
||||
logger.log('Number of consecutive failed sync requests:', this._failedSyncCount);
|
||||
|
||||
debuglog("Starting keep-alive");
|
||||
// Note that we do *not* mark the sync connection as
|
||||
@@ -1027,8 +1031,9 @@ 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') {
|
||||
client.pushRules = accountDataEvent.getContent();
|
||||
if (accountDataEvent.getType() === 'm.push_rules') {
|
||||
const rules = accountDataEvent.getContent();
|
||||
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
|
||||
}
|
||||
client.emit("accountData", accountDataEvent);
|
||||
return accountDataEvent;
|
||||
@@ -1040,8 +1045,26 @@ 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();
|
||||
@@ -1050,13 +1073,21 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
content.msgtype == "m.bad.encrypted"
|
||||
) {
|
||||
// the mapper already logged a warning.
|
||||
console.log(
|
||||
logger.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);
|
||||
},
|
||||
);
|
||||
@@ -1216,10 +1247,8 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
room.setSummary(joinObj.summary);
|
||||
}
|
||||
|
||||
// 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 ephemeral events to the timeline
|
||||
room.addEphemeralEvents(ephemeralEvents);
|
||||
|
||||
// we deliberately don't add accountData to the timeline
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
+10
-8
@@ -19,6 +19,7 @@ limitations under the License.
|
||||
|
||||
import Promise from 'bluebird';
|
||||
const EventTimeline = require("./models/event-timeline");
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -28,7 +29,7 @@ const DEBUG = false;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
const debuglog = DEBUG ? console.log.bind(console) : function() {};
|
||||
const debuglog = DEBUG ? logger.log.bind(logger) : function() {};
|
||||
|
||||
/**
|
||||
* the number of times we ask the server for more events before giving up
|
||||
@@ -131,14 +132,15 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
||||
// feeling snappy.
|
||||
//
|
||||
if (initialEventId) {
|
||||
const prom = this._client.getEventTimeline(this._timelineSet, initialEventId);
|
||||
|
||||
if (prom.isFulfilled()) {
|
||||
initFields(prom.value());
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return prom.then(initFields);
|
||||
const timeline = this._timelineSet.getTimelineForEvent(initialEventId);
|
||||
if (timeline) {
|
||||
// hot-path optimization to save a reactor tick by replicating the sync check getTimelineForEvent does.
|
||||
initFields(timeline);
|
||||
return Promise.resolve(timeline);
|
||||
}
|
||||
|
||||
const prom = this._client.getEventTimeline(this._timelineSet, initialEventId);
|
||||
return prom.then(initFields);
|
||||
} else {
|
||||
const tl = this._timelineSet.getLiveTimeline();
|
||||
initFields(tl);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
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.
|
||||
@@ -20,6 +21,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
const unhomoglyph = require('unhomoglyph');
|
||||
import Promise from 'bluebird';
|
||||
|
||||
/**
|
||||
* Encode a dictionary of query parameters.
|
||||
@@ -699,3 +701,33 @@ module.exports.globToRegexp = function(glob, extended) {
|
||||
}
|
||||
return pat;
|
||||
};
|
||||
|
||||
module.exports.ensureNoTrailingSlash = function(url) {
|
||||
if (url && url.endsWith("/")) {
|
||||
return url.substr(0, url.length - 1);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
// Returns a promise which resolves with a given value after the given number of ms
|
||||
module.exports.sleep = (ms, value) => new Promise((resolve => {
|
||||
setTimeout(resolve, ms, value);
|
||||
}));
|
||||
|
||||
module.exports.isNullOrUndefined = function(val) {
|
||||
return val === null || val === undefined;
|
||||
};
|
||||
|
||||
// Returns a Deferred
|
||||
module.exports.defer = () => {
|
||||
let resolve;
|
||||
let reject;
|
||||
|
||||
const promise = new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
|
||||
return {resolve, reject, promise};
|
||||
};
|
||||
|
||||
+50
-50
@@ -21,6 +21,7 @@ limitations under the License.
|
||||
*/
|
||||
const utils = require("../utils");
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
import logger from '../logger';
|
||||
const DEBUG = true; // set true to enable console logging.
|
||||
|
||||
// events: hangup, error(err), replaced(call), state(state, oldState)
|
||||
@@ -60,9 +61,9 @@ function MatrixCall(opts) {
|
||||
this.URL = opts.URL;
|
||||
// Array of Objects with urls, username, credential keys
|
||||
this.turnServers = opts.turnServers || [];
|
||||
if (this.turnServers.length === 0) {
|
||||
if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
|
||||
this.turnServers.push({
|
||||
urls: [MatrixCall.FALLBACK_STUN_SERVER],
|
||||
urls: [MatrixCall.FALLBACK_ICE_SERVER],
|
||||
});
|
||||
}
|
||||
utils.forEach(this.turnServers, function(server) {
|
||||
@@ -91,8 +92,8 @@ function MatrixCall(opts) {
|
||||
}
|
||||
/** The length of time a call can be ringing for. */
|
||||
MatrixCall.CALL_TIMEOUT_MS = 60000;
|
||||
/** The fallback server to use for STUN. */
|
||||
MatrixCall.FALLBACK_STUN_SERVER = 'stun:stun.l.google.com:19302';
|
||||
/** The fallback ICE server to use for STUN or TURN protocols. */
|
||||
MatrixCall.FALLBACK_ICE_SERVER = 'stun:turn.matrix.org';
|
||||
/** An error code when the local client failed to create an offer. */
|
||||
MatrixCall.ERR_LOCAL_OFFER_FAILED = "local_offer_failed";
|
||||
/**
|
||||
@@ -195,7 +196,7 @@ MatrixCall.prototype.placeScreenSharingCall =
|
||||
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
|
||||
*/
|
||||
MatrixCall.prototype.playElement = function(element, queueId) {
|
||||
console.log("queuing play on " + queueId + " and element " + element);
|
||||
logger.log("queuing play on " + queueId + " and element " + element);
|
||||
// XXX: FIXME: Does this leak elements, given the old promises
|
||||
// may hang around and retain a reference to them?
|
||||
if (this.mediaPromises[queueId]) {
|
||||
@@ -206,10 +207,10 @@ MatrixCall.prototype.playElement = function(element, queueId) {
|
||||
// these failures may be non-fatal (as in the case of unmounts)
|
||||
this.mediaPromises[queueId] =
|
||||
this.mediaPromises[queueId].then(function() {
|
||||
console.log("previous promise completed for " + queueId);
|
||||
logger.log("previous promise completed for " + queueId);
|
||||
return element.play();
|
||||
}, function() {
|
||||
console.log("previous promise failed for " + queueId);
|
||||
logger.log("previous promise failed for " + queueId);
|
||||
return element.play();
|
||||
});
|
||||
} else {
|
||||
@@ -224,14 +225,14 @@ MatrixCall.prototype.playElement = function(element, queueId) {
|
||||
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
|
||||
*/
|
||||
MatrixCall.prototype.pauseElement = function(element, queueId) {
|
||||
console.log("queuing pause on " + queueId + " and element " + element);
|
||||
logger.log("queuing pause on " + queueId + " and element " + element);
|
||||
if (this.mediaPromises[queueId]) {
|
||||
this.mediaPromises[queueId] =
|
||||
this.mediaPromises[queueId].then(function() {
|
||||
console.log("previous promise completed for " + queueId);
|
||||
logger.log("previous promise completed for " + queueId);
|
||||
return element.pause();
|
||||
}, function() {
|
||||
console.log("previous promise failed for " + queueId);
|
||||
logger.log("previous promise failed for " + queueId);
|
||||
return element.pause();
|
||||
});
|
||||
} else {
|
||||
@@ -250,15 +251,15 @@ MatrixCall.prototype.pauseElement = function(element, queueId) {
|
||||
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
|
||||
*/
|
||||
MatrixCall.prototype.assignElement = function(element, srcObject, queueId) {
|
||||
console.log("queuing assign on " + queueId + " element " + element + " for " +
|
||||
logger.log("queuing assign on " + queueId + " element " + element + " for " +
|
||||
srcObject);
|
||||
if (this.mediaPromises[queueId]) {
|
||||
this.mediaPromises[queueId] =
|
||||
this.mediaPromises[queueId].then(function() {
|
||||
console.log("previous promise completed for " + queueId);
|
||||
logger.log("previous promise completed for " + queueId);
|
||||
element.srcObject = srcObject;
|
||||
}, function() {
|
||||
console.log("previous promise failed for " + queueId);
|
||||
logger.log("previous promise failed for " + queueId);
|
||||
element.srcObject = srcObject;
|
||||
});
|
||||
} else {
|
||||
@@ -664,7 +665,7 @@ MatrixCall.prototype._maybeGotUserMediaForAnswer = function(stream) {
|
||||
},
|
||||
};
|
||||
self.peerConn.createAnswer(function(description) {
|
||||
debuglog("Created answer: " + description);
|
||||
debuglog("Created answer: ", description);
|
||||
self.peerConn.setLocalDescription(description, function() {
|
||||
self._answerContent = {
|
||||
version: 0,
|
||||
@@ -753,7 +754,7 @@ MatrixCall.prototype._receivedAnswer = function(msg) {
|
||||
*/
|
||||
MatrixCall.prototype._gotLocalOffer = function(description) {
|
||||
const self = this;
|
||||
debuglog("Created offer: " + description);
|
||||
debuglog("Created offer: ", description);
|
||||
|
||||
if (self.state == 'ended') {
|
||||
debuglog("Ignoring newly created offer on call ID " + self.callId +
|
||||
@@ -1159,7 +1160,7 @@ const callError = function(code, msg) {
|
||||
|
||||
const debuglog = function() {
|
||||
if (DEBUG) {
|
||||
console.log(...arguments);
|
||||
logger.log(...arguments);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1216,24 +1217,9 @@ const _placeCallWithConstraints = function(self, constraints) {
|
||||
};
|
||||
|
||||
const _createPeerConnection = function(self) {
|
||||
let servers = self.turnServers;
|
||||
if (self.webRtc.vendor === "mozilla") {
|
||||
// modify turnServers struct to match what mozilla expects.
|
||||
servers = [];
|
||||
for (let i = 0; i < self.turnServers.length; i++) {
|
||||
for (let j = 0; j < self.turnServers[i].urls.length; j++) {
|
||||
servers.push({
|
||||
url: self.turnServers[i].urls[j],
|
||||
username: self.turnServers[i].username,
|
||||
credential: self.turnServers[i].credential,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pc = new self.webRtc.RtcPeerConnection({
|
||||
iceTransportPolicy: self.forceTURN ? 'relay' : undefined,
|
||||
iceServers: servers,
|
||||
iceServers: self.turnServers,
|
||||
});
|
||||
pc.oniceconnectionstatechange = hookCallback(self, self._onIceConnectionStateChanged);
|
||||
pc.onsignalingstatechange = hookCallback(self, self._onSignallingStateChanged);
|
||||
@@ -1351,7 +1337,9 @@ module.exports.setVideoInput = function(deviceId) { videoInput = deviceId; };
|
||||
* @param {MatrixClient} client The client instance to use.
|
||||
* @param {string} roomId The room the call is in.
|
||||
* @param {Object?} options DEPRECATED optional options map.
|
||||
* @param {boolean} options.forceTURN DEPRECATED whether relay through TURN should be forced. This option is deprecated - use opts.forceTURN when creating the matrix client since it's only possible to set this option on outbound calls.
|
||||
* @param {boolean} options.forceTURN DEPRECATED whether relay through TURN should be
|
||||
* forced. This option is deprecated - use opts.forceTURN when creating the matrix client
|
||||
* since it's only possible to set this option on outbound calls.
|
||||
* @return {MatrixCall} the call or null if the browser doesn't support calling.
|
||||
*/
|
||||
module.exports.createNewMatrixCall = function(client, roomId, options) {
|
||||
@@ -1382,24 +1370,36 @@ module.exports.createNewMatrixCall = function(client, roomId, options) {
|
||||
return getUserMedia.apply(w.navigator, arguments);
|
||||
};
|
||||
}
|
||||
webRtc.RtcPeerConnection = (
|
||||
w.RTCPeerConnection || w.webkitRTCPeerConnection || w.mozRTCPeerConnection
|
||||
);
|
||||
webRtc.RtcSessionDescription = (
|
||||
w.RTCSessionDescription || w.webkitRTCSessionDescription ||
|
||||
w.mozRTCSessionDescription
|
||||
);
|
||||
webRtc.RtcIceCandidate = (
|
||||
w.RTCIceCandidate || w.webkitRTCIceCandidate || w.mozRTCIceCandidate
|
||||
);
|
||||
webRtc.vendor = null;
|
||||
if (w.mozRTCPeerConnection) {
|
||||
webRtc.vendor = "mozilla";
|
||||
} else if (w.webkitRTCPeerConnection) {
|
||||
webRtc.vendor = "webkit";
|
||||
} else if (w.RTCPeerConnection) {
|
||||
webRtc.vendor = "generic";
|
||||
|
||||
// Firefox throws on so little as accessing the RTCPeerConnection when operating in
|
||||
// a secure mode. There's some information at https://bugzilla.mozilla.org/show_bug.cgi?id=1542616
|
||||
// though the concern is that the browser throwing a SecurityError will brick the
|
||||
// client creation process.
|
||||
try {
|
||||
webRtc.RtcPeerConnection = (
|
||||
w.RTCPeerConnection || w.webkitRTCPeerConnection || w.mozRTCPeerConnection
|
||||
);
|
||||
webRtc.RtcSessionDescription = (
|
||||
w.RTCSessionDescription || w.webkitRTCSessionDescription ||
|
||||
w.mozRTCSessionDescription
|
||||
);
|
||||
webRtc.RtcIceCandidate = (
|
||||
w.RTCIceCandidate || w.webkitRTCIceCandidate || w.mozRTCIceCandidate
|
||||
);
|
||||
webRtc.vendor = null;
|
||||
if (w.mozRTCPeerConnection) {
|
||||
webRtc.vendor = "mozilla";
|
||||
} else if (w.webkitRTCPeerConnection) {
|
||||
webRtc.vendor = "webkit";
|
||||
} else if (w.RTCPeerConnection) {
|
||||
webRtc.vendor = "generic";
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Failed to set up WebRTC object: possible browser interference?");
|
||||
logger.error(e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!webRtc.RtcIceCandidate || !webRtc.RtcSessionDescription ||
|
||||
!webRtc.RtcPeerConnection || !webRtc.getUserMedia) {
|
||||
return null; // WebRTC is not supported.
|
||||
|
||||
Reference in New Issue
Block a user