From 291e58a32c7a946bbb826a4d1fda3888b9418749 Mon Sep 17 00:00:00 2001 From: David Krmpotic Date: Wed, 29 Mar 2023 14:02:19 +0200 Subject: [PATCH 1/2] ~ ACCUMULATING FURTHER CONTRIBUTIONS FROM GitHub/DMTSYS ~ --- .version | 2 +- README.md | 8 +- .../{index-3c0f37a7.js => index-989ceaa9.js} | 8 +- apps/dmt-mobile/index.html | 2 +- apps/dmt-search/dmt/connectome-next/index.js | 4 + .../contentServer/checkPermission.js | 15 + .../contentServer--full--unused.js | 263 + .../contentServer/contentServer.js | 88 + .../feedBytesIntoChannel/binaryReader.js | 89 + .../feedBytesIntoChannel/streamFile.js | 35 + .../fileTransport/fiberHandle/encodePath.js | 11 + .../fileTransport/fiberHandle/fiberHandle.js | 15 + .../dmt/connectome-next/lib/utils/index.js | 65 + .../connectome-next/lib/utils/mime/HISTORY.md | 325 + .../connectome-next/lib/utils/mime/LICENSE | 23 + .../connectome-next/lib/utils/mime/README.md | 113 + .../connectome-next/lib/utils/mime/db.json | 8060 +++++++++++++++++ .../connectome-next/lib/utils/mime/index.js | 184 + .../lib/utils/mime/package.json | 87 + .../dmt/protocol/searchGUI/index.js | 2 +- .../dmt/protocol/searchGUI/objects/search.js | 2 +- apps/node_modules/.package-lock.json | 41 + apps/node_modules/connectome | 1 + apps/node_modules/connectome-next | 1 + apps/package-lock.json | 49 + apps/package.json | 9 +- ...up--brisi-after-kriptosola-rewrite.svelte} | 2 +- core/lib/dmt-frontend-components/src/index.js | 2 +- core/lib/dmt-gui-kit/components/Noise.svelte | 44 + .../dmt-gui-kit/components/Noise.svelte.d.ts | 23 + core/lib/dmt-gui-kit/index.d.ts | 1 + core/lib/dmt-gui-kit/index.js | 1 + core/lib/dmt-gui-kit/package.json | 3 +- .../apps-load/appFrontendList.js | 4 +- core/node/aspect-extend/apps-load/index.js | 43 +- core/node/aspect-extend/apps-load/loadApps.js | 38 +- core/node/aspect-extend/apps-serve/index.js | 2 +- .../aspect-extend/apps-serve/lib/server.js | 34 +- .../user-engine-load/modifyPackageJson.js | 9 + core/node/common/index.js | 5 + core/node/common/lib/dmtPreHelper.js | 2 + .../setupConnectionsCounter.js | 22 + .../lib/timeutils/formatMilliseconds.js | 30 +- core/node/connectome/dist/index.js | 227 +- core/node/connectome/dist/index.mjs | 227 +- core/node/connectome/dist/node/index.js | 2321 +++-- core/node/connectome/dist/node/index.mjs | 2318 +++-- .../node_modules/.package-lock.json | 24 +- .../node_modules/bufferutil/build/Makefile | 9 +- .../obj.target/bufferutil/src/bufferutil.o.d | 18 +- .../bufferutil/build/Release/bufferutil.node | Bin 52668 -> 52668 bytes .../obj.target/bufferutil/src/bufferutil.o | Bin 16768 -> 16800 bytes .../bufferutil/build/bufferutil.target.mk | 28 +- .../node_modules/bufferutil/build/config.gypi | 31 +- .../utf-8-validate/build/Makefile | 9 +- .../obj.target/validation/src/validation.o.d | 18 +- .../obj.target/validation/src/validation.o | Bin 8992 -> 9032 bytes .../build/Release/validation.node | Bin 51564 -> 51564 bytes .../utf-8-validate/build/config.gypi | 31 +- .../utf-8-validate/build/validation.target.mk | 28 +- core/node/connectome/node_modules/ws/LICENSE | 27 +- .../node/connectome/node_modules/ws/README.md | 190 +- core/node/connectome/node_modules/ws/index.js | 3 + .../node_modules/ws/lib/buffer-util.js | 62 +- .../node_modules/ws/lib/constants.js | 4 +- .../node_modules/ws/lib/event-target.js | 290 +- .../node_modules/ws/lib/extension.js | 36 +- .../node_modules/ws/lib/permessage-deflate.js | 35 +- .../node_modules/ws/lib/receiver.js | 210 +- .../connectome/node_modules/ws/lib/sender.js | 319 +- .../connectome/node_modules/ws/lib/stream.js | 48 +- .../node_modules/ws/lib/subprotocol.js | 62 + .../node_modules/ws/lib/validation.js | 74 +- .../node_modules/ws/lib/websocket-server.js | 291 +- .../node_modules/ws/lib/websocket.js | 624 +- .../connectome/node_modules/ws/package.json | 25 +- .../connectome/node_modules/ws/wrapper.mjs | 8 + core/node/connectome/package-lock.json | 33 +- core/node/connectome/package.json | 4 +- core/node/connectome/server/index.js | 2145 +++-- core/node/connectome/server/index.mjs | 2142 +++-- .../connect/establishAndMaintainConnection.js | 83 +- .../src/client/connector/connector.js | 2 +- .../src/client/connector/handshake.js | 35 +- .../src/client/connector/receive.js | 15 +- .../connectome/src/client/connector/send.js | 2 +- .../connectome/src/server/channel/channel.js | 2 + .../src/server/channel/channelList.js | 4 +- .../connectome/src/server/channel/receive.js | 8 +- .../connectome/src/server/channel/send.js | 6 +- .../src/server/connectome/wsServer.js | 13 +- core/node/connectome/src/stores-node/index.js | 6 +- .../connectome/src/stores-node/syncStore.js | 5 + .../src/stores-node/twoLevelMergeKVStore.js | 4 + core/node/connectome/src/stores/index.js | 6 +- core/node/connectome/stores/index.js | 232 +- core/node/connectome/stores/index.mjs | 233 +- core/node/connectome/stores/node/index.js | 232 +- core/node/connectome/stores/node/index.mjs | 233 +- core/node/connectome/yarn.lock | 454 +- core/node/controller/processes/abc/proc.js | 17 +- .../node/controller/processes/abc/startDMT.js | 2 +- core/node/controller/processes/dmt-proc.js | 15 +- core/node/controller/processes/manager.js | 6 + .../controller/program/connectionsAcceptor.js | 9 +- .../controller/program/connectomeLogging.js | 12 + .../program/interval/onProgramTick.js | 7 +- .../program/peerlist/createFiberPool.js | 10 +- core/node/controller/program/program.js | 10 +- core/node/gui/protocol/dmtGUI/index.js | 2 + core/node/node_modules/.bin/node-gyp-build | 1 + .../node_modules/.bin/node-gyp-build-optional | 1 + .../node_modules/.bin/node-gyp-build-test | 1 + core/node/node_modules/.package-lock.json | 95 +- core/node/node_modules/bufferutil/LICENSE | 19 + core/node/node_modules/bufferutil/README.md | 78 + core/node/node_modules/bufferutil/binding.gyp | 18 + core/node/node_modules/bufferutil/fallback.js | 34 + core/node/node_modules/bufferutil/index.js | 7 + .../node/node_modules/bufferutil/package.json | 36 + .../prebuilds/darwin-x64+arm64/node.napi.node | Bin 0 -> 116128 bytes .../prebuilds/linux-x64/node.napi.node | Bin 0 -> 10328 bytes .../prebuilds/win32-ia32/node.napi.node | Bin 0 -> 122368 bytes .../prebuilds/win32-x64/node.napi.node | Bin 0 -> 151552 bytes .../node_modules/bufferutil/src/bufferutil.c | 171 + .../mqtt/node_modules/ws/lib/validation.js | 30 - .../mqtt/node_modules/ws/package.json | 94 - core/node/node_modules/node-gyp-build/LICENSE | 21 + .../node_modules/node-gyp-build/README.md | 58 + core/node/node_modules/node-gyp-build/bin.js | 77 + .../node_modules/node-gyp-build/build-test.js | 19 + .../node/node_modules/node-gyp-build/index.js | 5 + .../node-gyp-build/node-gyp-build.js | 207 + .../node_modules/node-gyp-build/optional.js | 7 + .../node_modules/node-gyp-build/package.json | 29 + core/node/node_modules/utf-8-validate/LICENSE | 30 + .../node_modules/utf-8-validate/README.md | 50 + .../node_modules/utf-8-validate/binding.gyp | 18 + .../node_modules/utf-8-validate/fallback.js | 62 + .../node/node_modules/utf-8-validate/index.js | 7 + .../node_modules/utf-8-validate/package.json | 36 + .../prebuilds/darwin-x64+arm64/node.napi.node | Bin 0 -> 116000 bytes .../prebuilds/linux-x64/node.napi.node | Bin 0 -> 6232 bytes .../prebuilds/win32-ia32/node.napi.node | Bin 0 -> 121856 bytes .../prebuilds/win32-x64/node.napi.node | Bin 0 -> 150528 bytes .../utf-8-validate/src/validation.c | 109 + .../{mqtt/node_modules => }/ws/LICENSE | 0 .../{mqtt/node_modules => }/ws/README.md | 13 +- .../{mqtt/node_modules => }/ws/browser.js | 0 .../{mqtt/node_modules => }/ws/index.js | 0 .../node_modules => }/ws/lib/buffer-util.js | 0 .../node_modules => }/ws/lib/constants.js | 0 .../node_modules => }/ws/lib/event-target.js | 0 .../node_modules => }/ws/lib/extension.js | 0 .../{mqtt/node_modules => }/ws/lib/limiter.js | 0 .../ws/lib/permessage-deflate.js | 16 +- .../node_modules => }/ws/lib/receiver.js | 142 +- .../{mqtt/node_modules => }/ws/lib/sender.js | 6 +- .../{mqtt/node_modules => }/ws/lib/stream.js | 23 +- core/node/node_modules/ws/lib/validation.js | 104 + .../ws/lib/websocket-server.js | 69 +- .../node_modules => }/ws/lib/websocket.js | 368 +- core/node/node_modules/ws/package.json | 56 + core/node/package-lock.json | 131 +- etc/.abc_version | 2 +- etc/.deployignore | 1 - etc/integrate/README.md | 19 +- etc/integrate/dmt-integrate | 12 +- etc/integrate/editBase.js | 37 - etc/integrate/resetBase.js | 23 - .../dmt_apps/package.json | 9 +- .../dmt_user_engine/devDependencies.json | 3 + .../dmt_user_engine/exports.json | 5 +- .../dmt_user_engine/package.json | 1 + .../prepare_apps_and_user_engine/prepare_apps | 67 +- .../prepare_user_engine | 3 + shell/.bash_dep | 21 +- shell/.bash_dmt | 5 +- shell/.bash_short_useful | 4 + shell/.bash_util | 7 + 180 files changed, 21182 insertions(+), 4638 deletions(-) rename apps/dmt-mobile/assets/{index-3c0f37a7.js => index-989ceaa9.js} (96%) create mode 100644 apps/dmt-search/dmt/connectome-next/index.js create mode 100644 apps/dmt-search/dmt/connectome-next/lib/fileTransport/contentServer/checkPermission.js create mode 100644 apps/dmt-search/dmt/connectome-next/lib/fileTransport/contentServer/contentServer--full--unused.js create mode 100644 apps/dmt-search/dmt/connectome-next/lib/fileTransport/contentServer/contentServer.js create mode 100644 apps/dmt-search/dmt/connectome-next/lib/fileTransport/feedBytesIntoChannel/binaryReader.js create mode 100644 apps/dmt-search/dmt/connectome-next/lib/fileTransport/feedBytesIntoChannel/streamFile.js create mode 100644 apps/dmt-search/dmt/connectome-next/lib/fileTransport/fiberHandle/encodePath.js create mode 100644 apps/dmt-search/dmt/connectome-next/lib/fileTransport/fiberHandle/fiberHandle.js create mode 100644 apps/dmt-search/dmt/connectome-next/lib/utils/index.js create mode 100644 apps/dmt-search/dmt/connectome-next/lib/utils/mime/HISTORY.md create mode 100644 apps/dmt-search/dmt/connectome-next/lib/utils/mime/LICENSE create mode 100644 apps/dmt-search/dmt/connectome-next/lib/utils/mime/README.md create mode 100644 apps/dmt-search/dmt/connectome-next/lib/utils/mime/db.json create mode 100644 apps/dmt-search/dmt/connectome-next/lib/utils/mime/index.js create mode 100644 apps/dmt-search/dmt/connectome-next/lib/utils/mime/package.json create mode 100644 apps/node_modules/.package-lock.json create mode 120000 apps/node_modules/connectome create mode 120000 apps/node_modules/connectome-next create mode 100644 apps/package-lock.json rename core/lib/dmt-frontend-components/src/components/{Meetup.svelte => Meetup--brisi-after-kriptosola-rewrite.svelte} (99%) create mode 100644 core/lib/dmt-gui-kit/components/Noise.svelte create mode 100644 core/lib/dmt-gui-kit/components/Noise.svelte.d.ts create mode 100644 core/node/common/lib/protocolHelpers/setupConnectionsCounter.js create mode 100644 core/node/connectome/node_modules/ws/lib/subprotocol.js create mode 100644 core/node/connectome/node_modules/ws/wrapper.mjs create mode 100644 core/node/controller/program/connectomeLogging.js create mode 120000 core/node/node_modules/.bin/node-gyp-build create mode 120000 core/node/node_modules/.bin/node-gyp-build-optional create mode 120000 core/node/node_modules/.bin/node-gyp-build-test create mode 100644 core/node/node_modules/bufferutil/LICENSE create mode 100644 core/node/node_modules/bufferutil/README.md create mode 100644 core/node/node_modules/bufferutil/binding.gyp create mode 100644 core/node/node_modules/bufferutil/fallback.js create mode 100644 core/node/node_modules/bufferutil/index.js create mode 100644 core/node/node_modules/bufferutil/package.json create mode 100644 core/node/node_modules/bufferutil/prebuilds/darwin-x64+arm64/node.napi.node create mode 100644 core/node/node_modules/bufferutil/prebuilds/linux-x64/node.napi.node create mode 100644 core/node/node_modules/bufferutil/prebuilds/win32-ia32/node.napi.node create mode 100644 core/node/node_modules/bufferutil/prebuilds/win32-x64/node.napi.node create mode 100644 core/node/node_modules/bufferutil/src/bufferutil.c delete mode 100644 core/node/node_modules/mqtt/node_modules/ws/lib/validation.js delete mode 100644 core/node/node_modules/mqtt/node_modules/ws/package.json create mode 100644 core/node/node_modules/node-gyp-build/LICENSE create mode 100644 core/node/node_modules/node-gyp-build/README.md create mode 100755 core/node/node_modules/node-gyp-build/bin.js create mode 100755 core/node/node_modules/node-gyp-build/build-test.js create mode 100644 core/node/node_modules/node-gyp-build/index.js create mode 100644 core/node/node_modules/node-gyp-build/node-gyp-build.js create mode 100755 core/node/node_modules/node-gyp-build/optional.js create mode 100644 core/node/node_modules/node-gyp-build/package.json create mode 100644 core/node/node_modules/utf-8-validate/LICENSE create mode 100644 core/node/node_modules/utf-8-validate/README.md create mode 100644 core/node/node_modules/utf-8-validate/binding.gyp create mode 100644 core/node/node_modules/utf-8-validate/fallback.js create mode 100644 core/node/node_modules/utf-8-validate/index.js create mode 100644 core/node/node_modules/utf-8-validate/package.json create mode 100644 core/node/node_modules/utf-8-validate/prebuilds/darwin-x64+arm64/node.napi.node create mode 100644 core/node/node_modules/utf-8-validate/prebuilds/linux-x64/node.napi.node create mode 100644 core/node/node_modules/utf-8-validate/prebuilds/win32-ia32/node.napi.node create mode 100644 core/node/node_modules/utf-8-validate/prebuilds/win32-x64/node.napi.node create mode 100644 core/node/node_modules/utf-8-validate/src/validation.c rename core/node/node_modules/{mqtt/node_modules => }/ws/LICENSE (100%) rename core/node/node_modules/{mqtt/node_modules => }/ws/README.md (95%) rename core/node/node_modules/{mqtt/node_modules => }/ws/browser.js (100%) rename core/node/node_modules/{mqtt/node_modules => }/ws/index.js (100%) rename core/node/node_modules/{mqtt/node_modules => }/ws/lib/buffer-util.js (100%) rename core/node/node_modules/{mqtt/node_modules => }/ws/lib/constants.js (100%) rename core/node/node_modules/{mqtt/node_modules => }/ws/lib/event-target.js (100%) rename core/node/node_modules/{mqtt/node_modules => }/ws/lib/extension.js (100%) rename core/node/node_modules/{mqtt/node_modules => }/ws/lib/limiter.js (100%) rename core/node/node_modules/{mqtt/node_modules => }/ws/lib/permessage-deflate.js (97%) rename core/node/node_modules/{mqtt/node_modules => }/ws/lib/receiver.js (79%) rename core/node/node_modules/{mqtt/node_modules => }/ws/lib/sender.js (98%) rename core/node/node_modules/{mqtt/node_modules => }/ws/lib/stream.js (81%) create mode 100644 core/node/node_modules/ws/lib/validation.js rename core/node/node_modules/{mqtt/node_modules => }/ws/lib/websocket-server.js (84%) rename core/node/node_modules/{mqtt/node_modules => }/ws/lib/websocket.js (74%) create mode 100644 core/node/node_modules/ws/package.json delete mode 100644 etc/integrate/editBase.js delete mode 100644 etc/integrate/resetBase.js create mode 100644 etc/scripts/prepare_apps_and_user_engine/dmt_user_engine/devDependencies.json diff --git a/.version b/.version index 09b082787..51e20cfdb 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -1.2.200 · 2023-03-29 +1.2.208 · 2023-04-17 diff --git a/README.md b/README.md index 557be2dc7..34f787e0d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ ## What -[[[ [DMT SYSTEM](https://dmt-system.com/) ]]] is best understood as a set of always-running processes, one per device. The user has total control but also full responsibility for correct setup and specification of his or her needs. - -**DMT ENGINE** is like a canvas to paint desirable software-enabled functionalities on top. The more a user invests into the exploration of DMT SYSTEM, the more they stand to gain. It's hard-ish at first but then smooth as butter. - - Let's try it in another way: DMT SYSTEM is a computing platform for individual power users. Gooosh! Why so mysterious? Can't you just tell me what this is? Well, we could but then you'd have to ... +[[[ [DMT SYSTEM](https://dmt-system.com/) ]]] is a framework for creating powerful decentralized realtime apps. ## Install DMT ENGINE @@ -18,4 +14,4 @@ _The desire for excellence is an essential feature for doing great work. Without such a goal you will tend to wander like a drunken sailor. The sailor takes one step in one direction and the next in some independent direction. As a result the steps tend to cancel each other out, and the expected distance from the starting point is proportional to the square root of the number of steps taken. With a vision of excellence, and with the goal of doing significant work, there is a tendency for the steps to go in the same direction and thus go a distance proportional to the number of steps taken, which in a lifetime is a large number indeed._ -— *dr. [Richard Hamming](https://zetaseek.com/?q=Richard%20Hamming), helped invent the modern software* +— *dr. Richard Hamming, helped invent the modern software* diff --git a/apps/dmt-mobile/assets/index-3c0f37a7.js b/apps/dmt-mobile/assets/index-989ceaa9.js similarity index 96% rename from apps/dmt-mobile/assets/index-3c0f37a7.js rename to apps/dmt-mobile/assets/index-989ceaa9.js index 4661d50b0..2060c336d 100644 --- a/apps/dmt-mobile/assets/index-3c0f37a7.js +++ b/apps/dmt-mobile/assets/index-989ceaa9.js @@ -28,12 +28,12 @@ * https://github.com/Starcounter-Jack/JSON-Patch * (c) 2017 Joachim Wester * MIT license - */var si=new WeakMap,Mc=function(){function t(e){this.observers=new Map,this.obj=e}return t}(),Uc=function(){function t(e,n){this.callback=e,this.observer=n}return t}();function Lc(t){return si.get(t)}function jc(t,e){return t.observers.get(e)}function Bc(t,e){t.observers.delete(e.callback)}function zc(t,e){e.unobserve()}function Kc(t,e){var n=[],r,i=Lc(t);if(!i)i=new Mc(t),si.set(t,i);else{var s=jc(i,e);r=s&&s.observer}if(r)return r;if(r={},i.value=qt(t),e){r.callback=e,r.next=null;var l=function(){Jr(r)},c=function(){clearTimeout(r.next),r.next=setTimeout(l)};typeof window<"u"&&(window.addEventListener("mouseup",c),window.addEventListener("keyup",c),window.addEventListener("mousedown",c),window.addEventListener("keydown",c),window.addEventListener("change",c))}return r.patches=n,r.object=t,r.unobserve=function(){Jr(r),clearTimeout(r.next),Bc(i,r),typeof window<"u"&&(window.removeEventListener("mouseup",c),window.removeEventListener("keyup",c),window.removeEventListener("mousedown",c),window.removeEventListener("keydown",c),window.removeEventListener("change",c))},i.observers.set(e,new Uc(e,r)),r}function Jr(t,e){e===void 0&&(e=!1);var n=si.get(t.object);oi(n.value,t.object,t.patches,"",e),t.patches.length&&fi(n.value,t.patches);var r=t.patches;return r.length>0&&(t.patches=[],t.callback&&t.callback(r)),r}function oi(t,e,n,r,i){if(e!==t){typeof e.toJSON=="function"&&(e=e.toJSON());for(var s=Yr(e),l=Yr(t),c=!1,u=l.length-1;u>=0;u--){var h=l[u],p=t[h];if(qr(e,h)&&!(e[h]===void 0&&p!==void 0&&Array.isArray(e)===!1)){var b=e[h];typeof p=="object"&&p!=null&&typeof b=="object"&&b!=null?oi(p,b,n,r+"/"+Sn(h),i):p!==b&&(i&&n.push({op:"test",path:r+"/"+Sn(h),value:qt(p)}),n.push({op:"replace",path:r+"/"+Sn(h),value:qt(b)}))}else Array.isArray(t)===Array.isArray(e)?(i&&n.push({op:"test",path:r+"/"+Sn(h),value:qt(p)}),n.push({op:"remove",path:r+"/"+Sn(h)}),c=!0):(i&&n.push({op:"test",path:r,value:t}),n.push({op:"replace",path:r,value:e}))}if(!(!c&&s.length==l.length))for(var u=0;u{this.wireStateReceived=!0,this.set(n)}),this.connector.on("receive_diff",n=>{this.wireStateReceived&&(Hc(this.state,n),this.announceStateChange())})}field(e){return this.connector.connectionState.get(e)}},Wc=class extends dr{constructor(e){super({}),this.fields={},this.connector=e,this.connector.on("receive_state_field",({name:n,state:r})=>{this.get(n).set(r)})}get(e){return this.fields[e]||(this.fields[e]=new dr),this.fields[e]}};Zt.util=Fn;const Zc=700,Gc=6e4,Vc=1;let Qc=class extends ri{constructor({endpoint:e,protocol:n,keypair:r=kc(),rpcRequestTimeout:i,verbose:s=!1,tag:l,log:c=console.log,autoDecommission:u=!1,dummy:h}={}){super(),this.protocol=n,this.log=c;const{privateKey:p,publicKey:b}=Cc(r);this.clientPrivateKey=p,this.clientPublicKey=b,this.clientPublicKeyHex=ar(b),this.rpcClient=new Oc(this,i),this.endpoint=e,this.verbose=s,this.tag=l,this.autoDecommission=u,this.sentCount=0,this.receivedCount=0,this.successfulConnectsCount=0,h||(this.state=new Jc(this),this.connectionState=new Wc(this)),this.connected=new dr,this.delayedAdjustConnectionStatus(),s&&tt.green(this.log,`Connector ${this.endpoint} created`),this.decommissionCheckCounter=0,this.lastPongReceivedAt=Date.now(),this.on("pong",()=>{this.lastPongReceivedAt=Date.now()})}delayedAdjustConnectionStatus(){setTimeout(()=>{this.connected.get()==null&&this.connected.set(!1)},Zc)}send(e){sc({data:e,connector:this}),this.sentCount+=1}signal(e,n){this.connected.get()?this.send({signal:e,data:n}):tt.write(this.log,"Warning: trying to send signal over disconnected connector, this should be prevented by GUI")}userAction({action:e,scope:n,payload:r}){this.signal("__action",{action:e,scope:n,payload:r})}on(e,n){e=="ready"&&this.isReady()&&n(),super.on(e,n)}getSharedSecret(){return this.sharedSecret?ar(this.sharedSecret):void 0}wireReceive({jsonData:e,encryptedData:n,rawMessage:r}){Ss({jsonData:e,encryptedData:n,rawMessage:r,connector:this}),this.receivedCount+=1}field(e){return this.connectionState.get(e)}isReady(){return this.ready}closed(){return!this.transportConnected}connectStatus(e){if(e){this.sentCount=0,this.receivedCount=0,this.transportConnected=!0,this.successfulConnectsCount+=1,this.verbose&&tt.green(this.log,`✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`);const n=this.connection.websocket.__id;lc({connector:this,afterFirstStep:({sharedSecret:i,remotePubkeyHex:s})=>{this.sharedSecret=i,this._remotePubkeyHex=s}}).then(()=>{this.connectedAt=Date.now(),this.connected.set(!0),this.ready=!0,this.emit("ready")}).catch(i=>{this.connection.websocket.__id==n&&this.connection.websocket.readyState==Vc&&i.code==Gt.TIMEOUT&&(tt.write(this.log,`${this.endpoint} x Connector [ ${this.protocol} ] handshake error: "${i.message}"`),tt.write(this.log,`${this.endpoint} Connector dropping stale websocket after handshake error`),this.connection.terminate()),i.code!=Gt.TIMEOUT&&tt.write(this.log,`${this.endpoint} x Connector [ ${this.protocol} ] on:ready error: "${i.stack}" — (will not try to reconnect, fix the error and reload this gui)`)})}else{let n;this.transportConnected&&(n=!0),this.transportConnected==null&&tt.write(this.log,`${this.endpoint} Connector was not able to connect at first try`),this.transportConnected=!1,this.ready=!1,this.sharedSecret=void 0,delete this.connectedAt,n&&(this.emit("disconnect"),e==null&&this.delayedAdjustConnectionStatus(),this.connected.set(e))}}checkForDecommission(){this.autoDecommission&&(this.decommissionCheckRequestedAt&&Date.now()-this.decommissionCheckRequestedAt>3e3&&(this.decommissionCheckCounter=0),this.decommissionCheckRequestedAt=Date.now(),this.decommissionCheckCounter+=1,this.decommissionCheckCounter>12&&Date.now()-this.lastPongReceivedAt>Gc&&(tt.write(this.log,`Decommissioning connector ${this.endpoint} (long inactive)`),this.decommission(),this.emit("decommission")))}decommission(){this.decommissioned=!0}remoteObject(e){return{call:(n,r=[])=>this.rpcClient.remoteObject(e).call(n,Zl(r))}}attachObject(e,n){new Tc({serversideChannel:this,serverMethods:n,methodPrefix:e})}clientPubkey(){return this.clientPublicKeyHex}remotePubkeyHex(){return this._remotePubkeyHex}remoteAddress(){return this.endpoint}};const af=typeof window<"u";function Xc({endpoint:t,host:e,port:n}){if(af&&t&&t.startsWith("/")&&(t=`${window.location.protocol.includes("s")?"wss":"ws"}://${window.location.host}${t}`),!t)if(af){e=e||window.location.hostname;const r=window.location.protocol.includes("s")?"wss":"ws";t=`${r}://${e}`,r=="wss"?t=`${r}://${e}/ws`:n?t=`${t}:${n}`:window.location.port&&(t=`${t}:${window.location.port}`)}else{if(!n)throw new Error(`Connectome determineEndpoint: No websocket port provided for ${e}`);t=`ws://${e||"localhost"}:${n}`}return t}const an=typeof window<"u",ea=0,ta=1,na=1e3,ra=3,ia=5;function fa({endpoint:t,host:e,port:n,protocol:r,keypair:i,remotePubkey:s,rpcRequestTimeout:l,autoDecommission:c,log:u,verbose:h,tag:p,dummy:b},{WebSocket:m}){t=Xc({endpoint:t,host:e,port:n});const y=new Qc({endpoint:t,protocol:r,rpcRequestTimeout:l,keypair:i,verbose:h,tag:p,log:u,autoDecommission:c,dummy:b}),w=()=>{oa({connector:y,endpoint:t},{WebSocket:m,reconnect:w,log:u,verbose:h})};y.connection={terminate(){this.websocket._removeAllCallbacks(),this.websocket.close(),y.connectStatus(!1),w()},endpoint:t,checkTicker:0};const S=()=>{y.decommissioned||(sa({connector:y,reconnect:w,log:u}),setTimeout(S,na))};return setTimeout(S,10),y}function sa({connector:t,reconnect:e,log:n}){const r=t.connection;if(ca(r)||t.decommissioned){t.decommissioned?(tt.yellow(n,`${t.endpoint} Connection decommisioned, closing websocket ${r.websocket.__id}, will not retry again `),$s(t)):(t.emit("inactive_connection"),tt.yellow(n,`${t.endpoint} ✖ Terminated inactive connection`)),r.terminate();return}Ps(r)?r.websocket.send("ping"):(t.connected==null&&(tt.write(n,`${t.endpoint} Setting connector status to FALSE because connector.connected is undefined`),t.connectStatus(!1)),e()),r.checkTicker+=1}function oa({connector:t,endpoint:e},{WebSocket:n,reconnect:r,log:i,verbose:s}){const l=t.connection;if(t.checkForDecommission(),t.decommissioned){$s(t);return}if(l.currentlyTryingWS&&l.currentlyTryingWS.readyState==ea){if(l.currentlyTryingWS._waitForConnectCounter{});const u=()=>{t.decommissioned||((s||an)&&tt.write(i,`${e} Websocket open`),l.currentlyTryingWS=null,l.checkTicker=0,la({ws:c,connector:t,openCallback:u,reconnect:r},{log:i,verbose:s}),l.websocket=c,t.connectStatus(!0))};c._removeAllCallbacks=()=>{c.removeEventListener("open",u)},an?c.addEventListener("open",u):c.on("open",u)}function la({ws:t,connector:e,openCallback:n,reconnect:r},{log:i,verbose:s}){const l=e.connection,c=p=>{const b=`${e.endpoint} Websocket error`;console.log(b),console.log(p)},u=()=>{if(tt.write(i,`${e.endpoint} ✖ Connection closed`),e.decommissioned){e.connectStatus(!1);return}e.connectStatus(void 0),r()},h=p=>{if(e.decommissioned)return;l.checkTicker=0;const b=an?p.data:p;if(b=="pong"){e.emit("pong");return}let m;try{m=JSON.parse(b)}catch{}if(m)e.wireReceive({jsonData:m,rawMessage:b});else{const y=an?new Uint8Array(b):b;e.wireReceive({encryptedData:y})}};t._removeAllCallbacks=()=>{t.removeEventListener("error",c),t.removeEventListener("close",u),t.removeEventListener("message",h),t.removeEventListener("open",n)},an?(t.addEventListener("error",c),t.addEventListener("close",u),t.addEventListener("message",h)):(t.on("error",c),t.on("close",u),t.on("message",h))}function $s(t){const e=t.connection;e.currentlyTryingWS&&(e.currentlyTryingWS._removeAllCallbacks(),e.currentlyTryingWS.close(),e.currentlyTryingWS=null),e.ws&&(e.ws._removeAllCallbacks(),e.ws.close(),e.ws=null),t.connectStatus(!1)}function Ps(t){return t.websocket&&t.websocket.readyState==ta}function ca(t){return Ps(t)&&t.checkTicker>ra}function aa(t){return t.log=t.log||console.log,fa(t,{WebSocket})}function uf(t,e,n){const r=t.slice();return r[70]=e[n],r}function df(t,e,n){const r=t.slice();return r[70]=e[n],r}function ua(t){let e,n=(t[0].network||"")+"",r;return{c(){e=M("h3"),r=B(n),L(e,"class","svelte-xmpa81")},m(i,s){C(i,e,s),O(e,r)},p(i,s){s[0]&1&&n!==(n=(i[0].network||"")+"")&&Ie(r,n)},i:ge,o:ge,d(i){i&&T(e)}}}function da(t){let e,n,r,i,s;function l(h,p){return h[11]==0?_a:ha}let c=l(t),u=c(t);return{c(){e=M("h3"),n=B(`Vhod + */var si=new WeakMap,Mc=function(){function t(e){this.observers=new Map,this.obj=e}return t}(),Uc=function(){function t(e,n){this.callback=e,this.observer=n}return t}();function Lc(t){return si.get(t)}function jc(t,e){return t.observers.get(e)}function Bc(t,e){t.observers.delete(e.callback)}function zc(t,e){e.unobserve()}function Kc(t,e){var n=[],r,i=Lc(t);if(!i)i=new Mc(t),si.set(t,i);else{var s=jc(i,e);r=s&&s.observer}if(r)return r;if(r={},i.value=qt(t),e){r.callback=e,r.next=null;var l=function(){Jr(r)},c=function(){clearTimeout(r.next),r.next=setTimeout(l)};typeof window<"u"&&(window.addEventListener("mouseup",c),window.addEventListener("keyup",c),window.addEventListener("mousedown",c),window.addEventListener("keydown",c),window.addEventListener("change",c))}return r.patches=n,r.object=t,r.unobserve=function(){Jr(r),clearTimeout(r.next),Bc(i,r),typeof window<"u"&&(window.removeEventListener("mouseup",c),window.removeEventListener("keyup",c),window.removeEventListener("mousedown",c),window.removeEventListener("keydown",c),window.removeEventListener("change",c))},i.observers.set(e,new Uc(e,r)),r}function Jr(t,e){e===void 0&&(e=!1);var n=si.get(t.object);oi(n.value,t.object,t.patches,"",e),t.patches.length&&fi(n.value,t.patches);var r=t.patches;return r.length>0&&(t.patches=[],t.callback&&t.callback(r)),r}function oi(t,e,n,r,i){if(e!==t){typeof e.toJSON=="function"&&(e=e.toJSON());for(var s=Yr(e),l=Yr(t),c=!1,u=l.length-1;u>=0;u--){var h=l[u],p=t[h];if(qr(e,h)&&!(e[h]===void 0&&p!==void 0&&Array.isArray(e)===!1)){var b=e[h];typeof p=="object"&&p!=null&&typeof b=="object"&&b!=null?oi(p,b,n,r+"/"+Sn(h),i):p!==b&&(i&&n.push({op:"test",path:r+"/"+Sn(h),value:qt(p)}),n.push({op:"replace",path:r+"/"+Sn(h),value:qt(b)}))}else Array.isArray(t)===Array.isArray(e)?(i&&n.push({op:"test",path:r+"/"+Sn(h),value:qt(p)}),n.push({op:"remove",path:r+"/"+Sn(h)}),c=!0):(i&&n.push({op:"test",path:r,value:t}),n.push({op:"replace",path:r,value:e}))}if(!(!c&&s.length==l.length))for(var u=0;u{this.wireStateReceived=!0,this.set(n)}),this.connector.on("receive_diff",n=>{this.wireStateReceived&&(Hc(this.state,n),this.announceStateChange())})}field(e){return this.connector.connectionState.get(e)}},Wc=class extends dr{constructor(e){super({}),this.fields={},this.connector=e,this.connector.on("receive_state_field",({name:n,state:r})=>{this.get(n).set(r)})}get(e){return this.fields[e]||(this.fields[e]=new dr),this.fields[e]}};Zt.util=Fn;const Zc=700,Gc=6e4,Vc=1;let Qc=class extends ri{constructor({endpoint:e,protocol:n,keypair:r=kc(),rpcRequestTimeout:i,verbose:s=!1,tag:l,log:c=console.log,autoDecommission:u=!1,dummy:h}={}){super(),this.protocol=n,this.log=c;const{privateKey:p,publicKey:b}=Cc(r);this.clientPrivateKey=p,this.clientPublicKey=b,this.clientPublicKeyHex=ar(b),this.rpcClient=new Oc(this,i),this.endpoint=e,this.verbose=s,this.tag=l,this.autoDecommission=u,this.sentCount=0,this.receivedCount=0,this.successfulConnectsCount=0,h||(this.state=new Jc(this),this.connectionState=new Wc(this)),this.connected=new dr,this.delayedAdjustConnectionStatus(),s&&tt.green(this.log,`Connector ${this.endpoint} created`),this.decommissionCheckCounter=0,this.lastPongReceivedAt=Date.now(),this.on("pong",()=>{this.lastPongReceivedAt=Date.now()})}delayedAdjustConnectionStatus(){setTimeout(()=>{this.connected.get()==null&&this.connected.set(!1)},Zc)}send(e){sc({data:e,connector:this}),this.sentCount+=1}signal(e,n){this.connected.get()?this.send({signal:e,data:n}):tt.write(this.log,"Warning: trying to send signal over disconnected connector, this should be prevented by GUI")}userAction({action:e,scope:n,payload:r}){this.signal("__action",{action:e,scope:n,payload:r})}on(e,n){e=="ready"&&this.isReady()&&n(),super.on(e,n)}getSharedSecret(){return this.sharedSecret?ar(this.sharedSecret):void 0}wireReceive({jsonData:e,encryptedData:n,rawMessage:r}){Ss({jsonData:e,encryptedData:n,rawMessage:r,connector:this}),this.receivedCount+=1}field(e){return this.connectionState.get(e)}isReady(){return this.ready}closed(){return!this.transportConnected}connectStatus(e){if(e){this.sentCount=0,this.receivedCount=0,this.transportConnected=!0,this.successfulConnectsCount+=1,this.verbose&&tt.green(this.log,`✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`);const n=this.connection.websocket.__id;lc({connector:this,afterFirstStep:({sharedSecret:i,remotePubkeyHex:s})=>{this.sharedSecret=i,this._remotePubkeyHex=s}}).then(()=>{this.connectedAt=Date.now(),this.connected.set(!0),this.ready=!0,this.emit("ready")}).catch(i=>{this.connection.websocket.__id==n&&this.connection.websocket.readyState==Vc&&i.code==Gt.TIMEOUT&&(tt.write(this.log,`${this.endpoint} x Connector [ ${this.protocol} ] handshake error: "${i.message}"`),tt.write(this.log,`${this.endpoint} Connector dropping stale websocket after handshake error`),this.connection.terminate()),i.code!=Gt.TIMEOUT&&tt.write(this.log,`${this.endpoint} x Connector [ ${this.protocol} ] on:ready error: "${i.stack}" — (will not try to reconnect, fix the error and reload this gui)`)})}else{let n;this.transportConnected&&(n=!0),this.transportConnected==null&&tt.write(this.log,`${this.endpoint} Connector was not able to connect at first try`),this.transportConnected=!1,this.ready=!1,this.sharedSecret=void 0,delete this.connectedAt,n&&(this.emit("disconnect"),e==null&&this.delayedAdjustConnectionStatus(),this.connected.set(e))}}checkForDecommission(){this.autoDecommission&&(this.decommissionCheckRequestedAt&&Date.now()-this.decommissionCheckRequestedAt>3e3&&(this.decommissionCheckCounter=0),this.decommissionCheckRequestedAt=Date.now(),this.decommissionCheckCounter+=1,this.decommissionCheckCounter>12&&Date.now()-this.lastPongReceivedAt>Gc&&(tt.write(this.log,`Decommissioning connector ${this.endpoint} (long inactive)`),this.decommission(),this.emit("decommission")))}decommission(){this.decommissioned=!0}remoteObject(e){return{call:(n,r=[])=>this.rpcClient.remoteObject(e).call(n,Zl(r))}}attachObject(e,n){new Tc({serversideChannel:this,serverMethods:n,methodPrefix:e})}clientPubkey(){return this.clientPublicKeyHex}remotePubkeyHex(){return this._remotePubkeyHex}remoteAddress(){return this.endpoint}};const af=typeof window<"u";function Xc({endpoint:t,host:e,port:n}){if(af&&t&&t.startsWith("/")&&(t=`${window.location.protocol.includes("s")?"wss":"ws"}://${window.location.host}${t}`),!t)if(af){e=e||window.location.hostname;const r=window.location.protocol.includes("s")?"wss":"ws";t=`${r}://${e}`,r=="wss"?t=`${r}://${e}/ws`:n?t=`${t}:${n}`:window.location.port&&(t=`${t}:${window.location.port}`)}else{if(!n)throw new Error(`Connectome determineEndpoint: No websocket port provided for ${e}`);t=`ws://${e||"localhost"}:${n}`}return t}const an=typeof window<"u",ea=0,ta=1,na=1e3,ra=3,ia=5;function fa({endpoint:t,host:e,port:n,protocol:r,keypair:i,remotePubkey:s,rpcRequestTimeout:l,autoDecommission:c,log:u,verbose:h,tag:p,dummy:b},{WebSocket:m}){t=Xc({endpoint:t,host:e,port:n});const y=new Qc({endpoint:t,protocol:r,rpcRequestTimeout:l,keypair:i,verbose:h,tag:p,log:u,autoDecommission:c,dummy:b}),w=()=>{oa({connector:y,endpoint:t},{WebSocket:m,reconnect:w,log:u,verbose:h})};y.connection={terminate(){this.websocket._removeAllCallbacks(),this.websocket.close(),y.connectStatus(!1),w()},endpoint:t,checkTicker:0};const S=()=>{y.decommissioned||(sa({connector:y,reconnect:w,log:u}),setTimeout(S,na))};return setTimeout(S,10),y}function sa({connector:t,reconnect:e,log:n}){const r=t.connection;if(ca(r)||t.decommissioned){t.decommissioned?(tt.yellow(n,`${t.endpoint} Connection decommisioned, closing websocket ${r.websocket.__id}, will not retry again `),$s(t)):(t.emit("inactive_connection"),tt.yellow(n,`${t.endpoint} ✖ Terminated inactive connection`)),r.terminate();return}Ps(r)?r.websocket.send("ping"):(t.connected==null&&(tt.write(n,`${t.endpoint} Setting connector status to FALSE because connector.connected is undefined`),t.connectStatus(!1)),e()),r.checkTicker+=1}function oa({connector:t,endpoint:e},{WebSocket:n,reconnect:r,log:i,verbose:s}){const l=t.connection;if(t.checkForDecommission(),t.decommissioned){$s(t);return}if(l.currentlyTryingWS&&l.currentlyTryingWS.readyState==ea){if(l.currentlyTryingWS._waitForConnectCounter{});const u=()=>{t.decommissioned||((s||an)&&tt.write(i,`${e} Websocket open`),l.currentlyTryingWS=null,l.checkTicker=0,la({ws:c,connector:t,openCallback:u,reconnect:r},{log:i,verbose:s}),l.websocket=c,t.connectStatus(!0))};c._removeAllCallbacks=()=>{c.removeEventListener("open",u)},an?c.addEventListener("open",u):c.on("open",u)}function la({ws:t,connector:e,openCallback:n,reconnect:r},{log:i,verbose:s}){const l=e.connection,c=p=>{const b=`${e.endpoint} Websocket error`;console.log(b),console.log(p)},u=()=>{if(tt.write(i,`${e.endpoint} ✖ Connection [ ${e.protocol} ] closed`),e.decommissioned){e.connectStatus(!1);return}e.connectStatus(void 0),r()},h=p=>{if(e.decommissioned)return;l.checkTicker=0;const b=an?p.data:p;if(b=="pong"){e.emit("pong");return}let m;try{m=JSON.parse(b)}catch{}if(m)e.wireReceive({jsonData:m,rawMessage:b});else{const y=an?new Uint8Array(b):b;e.wireReceive({encryptedData:y})}};t._removeAllCallbacks=()=>{t.removeEventListener("error",c),t.removeEventListener("close",u),t.removeEventListener("message",h),t.removeEventListener("open",n)},an?(t.addEventListener("error",c),t.addEventListener("close",u),t.addEventListener("message",h)):(t.on("error",c),t.on("close",u),t.on("message",h))}function $s(t){const e=t.connection;e.currentlyTryingWS&&(e.currentlyTryingWS._removeAllCallbacks(),e.currentlyTryingWS.close(),e.currentlyTryingWS=null),e.ws&&(e.ws._removeAllCallbacks(),e.ws.close(),e.ws=null),t.connectStatus(!1)}function Ps(t){return t.websocket&&t.websocket.readyState==ta}function ca(t){return Ps(t)&&t.checkTicker>ra}function aa(t){return t.log=t.log||console.log,fa(t,{WebSocket})}function uf(t,e,n){const r=t.slice();return r[70]=e[n],r}function df(t,e,n){const r=t.slice();return r[70]=e[n],r}function ua(t){let e,n=(t[0].network||"")+"",r;return{c(){e=M("h3"),r=B(n),L(e,"class","svelte-xmpa81")},m(i,s){C(i,e,s),O(e,r)},p(i,s){s[0]&1&&n!==(n=(i[0].network||"")+"")&&Ie(r,n)},i:ge,o:ge,d(i){i&&T(e)}}}function da(t){let e,n,r,i,s;function l(h,p){return h[11]==0?_a:ha}let c=l(t),u=c(t);return{c(){e=M("h3"),n=B(`Vhod `),r=M("span"),i=B(`— odpiranje - `),u.c(),L(r,"class","svelte-xmpa81"),L(e,"class","countdown svelte-xmpa81")},m(h,p){C(h,e,p),O(e,n),O(e,r),O(r,i),u.m(r,null)},p(h,p){c===(c=l(h))&&u?u.p(h,p):(u.d(1),u=c(h),u&&(u.c(),u.m(r,null)))},i(h){s||jt(()=>{s=hn(r,us,{}),s.start()})},o:ge,d(h){h&&T(e),u.d()}}}function ha(t){let e,n,r;return{c(){e=B("čez "),n=B(t[11]),r=B("s")},m(i,s){C(i,e,s),C(i,n,s),C(i,r,s)},p(i,s){s[0]&2048&&Ie(n,i[11])},d(i){i&&T(e),i&&T(n),i&&T(r)}}}function _a(t){let e;return{c(){e=B("ZDAJ")},m(n,r){C(n,e,r)},p:ge,d(n){n&&T(e)}}}function hf(t){let e,n,r,i,s,l,c,u,h,p,b,m,y,w;return{c(){e=M("div"),n=M("button"),r=B("ODPRI ZDAJ"),s=F(),l=M("button"),c=B("Zakasnitev +10s"),h=F(),p=M("button"),b=B("Prekliči"),L(n,"class","open svelte-xmpa81"),n.disabled=i=!t[12]||t[11]==0,L(l,"class","delayed_open svelte-xmpa81"),l.disabled=u=!t[12]||t[11]==0,L(p,"class","cancel svelte-xmpa81"),p.disabled=m=!t[12]||t[11]==0,L(e,"class","entry_door_options svelte-xmpa81")},m(S,P){C(S,e,P),O(e,n),O(n,r),O(e,s),O(e,l),O(l,c),O(e,h),O(e,p),O(p,b),y||(w=[Se(n,"click",t[36]),Se(l,"click",t[37]),Se(p,"click",t[38])],y=!0)},p(S,P){P[0]&6144&&i!==(i=!S[12]||S[11]==0)&&(n.disabled=i),P[0]&6144&&u!==(u=!S[12]||S[11]==0)&&(l.disabled=u),P[0]&6144&&m!==(m=!S[12]||S[11]==0)&&(p.disabled=m)},d(S){S&&T(e),y=!1,yt(w)}}}function pa(t){var Jt,Ut;let e,n,r,i,s,l,c,u,h,p,b,m,y,w,S,P=t[6]&&((Jt=t[1])==null?void 0:Jt.find(t[33])),k,G=t[5]&&((Ut=t[1])==null?void 0:Ut.find(t[31])),X,j,q,z,be,ye,ue,De,ot,Xe,ft,Ae,Je,He,ht,it,Oe,qe,gt,Nt,We,et,de,Ze,Dt,Xt,St,$t,Ot,Mt,Ft,fn,_n;function pn(te,$e){return te[3]?va:ma}let Ht=pn(t),At=Ht(t);function vt(te,$e){return te[4]?ya:ga}let Tt=vt(t),Et=Tt(t),lt=P&&pf(t),ct=G&&gf(t),rt=t[12]&&t[8]!=null&&Sf(t);return{c(){e=M("h3"),e.textContent="Ograja — parkirišče",n=F(),r=M("div"),i=M("button"),s=B("ODPRI"),c=F(),u=M("button"),h=B("OSEBNI PREHOD"),b=F(),At.c(),m=F(),y=M("h3"),y.textContent="Dnevna TV",w=F(),Et.c(),S=F(),lt&<.c(),k=F(),ct&&ct.c(),X=F(),j=M("h3"),j.textContent="Luč kabinet",q=F(),z=M("button"),be=B("PRIŽGI LUČ"),ue=F(),De=M("button"),ot=B("🛑 UGASNI"),ft=F(),Ae=M("button"),Je=B("⏱️"),ht=F(),rt&&rt.c(),it=F(),Oe=M("h3"),Oe.textContent="Alarm",qe=F(),gt=M("button"),Nt=B("VKLOPI"),et=F(),de=M("button"),Ze=B("IZKLOPI"),Xt=F(),St=M("h3"),St.textContent="Dodatne možnosti",$t=F(),Ot=M("button"),Mt=B("— ZAPRI VSE —"),L(e,"class","svelte-xmpa81"),i.disabled=l=!t[12]||t[10]||t[9],L(i,"class","svelte-xmpa81"),L(u,"class","personal_entry svelte-xmpa81"),u.disabled=p=!t[12]||t[10],ce(u,"personal_entry_in_progress",t[9]),L(r,"class","parking_door_options svelte-xmpa81"),L(y,"class","svelte-xmpa81"),L(j,"class","svelte-xmpa81"),z.disabled=ye=!t[12],L(z,"class","svelte-xmpa81"),L(De,"class","turn_off svelte-xmpa81"),De.disabled=Xe=!t[12],Ae.disabled=He=!t[12],L(Ae,"class","svelte-xmpa81"),L(Oe,"class","svelte-xmpa81"),gt.disabled=We=!t[12],L(gt,"class","svelte-xmpa81"),de.disabled=Dt=!t[12],L(de,"class","svelte-xmpa81"),L(St,"class","close_options svelte-xmpa81"),L(Ot,"class","close_options svelte-xmpa81"),Ot.disabled=Ft=!t[12]},m(te,$e){C(te,e,$e),C(te,n,$e),C(te,r,$e),O(r,i),O(i,s),O(r,c),O(r,u),O(u,h),O(r,b),At.m(r,null),C(te,m,$e),C(te,y,$e),C(te,w,$e),Et.m(te,$e),C(te,S,$e),lt&<.m(te,$e),C(te,k,$e),ct&&ct.m(te,$e),C(te,X,$e),C(te,j,$e),C(te,q,$e),C(te,z,$e),O(z,be),C(te,ue,$e),C(te,De,$e),O(De,ot),C(te,ft,$e),C(te,Ae,$e),O(Ae,Je),C(te,ht,$e),rt&&rt.m(te,$e),C(te,it,$e),C(te,Oe,$e),C(te,qe,$e),C(te,gt,$e),O(gt,Nt),C(te,et,$e),C(te,de,$e),O(de,Ze),C(te,Xt,$e),C(te,St,$e),C(te,$t,$e),C(te,Ot,$e),O(Ot,Mt),fn||(_n=[Se(i,"click",t[40]),Se(u,"click",t[41]),Se(z,"click",t[63]),Se(De,"click",t[64]),Se(Ae,"click",t[65]),Se(gt,"click",t[66]),Se(de,"click",t[67]),Se(Ot,"click",t[68])],fn=!0)},p(te,$e){var nn,bn;$e[0]&5632&&l!==(l=!te[12]||te[10]||te[9])&&(i.disabled=l),$e[0]&5120&&p!==(p=!te[12]||te[10])&&(u.disabled=p),$e[0]&512&&ce(u,"personal_entry_in_progress",te[9]),Ht===(Ht=pn(te))&&At?At.p(te,$e):(At.d(1),At=Ht(te),At&&(At.c(),At.m(r,null))),Tt===(Tt=vt(te))&&Et?Et.p(te,$e):(Et.d(1),Et=Tt(te),Et&&(Et.c(),Et.m(S.parentNode,S))),$e[0]&66&&(P=te[6]&&((nn=te[1])==null?void 0:nn.find(te[33]))),P?lt?lt.p(te,$e):(lt=pf(te),lt.c(),lt.m(k.parentNode,k)):lt&&(lt.d(1),lt=null),$e[0]&34&&(G=te[5]&&((bn=te[1])==null?void 0:bn.find(te[31]))),G?ct?ct.p(te,$e):(ct=gf(te),ct.c(),ct.m(X.parentNode,X)):ct&&(ct.d(1),ct=null),$e[0]&4096&&ye!==(ye=!te[12])&&(z.disabled=ye),$e[0]&4096&&Xe!==(Xe=!te[12])&&(De.disabled=Xe),$e[0]&4096&&He!==(He=!te[12])&&(Ae.disabled=He),te[12]&&te[8]!=null?rt?(rt.p(te,$e),$e[0]&4352&&re(rt,1)):(rt=Sf(te),rt.c(),re(rt,1),rt.m(it.parentNode,it)):rt&&(rt.d(1),rt=null),$e[0]&4096&&We!==(We=!te[12])&&(gt.disabled=We),$e[0]&4096&&Dt!==(Dt=!te[12])&&(de.disabled=Dt),$e[0]&4096&&Ft!==(Ft=!te[12])&&(Ot.disabled=Ft)},i(te){re(rt)},o:ge,d(te){te&&T(e),te&&T(n),te&&T(r),At.d(),te&&T(m),te&&T(y),te&&T(w),Et.d(te),te&&T(S),lt&<.d(te),te&&T(k),ct&&ct.d(te),te&&T(X),te&&T(j),te&&T(q),te&&T(z),te&&T(ue),te&&T(De),te&&T(ft),te&&T(Ae),te&&T(ht),rt&&rt.d(te),te&&T(it),te&&T(Oe),te&&T(qe),te&&T(gt),te&&T(et),te&&T(de),te&&T(Xt),te&&T(St),te&&T($t),te&&T(Ot),fn=!1,yt(_n)}}}function ba(t){let e,n,r,i,s=t[13]&&Of(t);return{c(){s&&s.c(),e=F(),n=M("button"),n.textContent="Več možnosti",L(n,"class","show_more_options svelte-xmpa81")},m(l,c){s&&s.m(l,c),C(l,e,c),C(l,n,c),r||(i=Se(n,"click",t[39]),r=!0)},p(l,c){l[13]?s?s.p(l,c):(s=Of(l),s.c(),s.m(e.parentNode,e)):s&&(s.d(1),s=null)},i:ge,o:ge,d(l){s&&s.d(l),l&&T(e),l&&T(n),r=!1,i()}}}function va(t){let e,n,r,i,s,l,c,u,h,p,b,m,y,w=t[10]&&_f();return{c(){e=M("h4"),e.textContent="— Odpri in ohrani —",n=F(),r=M("button"),i=B("ODPRI IN OHRANI"),l=F(),c=M("button"),u=B("ZAPRI OGRAJO"),p=F(),w&&w.c(),b=bt(),L(e,"class","suboption svelte-xmpa81"),L(r,"class","fence_keep_open svelte-xmpa81"),r.disabled=s=!t[12]||t[9],ce(r,"keep_open_in_progress",t[10]),L(c,"class","close_fence svelte-xmpa81"),c.disabled=h=!t[12]||t[10]||t[9]},m(S,P){C(S,e,P),C(S,n,P),C(S,r,P),O(r,i),C(S,l,P),C(S,c,P),O(c,u),C(S,p,P),w&&w.m(S,P),C(S,b,P),m||(y=[Se(r,"click",t[43]),Se(c,"click",t[44])],m=!0)},p(S,P){P[0]&4608&&s!==(s=!S[12]||S[9])&&(r.disabled=s),P[0]&1024&&ce(r,"keep_open_in_progress",S[10]),P[0]&5632&&h!==(h=!S[12]||S[10]||S[9])&&(c.disabled=h),S[10]?w||(w=_f(),w.c(),w.m(b.parentNode,b)):w&&(w.d(1),w=null)},d(S){S&&T(e),S&&T(n),S&&T(r),S&&T(l),S&&T(c),S&&T(p),w&&w.d(S),S&&T(b),m=!1,yt(y)}}}function ma(t){let e,n,r;return{c(){e=M("button"),e.textContent="Več",L(e,"class","show_more_options svelte-xmpa81")},m(i,s){C(i,e,s),n||(r=Se(e,"click",t[42]),n=!0)},p:ge,d(i){i&&T(e),n=!1,r()}}}function _f(t){let e;return{c(){e=M("h4"),e.textContent="[ ⚠️ v zadnji sekundi odpiranja ne sme biti prehoda, sicer vrata ne bodo ostala odprta ]",L(e,"class","suboption warn svelte-xmpa81")},m(n,r){C(n,e,r)},d(n){n&&T(e)}}}function ya(t){let e,n,r,i,s,l,c,u,h,p,b,m,y,w,S=(t[14]||"")+"",P,k,G,X,j,q,z,be,ye,ue,De,ot,Xe,ft,Ae,Je,He,ht,it;return{c(){e=M("button"),n=B("TV"),i=F(),s=M("button"),l=B("DMT"),u=F(),h=M("button"),p=B("🛑 UGASNI"),m=F(),y=M("h4"),y.textContent="— TV glasnost —",w=F(),P=B(S),k=F(),G=M("button"),X=B("VOL↑"),q=F(),z=M("button"),be=B("VOL ↓"),ue=F(),De=M("button"),ot=B("PRIVZETO"),ft=F(),Ae=M("button"),Je=B("🔇"),e.disabled=r=!t[12],L(e,"class","svelte-xmpa81"),s.disabled=c=!t[12],L(s,"class","svelte-xmpa81"),L(h,"class","turn_off svelte-xmpa81"),h.disabled=b=!t[12],L(y,"class","suboption volume svelte-xmpa81"),L(G,"class","volume svelte-xmpa81"),G.disabled=j=!t[12],L(z,"class","volume svelte-xmpa81"),z.disabled=ye=!t[12],L(De,"class","volume svelte-xmpa81"),De.disabled=Xe=!t[12],L(Ae,"class","volume svelte-xmpa81"),Ae.disabled=He=!t[12]},m(Oe,qe){C(Oe,e,qe),O(e,n),C(Oe,i,qe),C(Oe,s,qe),O(s,l),C(Oe,u,qe),C(Oe,h,qe),O(h,p),C(Oe,m,qe),C(Oe,y,qe),C(Oe,w,qe),C(Oe,P,qe),C(Oe,k,qe),C(Oe,G,qe),O(G,X),C(Oe,q,qe),C(Oe,z,qe),O(z,be),C(Oe,ue,qe),C(Oe,De,qe),O(De,ot),C(Oe,ft,qe),C(Oe,Ae,qe),O(Ae,Je),ht||(it=[Se(e,"click",t[46]),Se(s,"click",t[47]),Se(h,"click",t[48]),Se(G,"click",t[49]),Se(z,"click",t[50]),Se(De,"click",t[51]),Se(Ae,"click",t[52])],ht=!0)},p(Oe,qe){qe[0]&4096&&r!==(r=!Oe[12])&&(e.disabled=r),qe[0]&4096&&c!==(c=!Oe[12])&&(s.disabled=c),qe[0]&4096&&b!==(b=!Oe[12])&&(h.disabled=b),qe[0]&16384&&S!==(S=(Oe[14]||"")+"")&&Ie(P,S),qe[0]&4096&&j!==(j=!Oe[12])&&(G.disabled=j),qe[0]&4096&&ye!==(ye=!Oe[12])&&(z.disabled=ye),qe[0]&4096&&Xe!==(Xe=!Oe[12])&&(De.disabled=Xe),qe[0]&4096&&He!==(He=!Oe[12])&&(Ae.disabled=He)},d(Oe){Oe&&T(e),Oe&&T(i),Oe&&T(s),Oe&&T(u),Oe&&T(h),Oe&&T(m),Oe&&T(y),Oe&&T(w),Oe&&T(P),Oe&&T(k),Oe&&T(G),Oe&&T(q),Oe&&T(z),Oe&&T(ue),Oe&&T(De),Oe&&T(ft),Oe&&T(Ae),ht=!1,yt(it)}}}function ga(t){let e,n,r;return{c(){e=M("button"),e.textContent="Možnosti TV",L(e,"class","show_more_options svelte-xmpa81")},m(i,s){C(i,e,s),n||(r=Se(e,"click",t[45]),n=!0)},p:ge,d(i){i&&T(e),n=!1,r()}}}function pf(t){let e,n,r,i=t[17],s=[];for(let l=0;l{s=hn(r,us,{}),s.start()})},o:ge,d(h){h&&T(e),u.d()}}}function ha(t){let e,n,r;return{c(){e=B("čez "),n=B(t[11]),r=B("s")},m(i,s){C(i,e,s),C(i,n,s),C(i,r,s)},p(i,s){s[0]&2048&&Ie(n,i[11])},d(i){i&&T(e),i&&T(n),i&&T(r)}}}function _a(t){let e;return{c(){e=B("ZDAJ")},m(n,r){C(n,e,r)},p:ge,d(n){n&&T(e)}}}function hf(t){let e,n,r,i,s,l,c,u,h,p,b,m,y,w;return{c(){e=M("div"),n=M("button"),r=B("Zakasnitev +10s"),s=F(),l=M("button"),c=B("ODPRI ZDAJ"),h=F(),p=M("button"),b=B("Prekliči"),L(n,"class","delayed_open svelte-xmpa81"),n.disabled=i=!t[12]||t[11]==0,L(l,"class","open svelte-xmpa81"),l.disabled=u=!t[12]||t[11]==0,L(p,"class","cancel svelte-xmpa81"),p.disabled=m=!t[12]||t[11]==0,L(e,"class","entry_door_options svelte-xmpa81")},m(S,P){C(S,e,P),O(e,n),O(n,r),O(e,s),O(e,l),O(l,c),O(e,h),O(e,p),O(p,b),y||(w=[Se(n,"click",t[36]),Se(l,"click",t[37]),Se(p,"click",t[38])],y=!0)},p(S,P){P[0]&6144&&i!==(i=!S[12]||S[11]==0)&&(n.disabled=i),P[0]&6144&&u!==(u=!S[12]||S[11]==0)&&(l.disabled=u),P[0]&6144&&m!==(m=!S[12]||S[11]==0)&&(p.disabled=m)},d(S){S&&T(e),y=!1,yt(w)}}}function pa(t){var Jt,Ut;let e,n,r,i,s,l,c,u,h,p,b,m,y,w,S,P=t[6]&&((Jt=t[1])==null?void 0:Jt.find(t[33])),k,G=t[5]&&((Ut=t[1])==null?void 0:Ut.find(t[31])),X,j,q,z,be,ye,ue,De,ot,Xe,ft,Ae,Je,He,ht,it,Oe,qe,gt,Nt,We,et,de,Ze,Dt,Xt,St,$t,Ot,Mt,Ft,fn,_n;function pn(te,$e){return te[3]?va:ma}let Ht=pn(t),At=Ht(t);function vt(te,$e){return te[4]?ya:ga}let Tt=vt(t),Et=Tt(t),lt=P&&pf(t),ct=G&&gf(t),rt=t[12]&&t[8]!=null&&Sf(t);return{c(){e=M("h3"),e.textContent="Ograja — parkirišče",n=F(),r=M("div"),i=M("button"),s=B("ODPRI"),c=F(),u=M("button"),h=B("OSEBNI PREHOD"),b=F(),At.c(),m=F(),y=M("h3"),y.textContent="Dnevna TV",w=F(),Et.c(),S=F(),lt&<.c(),k=F(),ct&&ct.c(),X=F(),j=M("h3"),j.textContent="Luč kabinet",q=F(),z=M("button"),be=B("PRIŽGI LUČ"),ue=F(),De=M("button"),ot=B("🛑 UGASNI"),ft=F(),Ae=M("button"),Je=B("⏱️"),ht=F(),rt&&rt.c(),it=F(),Oe=M("h3"),Oe.textContent="Alarm",qe=F(),gt=M("button"),Nt=B("VKLOPI"),et=F(),de=M("button"),Ze=B("IZKLOPI"),Xt=F(),St=M("h3"),St.textContent="Dodatne možnosti",$t=F(),Ot=M("button"),Mt=B("— ZAPRI VSE —"),L(e,"class","svelte-xmpa81"),i.disabled=l=!t[12]||t[10]||t[9],L(i,"class","svelte-xmpa81"),L(u,"class","personal_entry svelte-xmpa81"),u.disabled=p=!t[12]||t[10],ce(u,"personal_entry_in_progress",t[9]),L(r,"class","parking_door_options svelte-xmpa81"),L(y,"class","svelte-xmpa81"),L(j,"class","svelte-xmpa81"),z.disabled=ye=!t[12],L(z,"class","svelte-xmpa81"),L(De,"class","turn_off svelte-xmpa81"),De.disabled=Xe=!t[12],Ae.disabled=He=!t[12],L(Ae,"class","svelte-xmpa81"),L(Oe,"class","svelte-xmpa81"),gt.disabled=We=!t[12],L(gt,"class","svelte-xmpa81"),de.disabled=Dt=!t[12],L(de,"class","svelte-xmpa81"),L(St,"class","close_options svelte-xmpa81"),L(Ot,"class","close_options svelte-xmpa81"),Ot.disabled=Ft=!t[12]},m(te,$e){C(te,e,$e),C(te,n,$e),C(te,r,$e),O(r,i),O(i,s),O(r,c),O(r,u),O(u,h),O(r,b),At.m(r,null),C(te,m,$e),C(te,y,$e),C(te,w,$e),Et.m(te,$e),C(te,S,$e),lt&<.m(te,$e),C(te,k,$e),ct&&ct.m(te,$e),C(te,X,$e),C(te,j,$e),C(te,q,$e),C(te,z,$e),O(z,be),C(te,ue,$e),C(te,De,$e),O(De,ot),C(te,ft,$e),C(te,Ae,$e),O(Ae,Je),C(te,ht,$e),rt&&rt.m(te,$e),C(te,it,$e),C(te,Oe,$e),C(te,qe,$e),C(te,gt,$e),O(gt,Nt),C(te,et,$e),C(te,de,$e),O(de,Ze),C(te,Xt,$e),C(te,St,$e),C(te,$t,$e),C(te,Ot,$e),O(Ot,Mt),fn||(_n=[Se(i,"click",t[40]),Se(u,"click",t[41]),Se(z,"click",t[63]),Se(De,"click",t[64]),Se(Ae,"click",t[65]),Se(gt,"click",t[66]),Se(de,"click",t[67]),Se(Ot,"click",t[68])],fn=!0)},p(te,$e){var nn,bn;$e[0]&5632&&l!==(l=!te[12]||te[10]||te[9])&&(i.disabled=l),$e[0]&5120&&p!==(p=!te[12]||te[10])&&(u.disabled=p),$e[0]&512&&ce(u,"personal_entry_in_progress",te[9]),Ht===(Ht=pn(te))&&At?At.p(te,$e):(At.d(1),At=Ht(te),At&&(At.c(),At.m(r,null))),Tt===(Tt=vt(te))&&Et?Et.p(te,$e):(Et.d(1),Et=Tt(te),Et&&(Et.c(),Et.m(S.parentNode,S))),$e[0]&66&&(P=te[6]&&((nn=te[1])==null?void 0:nn.find(te[33]))),P?lt?lt.p(te,$e):(lt=pf(te),lt.c(),lt.m(k.parentNode,k)):lt&&(lt.d(1),lt=null),$e[0]&34&&(G=te[5]&&((bn=te[1])==null?void 0:bn.find(te[31]))),G?ct?ct.p(te,$e):(ct=gf(te),ct.c(),ct.m(X.parentNode,X)):ct&&(ct.d(1),ct=null),$e[0]&4096&&ye!==(ye=!te[12])&&(z.disabled=ye),$e[0]&4096&&Xe!==(Xe=!te[12])&&(De.disabled=Xe),$e[0]&4096&&He!==(He=!te[12])&&(Ae.disabled=He),te[12]&&te[8]!=null?rt?(rt.p(te,$e),$e[0]&4352&&re(rt,1)):(rt=Sf(te),rt.c(),re(rt,1),rt.m(it.parentNode,it)):rt&&(rt.d(1),rt=null),$e[0]&4096&&We!==(We=!te[12])&&(gt.disabled=We),$e[0]&4096&&Dt!==(Dt=!te[12])&&(de.disabled=Dt),$e[0]&4096&&Ft!==(Ft=!te[12])&&(Ot.disabled=Ft)},i(te){re(rt)},o:ge,d(te){te&&T(e),te&&T(n),te&&T(r),At.d(),te&&T(m),te&&T(y),te&&T(w),Et.d(te),te&&T(S),lt&<.d(te),te&&T(k),ct&&ct.d(te),te&&T(X),te&&T(j),te&&T(q),te&&T(z),te&&T(ue),te&&T(De),te&&T(ft),te&&T(Ae),te&&T(ht),rt&&rt.d(te),te&&T(it),te&&T(Oe),te&&T(qe),te&&T(gt),te&&T(et),te&&T(de),te&&T(Xt),te&&T(St),te&&T($t),te&&T(Ot),fn=!1,yt(_n)}}}function ba(t){let e,n,r,i,s=t[13]&&Of(t);return{c(){s&&s.c(),e=F(),n=M("button"),n.textContent="Več možnosti",L(n,"class","show_more_options svelte-xmpa81")},m(l,c){s&&s.m(l,c),C(l,e,c),C(l,n,c),r||(i=Se(n,"click",t[39]),r=!0)},p(l,c){l[13]?s?s.p(l,c):(s=Of(l),s.c(),s.m(e.parentNode,e)):s&&(s.d(1),s=null)},i:ge,o:ge,d(l){s&&s.d(l),l&&T(e),l&&T(n),r=!1,i()}}}function va(t){let e,n,r,i,s,l,c,u,h,p,b,m,y,w=t[10]&&_f();return{c(){e=M("h4"),e.textContent="— Odpri in ohrani —",n=F(),r=M("button"),i=B("ODPRI IN OHRANI"),l=F(),c=M("button"),u=B("ZAPRI OGRAJO"),p=F(),w&&w.c(),b=bt(),L(e,"class","suboption svelte-xmpa81"),L(r,"class","fence_keep_open svelte-xmpa81"),r.disabled=s=!t[12]||t[9],ce(r,"keep_open_in_progress",t[10]),L(c,"class","close_fence svelte-xmpa81"),c.disabled=h=!t[12]||t[10]||t[9]},m(S,P){C(S,e,P),C(S,n,P),C(S,r,P),O(r,i),C(S,l,P),C(S,c,P),O(c,u),C(S,p,P),w&&w.m(S,P),C(S,b,P),m||(y=[Se(r,"click",t[43]),Se(c,"click",t[44])],m=!0)},p(S,P){P[0]&4608&&s!==(s=!S[12]||S[9])&&(r.disabled=s),P[0]&1024&&ce(r,"keep_open_in_progress",S[10]),P[0]&5632&&h!==(h=!S[12]||S[10]||S[9])&&(c.disabled=h),S[10]?w||(w=_f(),w.c(),w.m(b.parentNode,b)):w&&(w.d(1),w=null)},d(S){S&&T(e),S&&T(n),S&&T(r),S&&T(l),S&&T(c),S&&T(p),w&&w.d(S),S&&T(b),m=!1,yt(y)}}}function ma(t){let e,n,r;return{c(){e=M("button"),e.textContent="Več",L(e,"class","show_more_options svelte-xmpa81")},m(i,s){C(i,e,s),n||(r=Se(e,"click",t[42]),n=!0)},p:ge,d(i){i&&T(e),n=!1,r()}}}function _f(t){let e;return{c(){e=M("h4"),e.textContent="[ ⚠️ v zadnji sekundi odpiranja ne sme biti prehoda, sicer vrata ne bodo ostala odprta ]",L(e,"class","suboption warn svelte-xmpa81")},m(n,r){C(n,e,r)},d(n){n&&T(e)}}}function ya(t){let e,n,r,i,s,l,c,u,h,p,b,m,y,w,S=(t[14]||"")+"",P,k,G,X,j,q,z,be,ye,ue,De,ot,Xe,ft,Ae,Je,He,ht,it;return{c(){e=M("button"),n=B("TV"),i=F(),s=M("button"),l=B("DMT"),u=F(),h=M("button"),p=B("🛑 UGASNI"),m=F(),y=M("h4"),y.textContent="— TV glasnost —",w=F(),P=B(S),k=F(),G=M("button"),X=B("VOL↑"),q=F(),z=M("button"),be=B("VOL ↓"),ue=F(),De=M("button"),ot=B("PRIVZETO"),ft=F(),Ae=M("button"),Je=B("🔇"),e.disabled=r=!t[12],L(e,"class","svelte-xmpa81"),s.disabled=c=!t[12],L(s,"class","svelte-xmpa81"),L(h,"class","turn_off svelte-xmpa81"),h.disabled=b=!t[12],L(y,"class","suboption volume svelte-xmpa81"),L(G,"class","volume svelte-xmpa81"),G.disabled=j=!t[12],L(z,"class","volume svelte-xmpa81"),z.disabled=ye=!t[12],L(De,"class","volume svelte-xmpa81"),De.disabled=Xe=!t[12],L(Ae,"class","volume svelte-xmpa81"),Ae.disabled=He=!t[12]},m(Oe,qe){C(Oe,e,qe),O(e,n),C(Oe,i,qe),C(Oe,s,qe),O(s,l),C(Oe,u,qe),C(Oe,h,qe),O(h,p),C(Oe,m,qe),C(Oe,y,qe),C(Oe,w,qe),C(Oe,P,qe),C(Oe,k,qe),C(Oe,G,qe),O(G,X),C(Oe,q,qe),C(Oe,z,qe),O(z,be),C(Oe,ue,qe),C(Oe,De,qe),O(De,ot),C(Oe,ft,qe),C(Oe,Ae,qe),O(Ae,Je),ht||(it=[Se(e,"click",t[46]),Se(s,"click",t[47]),Se(h,"click",t[48]),Se(G,"click",t[49]),Se(z,"click",t[50]),Se(De,"click",t[51]),Se(Ae,"click",t[52])],ht=!0)},p(Oe,qe){qe[0]&4096&&r!==(r=!Oe[12])&&(e.disabled=r),qe[0]&4096&&c!==(c=!Oe[12])&&(s.disabled=c),qe[0]&4096&&b!==(b=!Oe[12])&&(h.disabled=b),qe[0]&16384&&S!==(S=(Oe[14]||"")+"")&&Ie(P,S),qe[0]&4096&&j!==(j=!Oe[12])&&(G.disabled=j),qe[0]&4096&&ye!==(ye=!Oe[12])&&(z.disabled=ye),qe[0]&4096&&Xe!==(Xe=!Oe[12])&&(De.disabled=Xe),qe[0]&4096&&He!==(He=!Oe[12])&&(Ae.disabled=He)},d(Oe){Oe&&T(e),Oe&&T(i),Oe&&T(s),Oe&&T(u),Oe&&T(h),Oe&&T(m),Oe&&T(y),Oe&&T(w),Oe&&T(P),Oe&&T(k),Oe&&T(G),Oe&&T(q),Oe&&T(z),Oe&&T(ue),Oe&&T(De),Oe&&T(ft),Oe&&T(Ae),ht=!1,yt(it)}}}function ga(t){let e,n,r;return{c(){e=M("button"),e.textContent="Možnosti TV",L(e,"class","show_more_options svelte-xmpa81")},m(i,s){C(i,e,s),n||(r=Se(e,"click",t[45]),n=!0)},p:ge,d(i){i&&T(e),n=!1,r()}}}function pf(t){let e,n,r,i=t[17],s=[];for(let l=0;l{i=hn(r,us,{}),i.start()})},o:ge,d(u){u&&T(e),c.d()}}}function Ca(t){let e,n;return{c(){e=B("čez "),n=B(t[7])},m(r,i){C(r,e,i),C(r,n,i)},p(r,i){i[0]&128&&Ie(n,r[7])},d(r){r&&T(e),r&&T(n)}}}function Ra(t){let e;return{c(){e=B("ZDAJ")},m(n,r){C(n,e,r)},p:ge,d(n){n&&T(e)}}}function Of(t){let e,n,r=(t[0].network||"")+"",i;return{c(){e=M("h3"),n=B("Več možnosti - "),i=B(r),L(e,"class","svelte-xmpa81")},m(s,l){C(s,e,l),O(e,n),O(e,i)},p(s,l){l[0]&1&&r!==(r=(s[0].network||"")+"")&&Ie(i,r)},d(s){s&&T(e)}}}function Na(t){let e,n,r,i,s,l,c,u,h;function p(k,G){return k[12]&&k[13]?da:ua}let b=p(t),m=b(t),y=t[13]&&hf(t);function w(k,G){return k[2]?pa:ba}let S=w(t),P=S(t);return{c(){e=M("div"),m.c(),n=F(),r=M("button"),i=B("ODPIRANJE VHODNIH VRAT"),l=F(),y&&y.c(),c=F(),P.c(),L(r,"class","open_door_show_options svelte-xmpa81"),r.disabled=s=!t[12],ce(r,"invisible",t[13]),L(e,"class","options svelte-xmpa81")},m(k,G){C(k,e,G),m.m(e,null),O(e,n),O(e,r),O(r,i),O(e,l),y&&y.m(e,null),O(e,c),P.m(e,null),u||(h=Se(r,"click",t[35]),u=!0)},p(k,G){b===(b=p(k))&&m?m.p(k,G):(m.d(1),m=b(k),m&&(m.c(),re(m,1),m.m(e,n))),G[0]&4096&&s!==(s=!k[12])&&(r.disabled=s),G[0]&8192&&ce(r,"invisible",k[13]),k[13]?y?y.p(k,G):(y=hf(k),y.c(),y.m(e,c)):y&&(y.d(1),y=null),S===(S=w(k))&&P?P.p(k,G):(P.d(1),P=S(k),P&&(P.c(),re(P,1),P.m(e,null)))},i(k){re(m),re(P)},o:ge,d(k){k&&T(e),m.d(),y&&y.d(),P.d(),u=!1,h()}}}const Da="192.168.0.20",$a=7780,Pa="david/home";function Ia(t,e,n){let r,i,s,l,c,u,h,p,b,m,y,{localDevice:w}=e,{nearbyDevices:S}=e;const P=aa({host:Da,port:$a,protocol:Pa}),{connected:k,state:G}=P;wt(t,k,Ee=>n(12,b=Ee)),wt(t,G,Ee=>n(30,p=Ee));const X=["kids","eclipse"],j=["david-room","ela-room"];w.deviceName=="turbine"&&X.push("turbine");const q=P.field("tvVolume");wt(t,q,Ee=>n(14,y=Ee));const z=Oo(G,Ee=>{var Ye;return((Ye=Ee.entryDoor)==null?void 0:Ye.counter)!=null});wt(t,z,Ee=>n(13,m=Ee));function be(Ee,Ye){P.userAction({action:Ee,payload:Ye,scope:"tv"})}function ye(Ee,Ye){P.userAction({action:Ee,payload:Ye,scope:"alarm"})}function ue(Ee,Ye){P.userAction({action:Ee,payload:Ye,scope:"entry-door"})}function De(Ee,Ye){P.userAction({action:Ee,payload:Ye,scope:"fence-door"})}function ot(Ee,Ye){P.userAction({action:Ee,payload:Ye,scope:"light-lab"})}function Xe(Ee,Ye){P.userAction({action:Ee,scope:"nearbyDevices",payload:{deviceName:Ye}})}function ft(Ee,Ye){P.userAction({action:Ee,scope:"nearbyDevices",payload:{deviceName:Ye,delay:!0}})}function Ae(Ee,Ye){P.userAction({action:Ee,scope:"nearbyDevices",payload:{deviceName:Ye,cancel:!0}})}let Je=w.deviceName=="turbine",He,ht;function it(){n(2,Je=!1),n(4,ht=!1),n(3,He=!1)}const Oe=({deviceName:Ee})=>j.includes(Ee),qe=(Ee,Ye)=>Ye.deviceName==Ee,gt=({deviceName:Ee})=>X.includes(Ee),Nt=(Ee,Ye)=>Ye.deviceName==Ee,We=()=>{ue("delayed-open")},et=()=>{ue("open")},de=()=>{ue("add-delay")},Ze=()=>{ue("cancel-opening")},Dt=()=>{n(2,Je=!0)},Xt=()=>{De("move")},St=()=>{De("personal-entry")},$t=()=>{n(3,He=!0)},Ot=()=>{De("keep-open")},Mt=()=>{De("close")},Ft=()=>{n(4,ht=!0),be("vol-report")},fn=()=>{be("hdmi1")},_n=()=>{be("hdmi2")},pn=()=>{be("off")},Ht=()=>{be("vol-up")},At=()=>{be("vol-down")},vt=()=>{be("vol-default")},Tt=()=>{be("mute")},Et=Ee=>{ft("sleep",Ee)},lt=Ee=>{Xe("sleep",Ee)},ct=Ee=>{ft("sleep",Ee)},rt=Ee=>{Ae("sleep",Ee)},Jt=(Ee,Ye)=>Ye.deviceName==Ee,Ut=Ee=>{ft("kid_sleep",Ee)},te=(Ee,Ye)=>Ye.deviceName==Ee,$e=Ee=>{Xe("kid_sleep",Ee)},nn=Ee=>{ft("kid_sleep",Ee)},bn=Ee=>{Ae("kid_sleep",Ee)},sn=()=>{ot("on")},on=()=>{ot("off")},en=()=>{ot("off-delay")},Bt=()=>{ye("enable")},Pt=()=>{ye("disable")},zt=()=>{it(),window.scrollTo({top:0,behavior:"smooth"})};return t.$$set=Ee=>{"localDevice"in Ee&&n(0,w=Ee.localDevice),"nearbyDevices"in Ee&&n(1,S=Ee.nearbyDevices)},t.$$.update=()=>{var Ee,Ye,Lt,rn,vn;t.$$.dirty[0]&1073741824&&n(11,r=(Ee=p.entryDoor)==null?void 0:Ee.counter),t.$$.dirty[0]&1073741824&&n(10,i=(Ye=p.parkingDoor)==null?void 0:Ye.keepOpenInProgress),t.$$.dirty[0]&1073741824&&n(9,s=(Lt=p.parkingDoor)==null?void 0:Lt.personalEntryInProgress),t.$$.dirty[0]&1073741824&&n(8,l=(rn=p.lights)==null?void 0:rn.labLightOffDelay),t.$$.dirty[0]&1073741824&&n(7,c=(vn=p.lights)==null?void 0:vn.labLightOffDelayStr),t.$$.dirty[0]&1073741824&&n(6,u=p.deviceSleepStarter),t.$$.dirty[0]&1073741824&&n(5,h=p.kidSleepStarter)},[w,S,Je,He,ht,h,u,c,l,s,i,r,b,m,y,k,G,X,j,q,z,be,ye,ue,De,ot,Xe,ft,Ae,it,p,Oe,qe,gt,Nt,We,et,de,Ze,Dt,Xt,St,$t,Ot,Mt,Ft,fn,_n,pn,Ht,At,vt,Tt,Et,lt,ct,rt,Jt,Ut,te,$e,nn,bn,sn,on,en,Bt,Pt,zt]}class Ma extends dt{constructor(e){super(),ut(this,e,Ia,Na,at,{localDevice:0,nearbyDevices:1},null,[-1,-1,-1])}}function Ua(t){let e,n=Tf(t[2])+"",r,i,s=t[1].replace("blinds","")+"",l,c,u,h;return{c(){e=M("button"),r=B(n),i=F(),l=B(s),e.disabled=c=!t[4],L(e,"class","svelte-1u4dkh7"),ce(e,"moving",t[3]&&t[3][mn(t[0],t[1],t[2])]&&t[3][mn(t[0],t[1],t[2])].blindsStatus=="moving"),ce(e,"present",t[3]&&t[3][mn(t[0],t[1],t[2])]&&t[3][mn(t[0],t[1],t[2])].present),ce(e,"disconnected",t[4]==!1)},m(p,b){C(p,e,b),O(e,r),O(e,i),O(e,l),u||(h=Se(e,"click",t[10]),u=!0)},p(p,[b]){b&4&&n!==(n=Tf(p[2])+"")&&Ie(r,n),b&2&&s!==(s=p[1].replace("blinds","")+"")&&Ie(l,s),b&16&&c!==(c=!p[4])&&(e.disabled=c),b&15&&ce(e,"moving",p[3]&&p[3][mn(p[0],p[1],p[2])]&&p[3][mn(p[0],p[1],p[2])].blindsStatus=="moving"),b&15&&ce(e,"present",p[3]&&p[3][mn(p[0],p[1],p[2])]&&p[3][mn(p[0],p[1],p[2])].present),b&16&&ce(e,"disconnected",p[4]==!1)},i:ge,o:ge,d(p){p&&T(e),u=!1,h()}}}function Tf(t){return t=="up"?"▲":"▼"}function mn(t,e,n){return`${t}-${e}-${n}`}function La(t,e,n){let r,i,s,{connector:l}=e,{placeId:c}=e,{blindsId:u}=e,{blindsDirection:h}=e;const{state:p,connected:b}=l;wt(t,p,S=>n(9,i=S)),wt(t,b,S=>n(4,s=S));function m(S,P){l.signal("action",{action:S,scope:"iot",payload:P})}function y(S,P,k){m("blinds",{placeId:S,blindsId:P,blindsDirection:k,blindsAction:"move"})}const w=()=>y(c,u,h);return t.$$set=S=>{"connector"in S&&n(8,l=S.connector),"placeId"in S&&n(0,c=S.placeId),"blindsId"in S&&n(1,u=S.blindsId),"blindsDirection"in S&&n(2,h=S.blindsDirection)},t.$$.update=()=>{t.$$.dirty&512&&n(3,r=i.blinds)},[c,u,h,r,s,p,b,y,l,i,w]}class mt extends dt{constructor(e){super(),ut(this,e,La,Ua,at,{connector:8,placeId:0,blindsId:1,blindsDirection:2})}}function ja(t){let e,n,r,i,s,l,c,u,h,p,b,m,y,w,S,P,k,G,X,j,q;return s=new mt({props:{placeId:"ap1",blindsId:"blinds4",blindsDirection:"up",connector:t[0]}}),c=new mt({props:{placeId:"ap1",blindsId:"blinds3",blindsDirection:"up",connector:t[0]}}),h=new mt({props:{placeId:"ap1",blindsId:"blinds2",blindsDirection:"up",connector:t[0]}}),b=new mt({props:{placeId:"ap1",blindsId:"blinds1",blindsDirection:"up",connector:t[0]}}),w=new mt({props:{placeId:"ap1",blindsId:"blinds4",blindsDirection:"down",connector:t[0]}}),P=new mt({props:{placeId:"ap1",blindsId:"blinds3",blindsDirection:"down",connector:t[0]}}),G=new mt({props:{placeId:"ap1",blindsId:"blinds2",blindsDirection:"down",connector:t[0]}}),j=new mt({props:{placeId:"ap1",blindsId:"blinds1",blindsDirection:"down",connector:t[0]}}),{c(){e=M("div"),n=M("h2"),n.textContent="Rolete",r=F(),i=M("div"),Ke(s.$$.fragment),l=F(),Ke(c.$$.fragment),u=B(` + `),r=M("span"),c.c(),L(r,"class","svelte-xmpa81"),L(e,"class","countdown svelte-xmpa81")},m(u,h){C(u,e,h),O(e,n),O(e,r),c.m(r,null)},p(u,h){l===(l=s(u))&&c?c.p(u,h):(c.d(1),c=l(u),c&&(c.c(),c.m(r,null)))},i(u){i||jt(()=>{i=hn(r,us,{}),i.start()})},o:ge,d(u){u&&T(e),c.d()}}}function Ca(t){let e,n;return{c(){e=B("čez "),n=B(t[7])},m(r,i){C(r,e,i),C(r,n,i)},p(r,i){i[0]&128&&Ie(n,r[7])},d(r){r&&T(e),r&&T(n)}}}function Ra(t){let e;return{c(){e=B("ZDAJ")},m(n,r){C(n,e,r)},p:ge,d(n){n&&T(e)}}}function Of(t){let e,n,r=(t[0].network||"")+"",i;return{c(){e=M("h3"),n=B("Več možnosti - "),i=B(r),L(e,"class","svelte-xmpa81")},m(s,l){C(s,e,l),O(e,n),O(e,i)},p(s,l){l[0]&1&&r!==(r=(s[0].network||"")+"")&&Ie(i,r)},d(s){s&&T(e)}}}function Na(t){let e,n,r,i,s,l,c,u,h;function p(k,G){return k[12]&&k[13]?da:ua}let b=p(t),m=b(t),y=t[13]&&hf(t);function w(k,G){return k[2]?pa:ba}let S=w(t),P=S(t);return{c(){e=M("div"),m.c(),n=F(),r=M("button"),i=B("ODPIRANJE VHODNIH VRAT"),l=F(),y&&y.c(),c=F(),P.c(),L(r,"class","open_door_show_options svelte-xmpa81"),r.disabled=s=!t[12],ce(r,"invisible",t[13]),L(e,"class","options svelte-xmpa81")},m(k,G){C(k,e,G),m.m(e,null),O(e,n),O(e,r),O(r,i),O(e,l),y&&y.m(e,null),O(e,c),P.m(e,null),u||(h=Se(r,"click",t[35]),u=!0)},p(k,G){b===(b=p(k))&&m?m.p(k,G):(m.d(1),m=b(k),m&&(m.c(),re(m,1),m.m(e,n))),G[0]&4096&&s!==(s=!k[12])&&(r.disabled=s),G[0]&8192&&ce(r,"invisible",k[13]),k[13]?y?y.p(k,G):(y=hf(k),y.c(),y.m(e,c)):y&&(y.d(1),y=null),S===(S=w(k))&&P?P.p(k,G):(P.d(1),P=S(k),P&&(P.c(),re(P,1),P.m(e,null)))},i(k){re(m),re(P)},o:ge,d(k){k&&T(e),m.d(),y&&y.d(),P.d(),u=!1,h()}}}const Da="192.168.0.20",$a=7780,Pa="david/home";function Ia(t,e,n){let r,i,s,l,c,u,h,p,b,m,y,{localDevice:w}=e,{nearbyDevices:S}=e;const P=aa({host:Da,port:$a,protocol:Pa}),{connected:k,state:G}=P;wt(t,k,Ee=>n(12,b=Ee)),wt(t,G,Ee=>n(30,p=Ee));const X=["kids","eclipse"],j=["david-room","ela-room"];w.deviceName=="turbine"&&X.push("turbine");const q=P.field("tvVolume");wt(t,q,Ee=>n(14,y=Ee));const z=Oo(G,Ee=>{var Ye;return((Ye=Ee.entryDoor)==null?void 0:Ye.counter)!=null});wt(t,z,Ee=>n(13,m=Ee));function be(Ee,Ye){P.userAction({action:Ee,payload:Ye,scope:"tv"})}function ye(Ee,Ye){P.userAction({action:Ee,payload:Ye,scope:"alarm"})}function ue(Ee,Ye){P.userAction({action:Ee,payload:Ye,scope:"entry-door"})}function De(Ee,Ye){P.userAction({action:Ee,payload:Ye,scope:"fence-door"})}function ot(Ee,Ye){P.userAction({action:Ee,payload:Ye,scope:"light-lab"})}function Xe(Ee,Ye){P.userAction({action:Ee,scope:"nearbyDevices",payload:{deviceName:Ye}})}function ft(Ee,Ye){P.userAction({action:Ee,scope:"nearbyDevices",payload:{deviceName:Ye,delay:!0}})}function Ae(Ee,Ye){P.userAction({action:Ee,scope:"nearbyDevices",payload:{deviceName:Ye,cancel:!0}})}let Je=w.deviceName=="turbine",He,ht;function it(){n(2,Je=!1),n(4,ht=!1),n(3,He=!1)}const Oe=({deviceName:Ee})=>j.includes(Ee),qe=(Ee,Ye)=>Ye.deviceName==Ee,gt=({deviceName:Ee})=>X.includes(Ee),Nt=(Ee,Ye)=>Ye.deviceName==Ee,We=()=>{ue("delayed-open")},et=()=>{ue("add-delay")},de=()=>{ue("open")},Ze=()=>{ue("cancel-opening")},Dt=()=>{n(2,Je=!0)},Xt=()=>{De("move")},St=()=>{De("personal-entry")},$t=()=>{n(3,He=!0)},Ot=()=>{De("keep-open")},Mt=()=>{De("close")},Ft=()=>{n(4,ht=!0),be("vol-report")},fn=()=>{be("hdmi1")},_n=()=>{be("hdmi2")},pn=()=>{be("off")},Ht=()=>{be("vol-up")},At=()=>{be("vol-down")},vt=()=>{be("vol-default")},Tt=()=>{be("mute")},Et=Ee=>{ft("sleep",Ee)},lt=Ee=>{Xe("sleep",Ee)},ct=Ee=>{ft("sleep",Ee)},rt=Ee=>{Ae("sleep",Ee)},Jt=(Ee,Ye)=>Ye.deviceName==Ee,Ut=Ee=>{ft("kid_sleep",Ee)},te=(Ee,Ye)=>Ye.deviceName==Ee,$e=Ee=>{Xe("kid_sleep",Ee)},nn=Ee=>{ft("kid_sleep",Ee)},bn=Ee=>{Ae("kid_sleep",Ee)},sn=()=>{ot("on")},on=()=>{ot("off")},en=()=>{ot("off-delay")},Bt=()=>{ye("enable")},Pt=()=>{ye("disable")},zt=()=>{it(),window.scrollTo({top:0,behavior:"smooth"})};return t.$$set=Ee=>{"localDevice"in Ee&&n(0,w=Ee.localDevice),"nearbyDevices"in Ee&&n(1,S=Ee.nearbyDevices)},t.$$.update=()=>{var Ee,Ye,Lt,rn,vn;t.$$.dirty[0]&1073741824&&n(11,r=(Ee=p.entryDoor)==null?void 0:Ee.counter),t.$$.dirty[0]&1073741824&&n(10,i=(Ye=p.parkingDoor)==null?void 0:Ye.keepOpenInProgress),t.$$.dirty[0]&1073741824&&n(9,s=(Lt=p.parkingDoor)==null?void 0:Lt.personalEntryInProgress),t.$$.dirty[0]&1073741824&&n(8,l=(rn=p.lights)==null?void 0:rn.labLightOffDelay),t.$$.dirty[0]&1073741824&&n(7,c=(vn=p.lights)==null?void 0:vn.labLightOffDelayStr),t.$$.dirty[0]&1073741824&&n(6,u=p.deviceSleepStarter),t.$$.dirty[0]&1073741824&&n(5,h=p.kidSleepStarter)},[w,S,Je,He,ht,h,u,c,l,s,i,r,b,m,y,k,G,X,j,q,z,be,ye,ue,De,ot,Xe,ft,Ae,it,p,Oe,qe,gt,Nt,We,et,de,Ze,Dt,Xt,St,$t,Ot,Mt,Ft,fn,_n,pn,Ht,At,vt,Tt,Et,lt,ct,rt,Jt,Ut,te,$e,nn,bn,sn,on,en,Bt,Pt,zt]}class Ma extends dt{constructor(e){super(),ut(this,e,Ia,Na,at,{localDevice:0,nearbyDevices:1},null,[-1,-1,-1])}}function Ua(t){let e,n=Tf(t[2])+"",r,i,s=t[1].replace("blinds","")+"",l,c,u,h;return{c(){e=M("button"),r=B(n),i=F(),l=B(s),e.disabled=c=!t[4],L(e,"class","svelte-1u4dkh7"),ce(e,"moving",t[3]&&t[3][mn(t[0],t[1],t[2])]&&t[3][mn(t[0],t[1],t[2])].blindsStatus=="moving"),ce(e,"present",t[3]&&t[3][mn(t[0],t[1],t[2])]&&t[3][mn(t[0],t[1],t[2])].present),ce(e,"disconnected",t[4]==!1)},m(p,b){C(p,e,b),O(e,r),O(e,i),O(e,l),u||(h=Se(e,"click",t[10]),u=!0)},p(p,[b]){b&4&&n!==(n=Tf(p[2])+"")&&Ie(r,n),b&2&&s!==(s=p[1].replace("blinds","")+"")&&Ie(l,s),b&16&&c!==(c=!p[4])&&(e.disabled=c),b&15&&ce(e,"moving",p[3]&&p[3][mn(p[0],p[1],p[2])]&&p[3][mn(p[0],p[1],p[2])].blindsStatus=="moving"),b&15&&ce(e,"present",p[3]&&p[3][mn(p[0],p[1],p[2])]&&p[3][mn(p[0],p[1],p[2])].present),b&16&&ce(e,"disconnected",p[4]==!1)},i:ge,o:ge,d(p){p&&T(e),u=!1,h()}}}function Tf(t){return t=="up"?"▲":"▼"}function mn(t,e,n){return`${t}-${e}-${n}`}function La(t,e,n){let r,i,s,{connector:l}=e,{placeId:c}=e,{blindsId:u}=e,{blindsDirection:h}=e;const{state:p,connected:b}=l;wt(t,p,S=>n(9,i=S)),wt(t,b,S=>n(4,s=S));function m(S,P){l.signal("action",{action:S,scope:"iot",payload:P})}function y(S,P,k){m("blinds",{placeId:S,blindsId:P,blindsDirection:k,blindsAction:"move"})}const w=()=>y(c,u,h);return t.$$set=S=>{"connector"in S&&n(8,l=S.connector),"placeId"in S&&n(0,c=S.placeId),"blindsId"in S&&n(1,u=S.blindsId),"blindsDirection"in S&&n(2,h=S.blindsDirection)},t.$$.update=()=>{t.$$.dirty&512&&n(3,r=i.blinds)},[c,u,h,r,s,p,b,y,l,i,w]}class mt extends dt{constructor(e){super(),ut(this,e,La,Ua,at,{connector:8,placeId:0,blindsId:1,blindsDirection:2})}}function ja(t){let e,n,r,i,s,l,c,u,h,p,b,m,y,w,S,P,k,G,X,j,q;return s=new mt({props:{placeId:"ap1",blindsId:"blinds4",blindsDirection:"up",connector:t[0]}}),c=new mt({props:{placeId:"ap1",blindsId:"blinds3",blindsDirection:"up",connector:t[0]}}),h=new mt({props:{placeId:"ap1",blindsId:"blinds2",blindsDirection:"up",connector:t[0]}}),b=new mt({props:{placeId:"ap1",blindsId:"blinds1",blindsDirection:"up",connector:t[0]}}),w=new mt({props:{placeId:"ap1",blindsId:"blinds4",blindsDirection:"down",connector:t[0]}}),P=new mt({props:{placeId:"ap1",blindsId:"blinds3",blindsDirection:"down",connector:t[0]}}),G=new mt({props:{placeId:"ap1",blindsId:"blinds2",blindsDirection:"down",connector:t[0]}}),j=new mt({props:{placeId:"ap1",blindsId:"blinds1",blindsDirection:"down",connector:t[0]}}),{c(){e=M("div"),n=M("h2"),n.textContent="Rolete",r=F(),i=M("div"),Ke(s.$$.fragment),l=F(),Ke(c.$$.fragment),u=B(` — `),Ke(h.$$.fragment),p=F(),Ke(b.$$.fragment),m=F(),y=M("div"),Ke(w.$$.fragment),S=F(),Ke(P.$$.fragment),k=B(` — @@ -70,4 +70,4 @@ * https://github.com/Starcounter-Jack/JSON-Patch * (c) 2017 Joachim Wester * MIT license - */var ai=new WeakMap,m0=function(){function t(e){this.observers=new Map,this.obj=e}return t}(),y0=function(){function t(e,n){this.callback=e,this.observer=n}return t}();function g0(t){return ai.get(t)}function x0(t,e){return t.observers.get(e)}function w0(t,e){t.observers.delete(e.callback)}function A0(t,e){e.unobserve()}function E0(t,e){var n=[],r,i=g0(t);if(!i)i=new m0(t),ai.set(t,i);else{var s=x0(i,e);r=s&&s.observer}if(r)return r;if(r={},i.value=Yt(t),e){r.callback=e,r.next=null;var l=function(){ei(r)},c=function(){clearTimeout(r.next),r.next=setTimeout(l)};typeof window<"u"&&(window.addEventListener("mouseup",c),window.addEventListener("keyup",c),window.addEventListener("mousedown",c),window.addEventListener("keydown",c),window.addEventListener("change",c))}return r.patches=n,r.object=t,r.unobserve=function(){ei(r),clearTimeout(r.next),w0(i,r),typeof window<"u"&&(window.removeEventListener("mouseup",c),window.removeEventListener("keyup",c),window.removeEventListener("mousedown",c),window.removeEventListener("keydown",c),window.removeEventListener("change",c))},i.observers.set(e,new y0(e,r)),r}function ei(t,e){e===void 0&&(e=!1);var n=ai.get(t.object);ui(n.value,t.object,t.patches,"",e),t.patches.length&&ci(n.value,t.patches);var r=t.patches;return r.length>0&&(t.patches=[],t.callback&&t.callback(r)),r}function ui(t,e,n,r,i){if(e!==t){typeof e.toJSON=="function"&&(e=e.toJSON());for(var s=Vr(e),l=Vr(t),c=!1,u=l.length-1;u>=0;u--){var h=l[u],p=t[h];if(Gr(e,h)&&!(e[h]===void 0&&p!==void 0&&Array.isArray(e)===!1)){var b=e[h];typeof p=="object"&&p!=null&&typeof b=="object"&&b!=null?ui(p,b,n,r+"/"+On(h),i):p!==b&&(i&&n.push({op:"test",path:r+"/"+On(h),value:Yt(p)}),n.push({op:"replace",path:r+"/"+On(h),value:Yt(b)}))}else Array.isArray(t)===Array.isArray(e)?(i&&n.push({op:"test",path:r+"/"+On(h),value:Yt(p)}),n.push({op:"remove",path:r+"/"+On(h)}),c=!0):(i&&n.push({op:"test",path:r,value:t}),n.push({op:"replace",path:r,value:e}))}if(!(!c&&s.length==l.length))for(var u=0;u{this.wireStateReceived=!0,this.set(n)}),this.connector.on("receive_diff",n=>{this.wireStateReceived&&(k0(this.state,n),this.announceStateChange())})}field(e){return this.connector.connectionState.get(e)}}class R0 extends qn{constructor(e){super({}),this.fields={},this.connector=e,this.connector.on("receive_state_field",({name:n,state:r})=>{this.get(n).set(r)})}get(e){return this.fields[e]||(this.fields[e]=new qn),this.fields[e]}}Vt.util=Hn;const N0=700,D0=6e4,$0=1;class P0 extends gr{constructor({endpoint:e,protocol:n,keypair:r=Bs(),rpcRequestTimeout:i,verbose:s=!1,tag:l,log:c=console.log,autoDecommission:u=!1,dummy:h}={}){super(),this.protocol=n,this.log=c;const{privateKey:p,publicKey:b}=zs(r);this.clientPrivateKey=p,this.clientPublicKey=b,this.clientPublicKeyHex=pr(b),this.rpcClient=new a0(this,i),this.endpoint=e,this.verbose=s,this.tag=l,this.autoDecommission=u,this.sentCount=0,this.receivedCount=0,this.successfulConnectsCount=0,h||(this.state=new C0(this),this.connectionState=new R0(this)),this.connected=new qn,this.delayedAdjustConnectionStatus(),s&&nt.green(this.log,`Connector ${this.endpoint} created`),this.decommissionCheckCounter=0,this.lastPongReceivedAt=Date.now(),this.on("pong",()=>{this.lastPongReceivedAt=Date.now()})}delayedAdjustConnectionStatus(){setTimeout(()=>{this.connected.get()==null&&this.connected.set(!1)},N0)}send(e){Fu({data:e,connector:this}),this.sentCount+=1}signal(e,n){this.connected.get()?this.send({signal:e,data:n}):nt.write(this.log,"Warning: trying to send signal over disconnected connector, this should be prevented by GUI")}userAction({action:e,scope:n,payload:r}){this.signal("__action",{action:e,scope:n,payload:r})}on(e,n){e=="ready"&&this.isReady()&&n(),super.on(e,n)}getSharedSecret(){return this.sharedSecret?pr(this.sharedSecret):void 0}wireReceive({jsonData:e,encryptedData:n,rawMessage:r}){Hs({jsonData:e,encryptedData:n,rawMessage:r,connector:this}),this.receivedCount+=1}field(e){return this.connectionState.get(e)}isReady(){return this.ready}closed(){return!this.transportConnected}connectStatus(e){if(e){this.sentCount=0,this.receivedCount=0,this.transportConnected=!0,this.successfulConnectsCount+=1,this.verbose&&nt.green(this.log,`✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`);const n=this.connection.websocket.__id;Ju({connector:this,afterFirstStep:({sharedSecret:i,remotePubkeyHex:s})=>{this.sharedSecret=i,this._remotePubkeyHex=s}}).then(()=>{this.connectedAt=Date.now(),this.connected.set(!0),this.ready=!0,this.emit("ready")}).catch(i=>{this.connection.websocket.__id==n&&this.connection.websocket.readyState==$0&&i.code==Qt.TIMEOUT&&(nt.write(this.log,`${this.endpoint} x Connector [ ${this.protocol} ] handshake error: "${i.message}"`),nt.write(this.log,`${this.endpoint} Connector dropping stale websocket after handshake error`),this.connection.terminate()),i.code!=Qt.TIMEOUT&&nt.write(this.log,`${this.endpoint} x Connector [ ${this.protocol} ] on:ready error: "${i.stack}" — (will not try to reconnect, fix the error and reload this gui)`)})}else{let n;this.transportConnected&&(n=!0),this.transportConnected==null&&nt.write(this.log,`${this.endpoint} Connector was not able to connect at first try`),this.transportConnected=!1,this.ready=!1,this.sharedSecret=void 0,delete this.connectedAt,n&&(this.emit("disconnect"),e==null&&this.delayedAdjustConnectionStatus(),this.connected.set(e))}}checkForDecommission(){this.autoDecommission&&(this.decommissionCheckRequestedAt&&Date.now()-this.decommissionCheckRequestedAt>3e3&&(this.decommissionCheckCounter=0),this.decommissionCheckRequestedAt=Date.now(),this.decommissionCheckCounter+=1,this.decommissionCheckCounter>12&&Date.now()-this.lastPongReceivedAt>D0&&(nt.write(this.log,`Decommissioning connector ${this.endpoint} (long inactive)`),this.decommission(),this.emit("decommission")))}decommission(){this.decommissioned=!0}remoteObject(e){return{call:(n,r=[])=>this.rpcClient.remoteObject(e).call(n,Du(r))}}attachObject(e,n){new u0({serversideChannel:this,serverMethods:n,methodPrefix:e})}clientPubkey(){return this.clientPublicKeyHex}remotePubkeyHex(){return this._remotePubkeyHex}remoteAddress(){return this.endpoint}}const Wf=typeof window<"u";function I0({endpoint:t,host:e,port:n}){if(Wf&&t&&t.startsWith("/")&&(t=`${window.location.protocol.includes("s")?"wss":"ws"}://${window.location.host}${t}`),!t)if(Wf){e=e||window.location.hostname;const r=window.location.protocol.includes("s")?"wss":"ws";t=`${r}://${e}`,r=="wss"?t=`${r}://${e}/ws`:n?t=`${t}:${n}`:window.location.port&&(t=`${t}:${window.location.port}`)}else{if(!n)throw new Error(`Connectome determineEndpoint: No websocket port provided for ${e}`);t=`ws://${e||"localhost"}:${n}`}return t}const un=typeof window<"u",M0=0,U0=1,L0=1e3,j0=3,B0=5;function z0({endpoint:t,host:e,port:n,protocol:r,keypair:i,remotePubkey:s,rpcRequestTimeout:l,autoDecommission:c,log:u,verbose:h,tag:p,dummy:b},{WebSocket:m}){t=I0({endpoint:t,host:e,port:n});const y=new P0({endpoint:t,protocol:r,rpcRequestTimeout:l,keypair:i,verbose:h,tag:p,log:u,autoDecommission:c,dummy:b}),w=()=>{q0({connector:y,endpoint:t},{WebSocket:m,reconnect:w,log:u,verbose:h})};y.connection={terminate(){this.websocket._removeAllCallbacks(),this.websocket.close(),y.connectStatus(!1),w()},endpoint:t,checkTicker:0};const S=()=>{y.decommissioned||(K0({connector:y,reconnect:w,log:u}),setTimeout(S,L0))};return setTimeout(S,10),y}function K0({connector:t,reconnect:e,log:n}){const r=t.connection;if(F0(r)||t.decommissioned){t.decommissioned?(nt.yellow(n,`${t.endpoint} Connection decommisioned, closing websocket ${r.websocket.__id}, will not retry again `),eo(t)):(t.emit("inactive_connection"),nt.yellow(n,`${t.endpoint} ✖ Terminated inactive connection`)),r.terminate();return}to(r)?r.websocket.send("ping"):(t.connected==null&&(nt.write(n,`${t.endpoint} Setting connector status to FALSE because connector.connected is undefined`),t.connectStatus(!1)),e()),r.checkTicker+=1}function q0({connector:t,endpoint:e},{WebSocket:n,reconnect:r,log:i,verbose:s}){const l=t.connection;if(t.checkForDecommission(),t.decommissioned){eo(t);return}if(l.currentlyTryingWS&&l.currentlyTryingWS.readyState==M0){if(l.currentlyTryingWS._waitForConnectCounter{});const u=()=>{t.decommissioned||((s||un)&&nt.write(i,`${e} Websocket open`),l.currentlyTryingWS=null,l.checkTicker=0,Y0({ws:c,connector:t,openCallback:u,reconnect:r},{log:i,verbose:s}),l.websocket=c,t.connectStatus(!0))};c._removeAllCallbacks=()=>{c.removeEventListener("open",u)},un?c.addEventListener("open",u):c.on("open",u)}function Y0({ws:t,connector:e,openCallback:n,reconnect:r},{log:i,verbose:s}){const l=e.connection,c=p=>{const b=`${e.endpoint} Websocket error`;console.log(b),console.log(p)},u=()=>{if(nt.write(i,`${e.endpoint} ✖ Connection closed`),e.decommissioned){e.connectStatus(!1);return}e.connectStatus(void 0),r()},h=p=>{if(e.decommissioned)return;l.checkTicker=0;const b=un?p.data:p;if(b=="pong"){e.emit("pong");return}let m;try{m=JSON.parse(b)}catch{}if(m)e.wireReceive({jsonData:m,rawMessage:b});else{const y=un?new Uint8Array(b):b;e.wireReceive({encryptedData:y})}};t._removeAllCallbacks=()=>{t.removeEventListener("error",c),t.removeEventListener("close",u),t.removeEventListener("message",h),t.removeEventListener("open",n)},un?(t.addEventListener("error",c),t.addEventListener("close",u),t.addEventListener("message",h)):(t.on("error",c),t.on("close",u),t.on("message",h))}function eo(t){const e=t.connection;e.currentlyTryingWS&&(e.currentlyTryingWS._removeAllCallbacks(),e.currentlyTryingWS.close(),e.currentlyTryingWS=null),e.ws&&(e.ws._removeAllCallbacks(),e.ws.close(),e.ws=null),t.connectStatus(!1)}function to(t){return t.websocket&&t.websocket.readyState==U0}function F0(t){return to(t)&&t.checkTicker>j0}function H0(t){return t.log=t.log||console.log,z0(t,{WebSocket})}class J0{constructor({mcs:e,foreground:n,connectToDeviceKey:r}){this.mcs=e,this.foreground=n,this.connectToDeviceKey=r}createConnector({host:e,autoDecommission:n=!1}){const{port:r,protocol:i,rpcRequestTimeout:s,log:l,verbose:c,keypair:u}=this.mcs;return H0({host:e,port:r,protocol:i,keypair:u,rpcRequestTimeout:s,autoDecommission:n,log:l,verbose:c})}getDeviceKey(e){var n;return(n=e==null?void 0:e.device)==null?void 0:n.deviceKey}connectThisDevice({host:e}){const n=this.createConnector({host:e});let r=!1;return n.state.subscribe(i=>{var l;i.nearbyDevices||(i.nearbyDevices=[]),i.notifications||(i.notifications=[]);const s=this.getDeviceKey(i);if(s){r||(n.on("pong",()=>{this.mcs.emit("pong",{deviceKey:s})}),r=!0),this.thisDeviceAlreadySetup||(this.mcs.set({activeDeviceKey:s}),this.initNewConnector({deviceKey:s,connector:n}));const c=this.connectToDeviceKey&&this.connectToDeviceKey!=s;if(!c&&this.mcs.activeDeviceKey()==s){const u=(l=i.device)==null?void 0:l.deviceName;this.foreground.set(i,{optimisticDeviceName:u})}this.foreground.setSpecial(i),this.thisDeviceAlreadySetup||(c&&(this.mcs.switch({deviceKey:this.connectToDeviceKey}),delete this.connectToDeviceKey),this.thisDeviceAlreadySetup=!0)}}),n}connectOtherDevice({host:e,deviceKey:n}){if(!this.mcs.connectors[n]){const r=this.createConnector({host:e,autoDecommission:!0});r.on("decommission",()=>{delete this.mcs.connectors[n],r.__removeListeners&&r.__removeListeners()});const i=()=>{this.mcs.emit("pong",{deviceKey:n})};r.on("pong",i),this.initNewConnector({deviceKey:n,connector:r});const s=r.state.subscribe(l=>{if(this.mcs.activeDeviceKey()==n){const c=l.device?l.device.deviceName:null;this.foreground.set(l,{optimisticDeviceName:c})}});r.__removeListeners=()=>{r.off("pong",i),s()}}return this.mcs.connectors[n]}initNewConnector({deviceKey:e,connector:n}){this.mcs.connectors[e]=n,this.setConnectedStore({deviceKey:e,connector:n})}setConnectedStore({deviceKey:e,connector:n}){n.connected.subscribe(r=>{this.mcs.activeDeviceKey()==e&&this.mcs.connected.set(r)})}}function no(t,e=0,n={}){const r=["day","h","min","s"],i=[24,60,60,1e3];if(e==r.length)return n.ms=t,n;e==0&&(n.totalSeconds=t/1e3);const s=i.slice(e).reduce((l,c)=>l*c,1);return n[r[e]]=Math.floor(t/s),no(t%s,e+1,n)}function W0(t){const e=["day","h","min","s"];let n="";for(const r of e)t[r]>0&&(r!="s"||r=="s"&&t.totalSeconds<60)&&(n=`${n} ${t[r]} ${r}`);return n.trim()}function ro(t){if(t){const e=Date.now(),n=3e3;return t.filter(r=>e({...r,relativeTimeAdded:e-r.addedAtc.deviceKey==n&&!c.thisDevice);if(l){const{deviceKey:c,deviceName:u,ip:h}=l;this.switch({host:h,deviceKey:c,deviceName:u})}else this.emit("connect_to_device_key_failed"),this.switchState(i.device)}}}const V0=500;class Q0 extends Ou{constructor({host:e,port:n,protocol:r,keypair:i=Bs(),connectToDeviceKey:s,rpcRequestTimeout:l=3e3,log:c,verbose:u}){super();const h=["time","environment","nearbyDevices","nearbySensors","notifications"],{publicKey:p,privateKey:b}=zs(i);this.publicKey=p,this.privateKey=b,this.keypair=i,this.port=n,this.protocol=r,this.log=c,this.rpcRequestTimeout=l,this.verbose=u,this.connectors={},this.connected=new qn;const m=new Z0({mcs:this,thisDeviceStateKeys:h}),y=new J0({mcs:this,foreground:m,connectToDeviceKey:s});this.connectDevice=y,this.switchDevice=new G0({mcs:this,connectDevice:y,foreground:m}),this.switchDevice.on("connect_to_device_key_failed",()=>{this.emit("connect_to_device_key_failed")}),this.localConnector=y.connectThisDevice({host:e}),this._notificationsExpireAndCalculateRelativeTime()}_notificationsExpireAndCalculateRelativeTime(){const{notifications:e}=this.get();this.setMerge({notifications:ro(e)}),setTimeout(()=>{this._notificationsExpireAndCalculateRelativeTime()},V0)}signal(e,n){this.activeConnector()?this.activeConnector().signal(e,n):console.log(`MCS: Error emitting remote signal ${e} / ${n}. Debug info: activeDeviceKey=${this.activeDeviceKey()}`)}signalLocalDevice(e,n){this.localConnector.signal(e,n)}remoteObject(e){if(this.activeConnector())return this.activeConnector().remoteObject(e);console.log(`Error obtaining remote object ${e}. Debug info: activeDeviceKey=${this.activeDeviceKey()}`)}preconnect({host:e,deviceKey:n,thisDevice:r}){return r?this.localConnector:this.connectDevice.connectOtherDevice({host:e,deviceKey:n})}switch({host:e,deviceKey:n,deviceName:r}){this.switchDevice.switch({host:e,deviceKey:n,deviceName:r})}activeConnector(){if(this.activeDeviceKey())return this.connectors[this.activeDeviceKey()]}activeDeviceKey(){return this.get().activeDeviceKey}}const X0=7780,e1="dmt/gui",io=localStorage.getItem("current_device_key");console.log(`connectToDeviceKey: ${io}`);const fo=new Q0({port:X0,protocol:e1,connectToDeviceKey:io,log:Wt.log});fo.on("connect_to_device_key_failed",()=>{console.log("connect_to_device_key_failed FAILED"),localStorage.removeItem("current_device_key")});window.onerror=ko(hs);new xu({target:document.body,props:{store:fo,log:Wt.log}}); + */var ai=new WeakMap,m0=function(){function t(e){this.observers=new Map,this.obj=e}return t}(),y0=function(){function t(e,n){this.callback=e,this.observer=n}return t}();function g0(t){return ai.get(t)}function x0(t,e){return t.observers.get(e)}function w0(t,e){t.observers.delete(e.callback)}function A0(t,e){e.unobserve()}function E0(t,e){var n=[],r,i=g0(t);if(!i)i=new m0(t),ai.set(t,i);else{var s=x0(i,e);r=s&&s.observer}if(r)return r;if(r={},i.value=Yt(t),e){r.callback=e,r.next=null;var l=function(){ei(r)},c=function(){clearTimeout(r.next),r.next=setTimeout(l)};typeof window<"u"&&(window.addEventListener("mouseup",c),window.addEventListener("keyup",c),window.addEventListener("mousedown",c),window.addEventListener("keydown",c),window.addEventListener("change",c))}return r.patches=n,r.object=t,r.unobserve=function(){ei(r),clearTimeout(r.next),w0(i,r),typeof window<"u"&&(window.removeEventListener("mouseup",c),window.removeEventListener("keyup",c),window.removeEventListener("mousedown",c),window.removeEventListener("keydown",c),window.removeEventListener("change",c))},i.observers.set(e,new y0(e,r)),r}function ei(t,e){e===void 0&&(e=!1);var n=ai.get(t.object);ui(n.value,t.object,t.patches,"",e),t.patches.length&&ci(n.value,t.patches);var r=t.patches;return r.length>0&&(t.patches=[],t.callback&&t.callback(r)),r}function ui(t,e,n,r,i){if(e!==t){typeof e.toJSON=="function"&&(e=e.toJSON());for(var s=Vr(e),l=Vr(t),c=!1,u=l.length-1;u>=0;u--){var h=l[u],p=t[h];if(Gr(e,h)&&!(e[h]===void 0&&p!==void 0&&Array.isArray(e)===!1)){var b=e[h];typeof p=="object"&&p!=null&&typeof b=="object"&&b!=null?ui(p,b,n,r+"/"+On(h),i):p!==b&&(i&&n.push({op:"test",path:r+"/"+On(h),value:Yt(p)}),n.push({op:"replace",path:r+"/"+On(h),value:Yt(b)}))}else Array.isArray(t)===Array.isArray(e)?(i&&n.push({op:"test",path:r+"/"+On(h),value:Yt(p)}),n.push({op:"remove",path:r+"/"+On(h)}),c=!0):(i&&n.push({op:"test",path:r,value:t}),n.push({op:"replace",path:r,value:e}))}if(!(!c&&s.length==l.length))for(var u=0;u{this.wireStateReceived=!0,this.set(n)}),this.connector.on("receive_diff",n=>{this.wireStateReceived&&(k0(this.state,n),this.announceStateChange())})}field(e){return this.connector.connectionState.get(e)}}class R0 extends qn{constructor(e){super({}),this.fields={},this.connector=e,this.connector.on("receive_state_field",({name:n,state:r})=>{this.get(n).set(r)})}get(e){return this.fields[e]||(this.fields[e]=new qn),this.fields[e]}}Vt.util=Hn;const N0=700,D0=6e4,$0=1;class P0 extends gr{constructor({endpoint:e,protocol:n,keypair:r=Bs(),rpcRequestTimeout:i,verbose:s=!1,tag:l,log:c=console.log,autoDecommission:u=!1,dummy:h}={}){super(),this.protocol=n,this.log=c;const{privateKey:p,publicKey:b}=zs(r);this.clientPrivateKey=p,this.clientPublicKey=b,this.clientPublicKeyHex=pr(b),this.rpcClient=new a0(this,i),this.endpoint=e,this.verbose=s,this.tag=l,this.autoDecommission=u,this.sentCount=0,this.receivedCount=0,this.successfulConnectsCount=0,h||(this.state=new C0(this),this.connectionState=new R0(this)),this.connected=new qn,this.delayedAdjustConnectionStatus(),s&&nt.green(this.log,`Connector ${this.endpoint} created`),this.decommissionCheckCounter=0,this.lastPongReceivedAt=Date.now(),this.on("pong",()=>{this.lastPongReceivedAt=Date.now()})}delayedAdjustConnectionStatus(){setTimeout(()=>{this.connected.get()==null&&this.connected.set(!1)},N0)}send(e){Fu({data:e,connector:this}),this.sentCount+=1}signal(e,n){this.connected.get()?this.send({signal:e,data:n}):nt.write(this.log,"Warning: trying to send signal over disconnected connector, this should be prevented by GUI")}userAction({action:e,scope:n,payload:r}){this.signal("__action",{action:e,scope:n,payload:r})}on(e,n){e=="ready"&&this.isReady()&&n(),super.on(e,n)}getSharedSecret(){return this.sharedSecret?pr(this.sharedSecret):void 0}wireReceive({jsonData:e,encryptedData:n,rawMessage:r}){Hs({jsonData:e,encryptedData:n,rawMessage:r,connector:this}),this.receivedCount+=1}field(e){return this.connectionState.get(e)}isReady(){return this.ready}closed(){return!this.transportConnected}connectStatus(e){if(e){this.sentCount=0,this.receivedCount=0,this.transportConnected=!0,this.successfulConnectsCount+=1,this.verbose&&nt.green(this.log,`✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`);const n=this.connection.websocket.__id;Ju({connector:this,afterFirstStep:({sharedSecret:i,remotePubkeyHex:s})=>{this.sharedSecret=i,this._remotePubkeyHex=s}}).then(()=>{this.connectedAt=Date.now(),this.connected.set(!0),this.ready=!0,this.emit("ready")}).catch(i=>{this.connection.websocket.__id==n&&this.connection.websocket.readyState==$0&&i.code==Qt.TIMEOUT&&(nt.write(this.log,`${this.endpoint} x Connector [ ${this.protocol} ] handshake error: "${i.message}"`),nt.write(this.log,`${this.endpoint} Connector dropping stale websocket after handshake error`),this.connection.terminate()),i.code!=Qt.TIMEOUT&&nt.write(this.log,`${this.endpoint} x Connector [ ${this.protocol} ] on:ready error: "${i.stack}" — (will not try to reconnect, fix the error and reload this gui)`)})}else{let n;this.transportConnected&&(n=!0),this.transportConnected==null&&nt.write(this.log,`${this.endpoint} Connector was not able to connect at first try`),this.transportConnected=!1,this.ready=!1,this.sharedSecret=void 0,delete this.connectedAt,n&&(this.emit("disconnect"),e==null&&this.delayedAdjustConnectionStatus(),this.connected.set(e))}}checkForDecommission(){this.autoDecommission&&(this.decommissionCheckRequestedAt&&Date.now()-this.decommissionCheckRequestedAt>3e3&&(this.decommissionCheckCounter=0),this.decommissionCheckRequestedAt=Date.now(),this.decommissionCheckCounter+=1,this.decommissionCheckCounter>12&&Date.now()-this.lastPongReceivedAt>D0&&(nt.write(this.log,`Decommissioning connector ${this.endpoint} (long inactive)`),this.decommission(),this.emit("decommission")))}decommission(){this.decommissioned=!0}remoteObject(e){return{call:(n,r=[])=>this.rpcClient.remoteObject(e).call(n,Du(r))}}attachObject(e,n){new u0({serversideChannel:this,serverMethods:n,methodPrefix:e})}clientPubkey(){return this.clientPublicKeyHex}remotePubkeyHex(){return this._remotePubkeyHex}remoteAddress(){return this.endpoint}}const Wf=typeof window<"u";function I0({endpoint:t,host:e,port:n}){if(Wf&&t&&t.startsWith("/")&&(t=`${window.location.protocol.includes("s")?"wss":"ws"}://${window.location.host}${t}`),!t)if(Wf){e=e||window.location.hostname;const r=window.location.protocol.includes("s")?"wss":"ws";t=`${r}://${e}`,r=="wss"?t=`${r}://${e}/ws`:n?t=`${t}:${n}`:window.location.port&&(t=`${t}:${window.location.port}`)}else{if(!n)throw new Error(`Connectome determineEndpoint: No websocket port provided for ${e}`);t=`ws://${e||"localhost"}:${n}`}return t}const un=typeof window<"u",M0=0,U0=1,L0=1e3,j0=3,B0=5;function z0({endpoint:t,host:e,port:n,protocol:r,keypair:i,remotePubkey:s,rpcRequestTimeout:l,autoDecommission:c,log:u,verbose:h,tag:p,dummy:b},{WebSocket:m}){t=I0({endpoint:t,host:e,port:n});const y=new P0({endpoint:t,protocol:r,rpcRequestTimeout:l,keypair:i,verbose:h,tag:p,log:u,autoDecommission:c,dummy:b}),w=()=>{q0({connector:y,endpoint:t},{WebSocket:m,reconnect:w,log:u,verbose:h})};y.connection={terminate(){this.websocket._removeAllCallbacks(),this.websocket.close(),y.connectStatus(!1),w()},endpoint:t,checkTicker:0};const S=()=>{y.decommissioned||(K0({connector:y,reconnect:w,log:u}),setTimeout(S,L0))};return setTimeout(S,10),y}function K0({connector:t,reconnect:e,log:n}){const r=t.connection;if(F0(r)||t.decommissioned){t.decommissioned?(nt.yellow(n,`${t.endpoint} Connection decommisioned, closing websocket ${r.websocket.__id}, will not retry again `),eo(t)):(t.emit("inactive_connection"),nt.yellow(n,`${t.endpoint} ✖ Terminated inactive connection`)),r.terminate();return}to(r)?r.websocket.send("ping"):(t.connected==null&&(nt.write(n,`${t.endpoint} Setting connector status to FALSE because connector.connected is undefined`),t.connectStatus(!1)),e()),r.checkTicker+=1}function q0({connector:t,endpoint:e},{WebSocket:n,reconnect:r,log:i,verbose:s}){const l=t.connection;if(t.checkForDecommission(),t.decommissioned){eo(t);return}if(l.currentlyTryingWS&&l.currentlyTryingWS.readyState==M0){if(l.currentlyTryingWS._waitForConnectCounter{});const u=()=>{t.decommissioned||((s||un)&&nt.write(i,`${e} Websocket open`),l.currentlyTryingWS=null,l.checkTicker=0,Y0({ws:c,connector:t,openCallback:u,reconnect:r},{log:i,verbose:s}),l.websocket=c,t.connectStatus(!0))};c._removeAllCallbacks=()=>{c.removeEventListener("open",u)},un?c.addEventListener("open",u):c.on("open",u)}function Y0({ws:t,connector:e,openCallback:n,reconnect:r},{log:i,verbose:s}){const l=e.connection,c=p=>{const b=`${e.endpoint} Websocket error`;console.log(b),console.log(p)},u=()=>{if(nt.write(i,`${e.endpoint} ✖ Connection [ ${e.protocol} ] closed`),e.decommissioned){e.connectStatus(!1);return}e.connectStatus(void 0),r()},h=p=>{if(e.decommissioned)return;l.checkTicker=0;const b=un?p.data:p;if(b=="pong"){e.emit("pong");return}let m;try{m=JSON.parse(b)}catch{}if(m)e.wireReceive({jsonData:m,rawMessage:b});else{const y=un?new Uint8Array(b):b;e.wireReceive({encryptedData:y})}};t._removeAllCallbacks=()=>{t.removeEventListener("error",c),t.removeEventListener("close",u),t.removeEventListener("message",h),t.removeEventListener("open",n)},un?(t.addEventListener("error",c),t.addEventListener("close",u),t.addEventListener("message",h)):(t.on("error",c),t.on("close",u),t.on("message",h))}function eo(t){const e=t.connection;e.currentlyTryingWS&&(e.currentlyTryingWS._removeAllCallbacks(),e.currentlyTryingWS.close(),e.currentlyTryingWS=null),e.ws&&(e.ws._removeAllCallbacks(),e.ws.close(),e.ws=null),t.connectStatus(!1)}function to(t){return t.websocket&&t.websocket.readyState==U0}function F0(t){return to(t)&&t.checkTicker>j0}function H0(t){return t.log=t.log||console.log,z0(t,{WebSocket})}class J0{constructor({mcs:e,foreground:n,connectToDeviceKey:r}){this.mcs=e,this.foreground=n,this.connectToDeviceKey=r}createConnector({host:e,autoDecommission:n=!1}){const{port:r,protocol:i,rpcRequestTimeout:s,log:l,verbose:c,keypair:u}=this.mcs;return H0({host:e,port:r,protocol:i,keypair:u,rpcRequestTimeout:s,autoDecommission:n,log:l,verbose:c})}getDeviceKey(e){var n;return(n=e==null?void 0:e.device)==null?void 0:n.deviceKey}connectThisDevice({host:e}){const n=this.createConnector({host:e});let r=!1;return n.state.subscribe(i=>{var l;i.nearbyDevices||(i.nearbyDevices=[]),i.notifications||(i.notifications=[]);const s=this.getDeviceKey(i);if(s){r||(n.on("pong",()=>{this.mcs.emit("pong",{deviceKey:s})}),r=!0),this.thisDeviceAlreadySetup||(this.mcs.set({activeDeviceKey:s}),this.initNewConnector({deviceKey:s,connector:n}));const c=this.connectToDeviceKey&&this.connectToDeviceKey!=s;if(!c&&this.mcs.activeDeviceKey()==s){const u=(l=i.device)==null?void 0:l.deviceName;this.foreground.set(i,{optimisticDeviceName:u})}this.foreground.setSpecial(i),this.thisDeviceAlreadySetup||(c&&(this.mcs.switch({deviceKey:this.connectToDeviceKey}),delete this.connectToDeviceKey),this.thisDeviceAlreadySetup=!0)}}),n}connectOtherDevice({host:e,deviceKey:n}){if(!this.mcs.connectors[n]){const r=this.createConnector({host:e,autoDecommission:!0});r.on("decommission",()=>{delete this.mcs.connectors[n],r.__removeListeners&&r.__removeListeners()});const i=()=>{this.mcs.emit("pong",{deviceKey:n})};r.on("pong",i),this.initNewConnector({deviceKey:n,connector:r});const s=r.state.subscribe(l=>{if(this.mcs.activeDeviceKey()==n){const c=l.device?l.device.deviceName:null;this.foreground.set(l,{optimisticDeviceName:c})}});r.__removeListeners=()=>{r.off("pong",i),s()}}return this.mcs.connectors[n]}initNewConnector({deviceKey:e,connector:n}){this.mcs.connectors[e]=n,this.setConnectedStore({deviceKey:e,connector:n})}setConnectedStore({deviceKey:e,connector:n}){n.connected.subscribe(r=>{this.mcs.activeDeviceKey()==e&&this.mcs.connected.set(r)})}}function no(t,e=0,n={}){const r=["day","h","min","s"],i=[24,60,60,1e3];if(e==r.length)return n.ms=t,n;e==0&&(n.totalSeconds=t/1e3);const s=i.slice(e).reduce((l,c)=>l*c,1);return n[r[e]]=Math.floor(t/s),no(t%s,e+1,n)}function W0(t){const e=["day","h","min","s"];let n="";for(const r of e)t[r]>0&&(r!="s"||r=="s"&&t.totalSeconds<60)&&(n=`${n} ${t[r]} ${r}`);return n.trim()}function ro(t){if(t){const e=Date.now(),n=3e3;return t.filter(r=>e({...r,relativeTimeAdded:e-r.addedAtc.deviceKey==n&&!c.thisDevice);if(l){const{deviceKey:c,deviceName:u,ip:h}=l;this.switch({host:h,deviceKey:c,deviceName:u})}else this.emit("connect_to_device_key_failed"),this.switchState(i.device)}}}const V0=500;class Q0 extends Ou{constructor({host:e,port:n,protocol:r,keypair:i=Bs(),connectToDeviceKey:s,rpcRequestTimeout:l=3e3,log:c,verbose:u}){super();const h=["time","environment","nearbyDevices","nearbySensors","notifications"],{publicKey:p,privateKey:b}=zs(i);this.publicKey=p,this.privateKey=b,this.keypair=i,this.port=n,this.protocol=r,this.log=c,this.rpcRequestTimeout=l,this.verbose=u,this.connectors={},this.connected=new qn;const m=new Z0({mcs:this,thisDeviceStateKeys:h}),y=new J0({mcs:this,foreground:m,connectToDeviceKey:s});this.connectDevice=y,this.switchDevice=new G0({mcs:this,connectDevice:y,foreground:m}),this.switchDevice.on("connect_to_device_key_failed",()=>{this.emit("connect_to_device_key_failed")}),this.localConnector=y.connectThisDevice({host:e}),this._notificationsExpireAndCalculateRelativeTime()}_notificationsExpireAndCalculateRelativeTime(){const{notifications:e}=this.get();this.setMerge({notifications:ro(e)}),setTimeout(()=>{this._notificationsExpireAndCalculateRelativeTime()},V0)}signal(e,n){this.activeConnector()?this.activeConnector().signal(e,n):console.log(`MCS: Error emitting remote signal ${e} / ${n}. Debug info: activeDeviceKey=${this.activeDeviceKey()}`)}signalLocalDevice(e,n){this.localConnector.signal(e,n)}remoteObject(e){if(this.activeConnector())return this.activeConnector().remoteObject(e);console.log(`Error obtaining remote object ${e}. Debug info: activeDeviceKey=${this.activeDeviceKey()}`)}preconnect({host:e,deviceKey:n,thisDevice:r}){return r?this.localConnector:this.connectDevice.connectOtherDevice({host:e,deviceKey:n})}switch({host:e,deviceKey:n,deviceName:r}){this.switchDevice.switch({host:e,deviceKey:n,deviceName:r})}activeConnector(){if(this.activeDeviceKey())return this.connectors[this.activeDeviceKey()]}activeDeviceKey(){return this.get().activeDeviceKey}}const X0=7780,e1="dmt/gui",io=localStorage.getItem("current_device_key");console.log(`connectToDeviceKey: ${io}`);const fo=new Q0({port:X0,protocol:e1,connectToDeviceKey:io,log:Wt.log});fo.on("connect_to_device_key_failed",()=>{console.log("connect_to_device_key_failed FAILED"),localStorage.removeItem("current_device_key")});window.onerror=ko(hs);new xu({target:document.body,props:{store:fo,log:Wt.log}}); diff --git a/apps/dmt-mobile/index.html b/apps/dmt-mobile/index.html index a76ed5cab..55f9f553d 100644 --- a/apps/dmt-mobile/index.html +++ b/apps/dmt-mobile/index.html @@ -10,7 +10,7 @@ DMT - + diff --git a/apps/dmt-search/dmt/connectome-next/index.js b/apps/dmt-search/dmt/connectome-next/index.js new file mode 100644 index 000000000..85cc468d2 --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/index.js @@ -0,0 +1,4 @@ +import contentServer from './lib/fileTransport/contentServer/contentServer.js'; +import * as fiberHandle from './lib/fileTransport/fiberHandle/fiberHandle.js'; + +export { contentServer, fiberHandle }; diff --git a/apps/dmt-search/dmt/connectome-next/lib/fileTransport/contentServer/checkPermission.js b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/contentServer/checkPermission.js new file mode 100644 index 000000000..e6cb7c80f --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/contentServer/checkPermission.js @@ -0,0 +1,15 @@ + +import { dmtContent, scan } from 'dmt/common'; + +let permittedPaths; + +export default function checkPermission({ directory }) { + if (!permittedPaths) { + // don't load this on top because it can crash the process before logger is ready! + permittedPaths = dmtContent.defaultContentPaths().map(path => scan.absolutizePath(path)); + } + // we check case sensitive ... there may be issues on macOS because there directories ./A and ./a are the same + // make sure that on macOS you specify directory in your content.def exactly as it is on the filesystem + // in linux you are forced to do this anyway by default (there ~/a and ~/A are different directories) + return permittedPaths.find(path => directory.startsWith(path)); +} diff --git a/apps/dmt-search/dmt/connectome-next/lib/fileTransport/contentServer/contentServer--full--unused.js b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/contentServer/contentServer--full--unused.js new file mode 100644 index 000000000..e345b745b --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/contentServer/contentServer--full--unused.js @@ -0,0 +1,263 @@ +import { decode } from '../fiberHandle/encodePath.js'; + +// TODO -- implement backpressure control, read about this: +// https://nodejs.org/es/docs/guides/backpressuring-in-streams/ +// https://nodejs.org/api/stream.html#stream_stream + +// TODO: refactor this, implement DataSource -- ? +// use this abstraction when streaming search results as well ... + +function log(...args) { + console.log(...args); +} + +const sha256 = (crypto, x) => crypto.createHash('sha256').update(x, 'utf8').digest('hex'); + +// function getSHA256Function() { +// return new Promise((success, reject) => { +// import('crypto').then(crypto => { +// const sha256 = x => +// crypto +// .createHash('sha256') +// .update(x, 'utf8') +// .digest('hex'); + +// success(sha256); +// }); +// }); +// } + +function fileNotFound({ providerAddress, fileName, res, host }) { + console.log(`File not found: ${providerAddress} -- ${fileName}`); + // TODO!! won't work on localhost!! /home ... ?q ... is wrong! + let pre = ''; + if (host.startsWith('localhost')) { + pre = 'apps/search/'; + } + + res.redirect(`/${pre}?q=${fileName}&error=file_not_found`); // TODO uri encode fileName ! + //res.status(404).send(`File not found -- ${fileName}`); +} + +// source: https://github.com/archiverjs/node-archiver/blob/master/examples/express.js +function contentServer({ app, connectorPool, defaultPort, emitter }) { + log('Starting content server ...'); + + if (!defaultPort) { + throw new Error('Must provide default fiber port for content server ...'); + } + + import('crypto').then(crypto => { + import('fs').then(fs => { + import('path').then(path => { + //getSHA256Function().then(sha256 => { + app.use('/file', (req, res) => { + // if we tried fetching the content too early, should try again .... + // if (!connector.isConnected()) { + // res.end(); + // return; + // } + + const { place } = req.query; + + const { host } = req.headers; + + log(`Received content request ${place}`); + + if (place && place.includes('-')) { + const [providerAddress, _directory] = place.split('-'); + const directory = decode(_directory); + const fileName = decodeURIComponent(req.path.slice(1)); + const filePath = path.join(directory, fileName); + + if (emitter) { + // for Swarm searches we don't have this yet.... + emitter.emit('file_request', { providerAddress, filePath, host }); + } + + //log(`FILEPATH: ${filePath}`); + + // LOCAL FILE + if (providerAddress == 'localhost') { + if (fs.existsSync(filePath)) { + res.sendFile(filePath); + } else { + fileNotFound({ providerAddress, fileName, res, host }); // will this work? test + } + + return; + } + + // FILE COMING OVER ENCRYPTED FIBER + + res.status(404).send('This feature is on hold -- streaming files over encrypted fibers'); + return; + + const sessionId = sha256(crypto, Math.random().toString()); + + let ip; + let port; + + if (providerAddress.includes(':')) { + const [_ip, _port] = providerAddress.split(':'); + ip = _ip; + port = _port; + } else { + ip = providerAddress; + port = defaultPort; + } + + connectorPool + .getConnector({ address: ip, port }) + .then(connector => { + //console.log(`GOT CONNECTOR, state: ${connector.isConnected()}`); + + // prepare ws data streaming handlers + const context = { sessionId, res, connector }; + + connector.on('file_not_found', ({ sessionId }) => { + if (context.sessionId == sessionId) { + // ok? + fileNotFound({ providerAddress, fileName, res, host }); + } + }); + + // this will attach handlers multiple times!! + // check if handlers already attached!! + // we remove lingering connections but sitll, maybe it would be useful + // TODO !! + + //if(!connector.contentServerHandlersAttached) { + + const binaryStartCallback = handleBinaryStart.bind(context); + connector.on('binary_start', binaryStartCallback); + + const binaryDataCallback = handleBinaryData.bind(context); + connector.on('binary_data', binaryDataCallback); + + const binaryEndCallback = handleBinaryEnd.bind(context); + connector.on('binary_end', binaryEndCallback); + + const expandedContext = Object.assign(context, { + attachedCallbacks: { start: binaryStartCallback, data: binaryDataCallback, end: binaryEndCallback } + }); + + //const filePath = '/home/eclipse/.dmt/etc/sounds/soundtest/music.mp3'; + connector.send({ tag: 'request_file', filePath, sessionId }); + + // const msg = { action: 'request', namespace: 'content', payload: { sessionId, filePath, requestHandle: id } }; + + // connector.send(msg); // actually initiate streaming, binary data will arrive to the handleBinaryData handler + + //dropLingeringConnection.call(expandedContext); + + // TODO!! IMPLEMENT FOR TEST::: send "request_next_chunk over the wire" ... to let the server know it can send the next chunk into the connector + // + res.once('drain', () => { + log('DRAIN!!!'); + //wait + // file.on('readable', write); + // write(); + }); + + setTimeout(dropLingeringConnection.bind(expandedContext), 60 * 1000); // cancel any connection that is open for more than a minute (really extreme case but we do it to clean things up) + // this should never be required except if our binary reader didn't return all the data in this time for some reason (error, really slow connection, really big file....) + + log(`Fiber-Content /get handler with SID=${sessionId} finished, fileName=${fileName}.`); + }) + .catch(e => { + res.status(503).send(e.message); + }); + + //res.send(`${providerAddress} / ${filePath}`); + } else { + res.status(404).send('Wrong file reference format, should be [ip]-[encodedRemoteDir]'); + } + }); + }); + }); + }); +} + +function dropLingeringConnection() { + // this == expandedContext + + if (!this.finished) { + log(`Dropping lingering connection: ${this.sessionId}`); + removeListeners(this); + this.res.end(); + } +} + +function handleBinaryStart({ mimeType, fileName, contentLength, sessionId }) { + //log.yellow(`BRISI --- Growin ? Fixed... REMOVE THIS LOG LINE --- ${this.sessionId} / ${sessionId}`); + + // this == context + if (this.sessionId == sessionId) { + //log.write(`BINARY START ${sessionId}`); + this.res.set({ + 'Content-Dispositon': `attachment; filename="${encodeURIComponent(fileName)}"`, // not useful anymore, we pass filein url, as recommended: https://stackoverflow.com/a/216777 + 'Content-Type': mimeType, // do we need that now ? probably a good ida + //'Content-Type': 'application/octet-stream;', + 'Content-Length': contentLength + }); + + //this.res.setHeader('Content-Description', 'File Transfer'); + //this.res.setHeader(); + //this.res.setHeader('Content-Type', 'application/octet-stream'); + + // this.res.setHeader('Content-Dispositon', `attachment; filename="${fileName}"`); + // this.res.setHeader('Content-Type', mimeType); + } +} + +function handleBinaryData({ data, sessionId }) { + // this == context + if (this.sessionId == sessionId) { + //console.log(`BINARY DATA ${sessionId}`); + + const flushed = this.res.write(data); + + if (!flushed) { + // todo CHECK if we have to check the returned boolean and wait a bit until sending the next chunk! + // log.red( + // `Data reported not flushed after res.write -- is everything working correctly? Consider holding off until drain event is emmited... check comments in source with links how to do it!` + // ); + // https://stackoverflow.com/a/54901120 + // https://nodejs.org/api/http.html#http_response_write_chunk_encoding_callback + } else { + log('Data reported flushed!'); + log('TODO: still have to fix and optimize, see comments in code...'); + } + } +} + +function handleBinaryEnd({ sessionId }) { + // this == expandedContext + if (this.sessionId == sessionId) { + //console.log(`BINARY END ${sessionId}`); + removeListeners(this); + //console.log(this); + + this.res.end(); + + this.finished = true; // expandedContext.finished = true + } +} + +// TODO, fix:: dropLngering connections has a bug, context is not set: +// test by removing tg handlers in connector and connection will drop! +// // TODO:: fix!! -- add removeListeners back!! +//eclipse pid 632 3/23/2020, 9:16:25 PM 62914ms (+01ms) ∞ TypeError: expandedContext.connector.removeListener is not a function +// at removeListeners (file:///Users/david/.dmt/core/node/aspect-content/dmt-content/lib/contentServer.js:128:29) +// at Object.dropLingeringConnection (file:///Users/david/.dmt/core/node/aspect-content/dmt-content/lib/contentServer.js:75:5) +// at listOnTimeout (internal/timers.js:549:17) +// at processTimers (internal/timers.js:492:7) + +function removeListeners(expandedContext) { + expandedContext.connector.removeListener('binary_start', expandedContext.attachedCallbacks.start); + expandedContext.connector.removeListener('binary_data', expandedContext.attachedCallbacks.data); + expandedContext.connector.removeListener('binary_end', expandedContext.attachedCallbacks.end); +} + +export default contentServer; diff --git a/apps/dmt-search/dmt/connectome-next/lib/fileTransport/contentServer/contentServer.js b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/contentServer/contentServer.js new file mode 100644 index 000000000..013fb9302 --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/contentServer/contentServer.js @@ -0,0 +1,88 @@ +import fs from 'fs'; +import path from 'path'; +import { decode } from '../fiberHandle/encodePath.js'; + + +import { log, colors } from 'dmt/common'; + +import { push } from 'dmt/notify'; + +import checkPermission from './checkPermission.js'; + +// function log(...args) { +// console.log(...args); +// } + +function fileNotFound({ providerAddress, fileName, res, host }) { + log.red(`File not found: ${providerAddress} -- ${fileName}`); + // TODO!! won't work on localhost!! /home ... ?q ... is wrong! + let pre = ''; + if (host.startsWith('localhost')) { + pre = 'apps/search/'; + } + + res.redirect(`/${pre}?q=${fileName}&error=file_not_found`); // TODO uri encode fileName ! + //res.status(404).send(`File not found -- ${fileName}`); +} + +// source: https://github.com/archiverjs/node-archiver/blob/master/examples/express.js +function contentServer({ app }) { + log.yellow('Starting content server ...'); + + // if (!defaultPort) { + // throw new Error('Must provide default fiber port for content server ...'); + // } + + app.use('/file', (req, res) => { + const { place } = req.query; + + const { host } = req.headers; + + //log.yellow(`Received content request ${place}`); + + if (place && place.includes('-')) { + const [providerAddress, _directory] = place.split('-'); + const directory = decode(_directory); + + const fileName = decodeURIComponent(req.path.slice(1)); + const filePath = path.join(directory, fileName); + + // we only serve files for default content (for now?) + if (!checkPermission({ directory })) { + // todo: change to something else? -- perhaps not! it's very suitable message + // and it masks the permission reason so that attacker cannot guess if directory actually does not exist + // or it's just not in default content! + log.red(`Prevented unauthorized file access - ${colors.gray(`Directory ${colors.yellow(directory)} is not exposed in default content`)}`); + fileNotFound({ providerAddress, fileName, res, host }); + return; + } + + // if (emitter) { + // emitter.emit('file_request', { providerAddress, filePath, host }); + // } + + // LOCAL FILE + if (providerAddress == 'localhost') { + if (fs.existsSync(filePath)) { + // todo: somehow fix repeated use, mp3, avi etc. + if (['.pdf', '.epub', '.txt'].includes(path.extname(filePath))) { + push.notify(`Serving ${fileName} (${filePath})`); + } + res.sendFile(filePath); + } else { + fileNotFound({ providerAddress, fileName, res, host }); // will this work? test + } + + return; + } + + // FILE COMING OVER ENCRYPTED FIBER + + res.status(404).send('This feature is on hold -- streaming files over encrypted fibers'); + } else { + res.status(404).send('Wrong file reference format, should be [ip]-[encodedRemoteDir]'); + } + }); +} + +export default contentServer; diff --git a/apps/dmt-search/dmt/connectome-next/lib/fileTransport/feedBytesIntoChannel/binaryReader.js b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/feedBytesIntoChannel/binaryReader.js new file mode 100644 index 000000000..17a0b005c --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/feedBytesIntoChannel/binaryReader.js @@ -0,0 +1,89 @@ +import fs from 'fs'; + +// convert to Uint8Array for browser consumption +// todo, explore more and find possibly a faster way if needed! +// source: https://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer +function toArrayBuffer(buf) { + const ab = new ArrayBuffer(buf.length); + const view = new Uint8Array(ab); + for (let i = 0; i < buf.length; ++i) { + view[i] = buf[i]; + } + return ab; +} + +class BinaryReader { + constructor(channel) { + this.channel = channel; + } + + sendFile({ sessionId, filePath }) { + let count = 0; + + return new Promise((success, reject) => { + const readStream = fs.createReadStream(filePath); + + // readStream.on('end', () => { + // console.log('STREAM ENDED'); + // readStream.destroy(); + // }); + + // setTimeout(() => { + // readStream.destroy(); + // }, 4000); + + // readStream.on('error', () => { + // console.log('STREAM ERROR'); + // readStream.destroy(); + // }); + + readStream.on('readable', () => { + const data = readStream.read(); + + if (data) { + if (count == 0) { + // log only the first chunk.... if needed for debugging, remove this + this.log({ data, sessionId, filePath, count }); + } + + const header = Buffer.from(sessionId); // sessionId length = 64 + const buffer = Buffer.concat([header, data]); // if we provide length ourselves it's faster + //const buffer = Buffer.concat([header, data], data.length + 64); // if we provide length ourselves it's faster + + // TODO: use some adaptive streaming... stream 500kb or so fast, then pause a bit to catch up, meanwhile the other side can already do something with first 500kb (of music etc.) + + this.channel.send(buffer); + + // OK? Check... don't feed all the data at once ! + // TODO: verify and improve, resources: + // + // + // + // + // + // + // if (count % 10 == 0) { + // readStream.pause(); + // console.log('PAUSING'); + // setTimeout(() => { + // readStream.resume(); + // console.log('RESUMING'); + // }, 30); // pause for 30ms + // } + + count += 1; + } else { + success(); // done + } + }); + + readStream.on('error', err => reject(new Error(`Problem serving file ${filePath} over ws: ${err.toString()}`))); + }); + } + + log({ data, sessionId, filePath, count }) { + console.log(`SID ${sessionId}: binary sending sequential data chunk n. ${count} - buffer length: ${data.length}, filePath: ${filePath}`); + } +} + +export default BinaryReader; diff --git a/apps/dmt-search/dmt/connectome-next/lib/fileTransport/feedBytesIntoChannel/streamFile.js b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/feedBytesIntoChannel/streamFile.js new file mode 100644 index 000000000..9769b167d --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/feedBytesIntoChannel/streamFile.js @@ -0,0 +1,35 @@ +import { loadModule } from '../../utils/index.js'; + +import BinaryReader from './binaryReader.js'; + +// TODO: +// make sure (test) that modules are only loaded at the first usage and not every time + +function streamFile({ channel, filePath, sessionId }) { + import('fs').then(fs => { + import('path').then(path => { + loadModule('mime').then(mimeModule => { + if (!fs.existsSync(filePath)) { + channel.send(JSON.stringify({ tag: 'file_not_found', sessionId })); + return; + } + + const mimeType = mimeModule.default.lookup(filePath); + + const contentLength = fs.statSync(filePath).size; + + channel.send(JSON.stringify({ tag: 'binary_start', fileName: path.basename(filePath), mimeType, contentLength, sessionId })); + + const binaryReader = new BinaryReader(channel); + + console.log(`fiber binary sending file: ${filePath}`); + + binaryReader.sendFile({ filePath, sessionId }).then(() => { + channel.send(JSON.stringify({ tag: 'binary_end', mimeType, sessionId })); + }); + }); + }); + }); +} + +export default streamFile; diff --git a/apps/dmt-search/dmt/connectome-next/lib/fileTransport/fiberHandle/encodePath.js b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/fiberHandle/encodePath.js new file mode 100644 index 000000000..cc7867a56 --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/fiberHandle/encodePath.js @@ -0,0 +1,11 @@ +import { isNodeJs, bufferToHex, hexToBuffer } from '../../utils/index.js'; + +function encode(text) { + return isNodeJs() && bufferToHex(Buffer.from(text, 'utf-8')); +} + +function decode(hexStr) { + return isNodeJs() && Buffer.from(hexToBuffer(hexStr)).toString(); +} + +export { encode, decode }; diff --git a/apps/dmt-search/dmt/connectome-next/lib/fileTransport/fiberHandle/fiberHandle.js b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/fiberHandle/fiberHandle.js new file mode 100644 index 000000000..d1444a3c2 --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/fileTransport/fiberHandle/fiberHandle.js @@ -0,0 +1,15 @@ +import { encode, decode } from './encodePath.js'; + +function create({ ip, port, defaultPort, fileName, directory }) { + let provider = ip; + + if (port && port != defaultPort) { + provider = `${ip}:${port}`; + } + + return `${encodeURIComponent(fileName)}?place=${provider}-${encode(directory)}`; +} + +export { create, encode, decode }; + +//console.log(encodeURI('1-Portrait of 'Night-Shining White', a favorite steed of Emperor Xuanzong.jpg')); diff --git a/apps/dmt-search/dmt/connectome-next/lib/utils/index.js b/apps/dmt-search/dmt/connectome-next/lib/utils/index.js new file mode 100644 index 000000000..900ba1070 --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/utils/index.js @@ -0,0 +1,65 @@ +// methods defined in this file +//⚠️ ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ +// this is a duplicate + +async function loadModule(whichUtil) { + return import(`./${whichUtil}`); +} + +function log(msg) { + console.log(`${new Date().toLocaleString()} → ${msg}`); +} + +function isBrowser() { + return typeof window !== 'undefined'; +} + +function isNodeJs() { + return !isBrowser(); +} + +function listify(obj) { + if (typeof obj == 'undefined' || obj == null) { + return []; + } + return Array.isArray(obj) ? obj : [obj]; +} + +function bufferToHex(buffer) { + return Array.from(new Uint8Array(buffer)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); +} + +function hexToBuffer(hex) { + const tokens = hex.match(/.{1,2}(?=(.{2})+(?!.))|.{1,2}$/g); // split by two, https://blog.abelotech.com/posts/split-string-tokens-defined-length-javascript/ + return new Uint8Array(tokens.map(token => parseInt(token, 16))); +} + +// source: https://stackoverflow.com/a/12965194/458177 +// good only up to 2**53 (JavaScript Integer range) -- usually this is plenty ... +function integerToByteArray(/*long*/ long, arrayLen = 8) { + // we want to represent the input as a 8-bytes array + const byteArray = new Array(arrayLen).fill(0); + + for (let index = 0; index < byteArray.length; index++) { + const byte = long & 0xff; + byteArray[index] = byte; + long = (long - byte) / 256; + } + + return byteArray; +} + +export { + // tool + loadModule, + // methods defined in this file: + log, + isBrowser, + isNodeJs, + listify, + bufferToHex, + hexToBuffer, + integerToByteArray +}; diff --git a/apps/dmt-search/dmt/connectome-next/lib/utils/mime/HISTORY.md b/apps/dmt-search/dmt/connectome-next/lib/utils/mime/HISTORY.md new file mode 100644 index 000000000..db3b311b6 --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/utils/mime/HISTORY.md @@ -0,0 +1,325 @@ +2.1.26 / 2020-01-05 +=================== + + * deps: mime-db@1.43.0 + - Add `application/x-keepass2` with extension `.kdbx` + - Add extension `.mxmf` to `audio/mobile-xmf` + - Add extensions from IANA for `application/*+xml` types + - Add new upstream MIME types + +2.1.25 / 2019-11-12 +=================== + + * deps: mime-db@1.42.0 + - Add new upstream MIME types + - Add `application/toml` with extension `.toml` + - Add `image/vnd.ms-dds` with extension `.dds` + +2.1.24 / 2019-04-20 +=================== + + * deps: mime-db@1.40.0 + - Add extensions from IANA for `model/*` types + - Add `text/mdx` with extension `.mdx` + +2.1.23 / 2019-04-17 +=================== + + * deps: mime-db@~1.39.0 + - Add extensions `.siv` and `.sieve` to `application/sieve` + - Add new upstream MIME types + +2.1.22 / 2019-02-14 +=================== + + * deps: mime-db@~1.38.0 + - Add extension `.nq` to `application/n-quads` + - Add extension `.nt` to `application/n-triples` + - Add new upstream MIME types + - Mark `text/less` as compressible + +2.1.21 / 2018-10-19 +=================== + + * deps: mime-db@~1.37.0 + - Add extensions to HEIC image types + - Add new upstream MIME types + +2.1.20 / 2018-08-26 +=================== + + * deps: mime-db@~1.36.0 + - Add Apple file extensions from IANA + - Add extensions from IANA for `image/*` types + - Add new upstream MIME types + +2.1.19 / 2018-07-17 +=================== + + * deps: mime-db@~1.35.0 + - Add extension `.csl` to `application/vnd.citationstyles.style+xml` + - Add extension `.es` to `application/ecmascript` + - Add extension `.owl` to `application/rdf+xml` + - Add new upstream MIME types + - Add UTF-8 as default charset for `text/turtle` + +2.1.18 / 2018-02-16 +=================== + + * deps: mime-db@~1.33.0 + - Add `application/raml+yaml` with extension `.raml` + - Add `application/wasm` with extension `.wasm` + - Add `text/shex` with extension `.shex` + - Add extensions for JPEG-2000 images + - Add extensions from IANA for `message/*` types + - Add new upstream MIME types + - Update font MIME types + - Update `text/hjson` to registered `application/hjson` + +2.1.17 / 2017-09-01 +=================== + + * deps: mime-db@~1.30.0 + - Add `application/vnd.ms-outlook` + - Add `application/x-arj` + - Add extension `.mjs` to `application/javascript` + - Add glTF types and extensions + - Add new upstream MIME types + - Add `text/x-org` + - Add VirtualBox MIME types + - Fix `source` records for `video/*` types that are IANA + - Update `font/opentype` to registered `font/otf` + +2.1.16 / 2017-07-24 +=================== + + * deps: mime-db@~1.29.0 + - Add `application/fido.trusted-apps+json` + - Add extension `.wadl` to `application/vnd.sun.wadl+xml` + - Add extension `.gz` to `application/gzip` + - Add new upstream MIME types + - Update extensions `.md` and `.markdown` to be `text/markdown` + +2.1.15 / 2017-03-23 +=================== + + * deps: mime-db@~1.27.0 + - Add new mime types + - Add `image/apng` + +2.1.14 / 2017-01-14 +=================== + + * deps: mime-db@~1.26.0 + - Add new mime types + +2.1.13 / 2016-11-18 +=================== + + * deps: mime-db@~1.25.0 + - Add new mime types + +2.1.12 / 2016-09-18 +=================== + + * deps: mime-db@~1.24.0 + - Add new mime types + - Add `audio/mp3` + +2.1.11 / 2016-05-01 +=================== + + * deps: mime-db@~1.23.0 + - Add new mime types + +2.1.10 / 2016-02-15 +=================== + + * deps: mime-db@~1.22.0 + - Add new mime types + - Fix extension of `application/dash+xml` + - Update primary extension for `audio/mp4` + +2.1.9 / 2016-01-06 +================== + + * deps: mime-db@~1.21.0 + - Add new mime types + +2.1.8 / 2015-11-30 +================== + + * deps: mime-db@~1.20.0 + - Add new mime types + +2.1.7 / 2015-09-20 +================== + + * deps: mime-db@~1.19.0 + - Add new mime types + +2.1.6 / 2015-09-03 +================== + + * deps: mime-db@~1.18.0 + - Add new mime types + +2.1.5 / 2015-08-20 +================== + + * deps: mime-db@~1.17.0 + - Add new mime types + +2.1.4 / 2015-07-30 +================== + + * deps: mime-db@~1.16.0 + - Add new mime types + +2.1.3 / 2015-07-13 +================== + + * deps: mime-db@~1.15.0 + - Add new mime types + +2.1.2 / 2015-06-25 +================== + + * deps: mime-db@~1.14.0 + - Add new mime types + +2.1.1 / 2015-06-08 +================== + + * perf: fix deopt during mapping + +2.1.0 / 2015-06-07 +================== + + * Fix incorrectly treating extension-less file name as extension + - i.e. `'path/to/json'` will no longer return `application/json` + * Fix `.charset(type)` to accept parameters + * Fix `.charset(type)` to match case-insensitive + * Improve generation of extension to MIME mapping + * Refactor internals for readability and no argument reassignment + * Prefer `application/*` MIME types from the same source + * Prefer any type over `application/octet-stream` + * deps: mime-db@~1.13.0 + - Add nginx as a source + - Add new mime types + +2.0.14 / 2015-06-06 +=================== + + * deps: mime-db@~1.12.0 + - Add new mime types + +2.0.13 / 2015-05-31 +=================== + + * deps: mime-db@~1.11.0 + - Add new mime types + +2.0.12 / 2015-05-19 +=================== + + * deps: mime-db@~1.10.0 + - Add new mime types + +2.0.11 / 2015-05-05 +=================== + + * deps: mime-db@~1.9.1 + - Add new mime types + +2.0.10 / 2015-03-13 +=================== + + * deps: mime-db@~1.8.0 + - Add new mime types + +2.0.9 / 2015-02-09 +================== + + * deps: mime-db@~1.7.0 + - Add new mime types + - Community extensions ownership transferred from `node-mime` + +2.0.8 / 2015-01-29 +================== + + * deps: mime-db@~1.6.0 + - Add new mime types + +2.0.7 / 2014-12-30 +================== + + * deps: mime-db@~1.5.0 + - Add new mime types + - Fix various invalid MIME type entries + +2.0.6 / 2014-12-30 +================== + + * deps: mime-db@~1.4.0 + - Add new mime types + - Fix various invalid MIME type entries + - Remove example template MIME types + +2.0.5 / 2014-12-29 +================== + + * deps: mime-db@~1.3.1 + - Fix missing extensions + +2.0.4 / 2014-12-10 +================== + + * deps: mime-db@~1.3.0 + - Add new mime types + +2.0.3 / 2014-11-09 +================== + + * deps: mime-db@~1.2.0 + - Add new mime types + +2.0.2 / 2014-09-28 +================== + + * deps: mime-db@~1.1.0 + - Add new mime types + - Add additional compressible + - Update charsets + +2.0.1 / 2014-09-07 +================== + + * Support Node.js 0.6 + +2.0.0 / 2014-09-02 +================== + + * Use `mime-db` + * Remove `.define()` + +1.0.2 / 2014-08-04 +================== + + * Set charset=utf-8 for `text/javascript` + +1.0.1 / 2014-06-24 +================== + + * Add `text/jsx` type + +1.0.0 / 2014-05-12 +================== + + * Return `false` for unknown types + * Set charset=utf-8 for `application/json` + +0.1.0 / 2014-05-02 +================== + + * Initial release diff --git a/apps/dmt-search/dmt/connectome-next/lib/utils/mime/LICENSE b/apps/dmt-search/dmt/connectome-next/lib/utils/mime/LICENSE new file mode 100644 index 000000000..06166077b --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/utils/mime/LICENSE @@ -0,0 +1,23 @@ +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/apps/dmt-search/dmt/connectome-next/lib/utils/mime/README.md b/apps/dmt-search/dmt/connectome-next/lib/utils/mime/README.md new file mode 100644 index 000000000..1dbef2b57 --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/utils/mime/README.md @@ -0,0 +1,113 @@ +# mime-types + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Node.js Version][node-version-image]][node-version-url] +[![Build Status][travis-image]][travis-url] +[![Test Coverage][coveralls-image]][coveralls-url] + +The ultimate javascript content-type utility. + +Similar to [the `mime@1.x` module](https://www.npmjs.com/package/mime), except: + +- __No fallbacks.__ Instead of naively returning the first available type, + `mime-types` simply returns `false`, so do + `var type = mime.lookup('unrecognized') || 'application/octet-stream'`. +- No `new Mime()` business, so you could do `var lookup = require('mime-types').lookup`. +- No `.define()` functionality +- Bug fixes for `.lookup(path)` + +Otherwise, the API is compatible with `mime` 1.x. + +## Install + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + +```sh +$ npm install mime-types +``` + +## Adding Types + +All mime types are based on [mime-db](https://www.npmjs.com/package/mime-db), +so open a PR there if you'd like to add mime types. + +## API + +```js +var mime = require('mime-types') +``` + +All functions return `false` if input is invalid or not found. + +### mime.lookup(path) + +Lookup the content-type associated with a file. + +```js +mime.lookup('json') // 'application/json' +mime.lookup('.md') // 'text/markdown' +mime.lookup('file.html') // 'text/html' +mime.lookup('folder/file.js') // 'application/javascript' +mime.lookup('folder/.htaccess') // false + +mime.lookup('cats') // false +``` + +### mime.contentType(type) + +Create a full content-type header given a content-type or extension. +When given an extension, `mime.lookup` is used to get the matching +content-type, otherwise the given content-type is used. Then if the +content-type does not already have a `charset` parameter, `mime.charset` +is used to get the default charset and add to the returned content-type. + +```js +mime.contentType('markdown') // 'text/x-markdown; charset=utf-8' +mime.contentType('file.json') // 'application/json; charset=utf-8' +mime.contentType('text/html') // 'text/html; charset=utf-8' +mime.contentType('text/html; charset=iso-8859-1') // 'text/html; charset=iso-8859-1' + +// from a full path +mime.contentType(path.extname('/path/to/file.json')) // 'application/json; charset=utf-8' +``` + +### mime.extension(type) + +Get the default extension for a content-type. + +```js +mime.extension('application/octet-stream') // 'bin' +``` + +### mime.charset(type) + +Lookup the implied default charset of a content-type. + +```js +mime.charset('text/markdown') // 'UTF-8' +``` + +### var type = mime.types[extension] + +A map of content-types by extension. + +### [extensions...] = mime.extensions[type] + +A map of extensions by content-type. + +## License + +[MIT](LICENSE) + +[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/mime-types/master +[coveralls-url]: https://coveralls.io/r/jshttp/mime-types?branch=master +[node-version-image]: https://badgen.net/npm/node/mime-types +[node-version-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/mime-types +[npm-url]: https://npmjs.org/package/mime-types +[npm-version-image]: https://badgen.net/npm/v/mime-types +[travis-image]: https://badgen.net/travis/jshttp/mime-types/master +[travis-url]: https://travis-ci.org/jshttp/mime-types diff --git a/apps/dmt-search/dmt/connectome-next/lib/utils/mime/db.json b/apps/dmt-search/dmt/connectome-next/lib/utils/mime/db.json new file mode 100644 index 000000000..cfa3c6351 --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/utils/mime/db.json @@ -0,0 +1,8060 @@ +{ + "application/1d-interleaved-parityfec": { + "source": "iana" + }, + "application/3gpdash-qoe-report+xml": { + "source": "iana", + "compressible": true + }, + "application/3gpp-ims+xml": { + "source": "iana", + "compressible": true + }, + "application/a2l": { + "source": "iana" + }, + "application/activemessage": { + "source": "iana" + }, + "application/activity+json": { + "source": "iana", + "compressible": true + }, + "application/alto-costmap+json": { + "source": "iana", + "compressible": true + }, + "application/alto-costmapfilter+json": { + "source": "iana", + "compressible": true + }, + "application/alto-directory+json": { + "source": "iana", + "compressible": true + }, + "application/alto-endpointcost+json": { + "source": "iana", + "compressible": true + }, + "application/alto-endpointcostparams+json": { + "source": "iana", + "compressible": true + }, + "application/alto-endpointprop+json": { + "source": "iana", + "compressible": true + }, + "application/alto-endpointpropparams+json": { + "source": "iana", + "compressible": true + }, + "application/alto-error+json": { + "source": "iana", + "compressible": true + }, + "application/alto-networkmap+json": { + "source": "iana", + "compressible": true + }, + "application/alto-networkmapfilter+json": { + "source": "iana", + "compressible": true + }, + "application/aml": { + "source": "iana" + }, + "application/andrew-inset": { + "source": "iana", + "extensions": ["ez"] + }, + "application/applefile": { + "source": "iana" + }, + "application/applixware": { + "source": "apache", + "extensions": ["aw"] + }, + "application/atf": { + "source": "iana" + }, + "application/atfx": { + "source": "iana" + }, + "application/atom+xml": { + "source": "iana", + "compressible": true, + "extensions": ["atom"] + }, + "application/atomcat+xml": { + "source": "iana", + "compressible": true, + "extensions": ["atomcat"] + }, + "application/atomdeleted+xml": { + "source": "iana", + "compressible": true, + "extensions": ["atomdeleted"] + }, + "application/atomicmail": { + "source": "iana" + }, + "application/atomsvc+xml": { + "source": "iana", + "compressible": true, + "extensions": ["atomsvc"] + }, + "application/atsc-dwd+xml": { + "source": "iana", + "compressible": true, + "extensions": ["dwd"] + }, + "application/atsc-held+xml": { + "source": "iana", + "compressible": true, + "extensions": ["held"] + }, + "application/atsc-rdt+json": { + "source": "iana", + "compressible": true + }, + "application/atsc-rsat+xml": { + "source": "iana", + "compressible": true, + "extensions": ["rsat"] + }, + "application/atxml": { + "source": "iana" + }, + "application/auth-policy+xml": { + "source": "iana", + "compressible": true + }, + "application/bacnet-xdd+zip": { + "source": "iana", + "compressible": false + }, + "application/batch-smtp": { + "source": "iana" + }, + "application/bdoc": { + "compressible": false, + "extensions": ["bdoc"] + }, + "application/beep+xml": { + "source": "iana", + "compressible": true + }, + "application/calendar+json": { + "source": "iana", + "compressible": true + }, + "application/calendar+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xcs"] + }, + "application/call-completion": { + "source": "iana" + }, + "application/cals-1840": { + "source": "iana" + }, + "application/cbor": { + "source": "iana" + }, + "application/cbor-seq": { + "source": "iana" + }, + "application/cccex": { + "source": "iana" + }, + "application/ccmp+xml": { + "source": "iana", + "compressible": true + }, + "application/ccxml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["ccxml"] + }, + "application/cdfx+xml": { + "source": "iana", + "compressible": true, + "extensions": ["cdfx"] + }, + "application/cdmi-capability": { + "source": "iana", + "extensions": ["cdmia"] + }, + "application/cdmi-container": { + "source": "iana", + "extensions": ["cdmic"] + }, + "application/cdmi-domain": { + "source": "iana", + "extensions": ["cdmid"] + }, + "application/cdmi-object": { + "source": "iana", + "extensions": ["cdmio"] + }, + "application/cdmi-queue": { + "source": "iana", + "extensions": ["cdmiq"] + }, + "application/cdni": { + "source": "iana" + }, + "application/cea": { + "source": "iana" + }, + "application/cea-2018+xml": { + "source": "iana", + "compressible": true + }, + "application/cellml+xml": { + "source": "iana", + "compressible": true + }, + "application/cfw": { + "source": "iana" + }, + "application/clue+xml": { + "source": "iana", + "compressible": true + }, + "application/clue_info+xml": { + "source": "iana", + "compressible": true + }, + "application/cms": { + "source": "iana" + }, + "application/cnrp+xml": { + "source": "iana", + "compressible": true + }, + "application/coap-group+json": { + "source": "iana", + "compressible": true + }, + "application/coap-payload": { + "source": "iana" + }, + "application/commonground": { + "source": "iana" + }, + "application/conference-info+xml": { + "source": "iana", + "compressible": true + }, + "application/cose": { + "source": "iana" + }, + "application/cose-key": { + "source": "iana" + }, + "application/cose-key-set": { + "source": "iana" + }, + "application/cpl+xml": { + "source": "iana", + "compressible": true + }, + "application/csrattrs": { + "source": "iana" + }, + "application/csta+xml": { + "source": "iana", + "compressible": true + }, + "application/cstadata+xml": { + "source": "iana", + "compressible": true + }, + "application/csvm+json": { + "source": "iana", + "compressible": true + }, + "application/cu-seeme": { + "source": "apache", + "extensions": ["cu"] + }, + "application/cwt": { + "source": "iana" + }, + "application/cybercash": { + "source": "iana" + }, + "application/dart": { + "compressible": true + }, + "application/dash+xml": { + "source": "iana", + "compressible": true, + "extensions": ["mpd"] + }, + "application/dashdelta": { + "source": "iana" + }, + "application/davmount+xml": { + "source": "iana", + "compressible": true, + "extensions": ["davmount"] + }, + "application/dca-rft": { + "source": "iana" + }, + "application/dcd": { + "source": "iana" + }, + "application/dec-dx": { + "source": "iana" + }, + "application/dialog-info+xml": { + "source": "iana", + "compressible": true + }, + "application/dicom": { + "source": "iana" + }, + "application/dicom+json": { + "source": "iana", + "compressible": true + }, + "application/dicom+xml": { + "source": "iana", + "compressible": true + }, + "application/dii": { + "source": "iana" + }, + "application/dit": { + "source": "iana" + }, + "application/dns": { + "source": "iana" + }, + "application/dns+json": { + "source": "iana", + "compressible": true + }, + "application/dns-message": { + "source": "iana" + }, + "application/docbook+xml": { + "source": "apache", + "compressible": true, + "extensions": ["dbk"] + }, + "application/dskpp+xml": { + "source": "iana", + "compressible": true + }, + "application/dssc+der": { + "source": "iana", + "extensions": ["dssc"] + }, + "application/dssc+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xdssc"] + }, + "application/dvcs": { + "source": "iana" + }, + "application/ecmascript": { + "source": "iana", + "compressible": true, + "extensions": ["ecma","es"] + }, + "application/edi-consent": { + "source": "iana" + }, + "application/edi-x12": { + "source": "iana", + "compressible": false + }, + "application/edifact": { + "source": "iana", + "compressible": false + }, + "application/efi": { + "source": "iana" + }, + "application/emergencycalldata.comment+xml": { + "source": "iana", + "compressible": true + }, + "application/emergencycalldata.control+xml": { + "source": "iana", + "compressible": true + }, + "application/emergencycalldata.deviceinfo+xml": { + "source": "iana", + "compressible": true + }, + "application/emergencycalldata.ecall.msd": { + "source": "iana" + }, + "application/emergencycalldata.providerinfo+xml": { + "source": "iana", + "compressible": true + }, + "application/emergencycalldata.serviceinfo+xml": { + "source": "iana", + "compressible": true + }, + "application/emergencycalldata.subscriberinfo+xml": { + "source": "iana", + "compressible": true + }, + "application/emergencycalldata.veds+xml": { + "source": "iana", + "compressible": true + }, + "application/emma+xml": { + "source": "iana", + "compressible": true, + "extensions": ["emma"] + }, + "application/emotionml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["emotionml"] + }, + "application/encaprtp": { + "source": "iana" + }, + "application/epp+xml": { + "source": "iana", + "compressible": true + }, + "application/epub+zip": { + "source": "iana", + "compressible": false, + "extensions": ["epub"] + }, + "application/eshop": { + "source": "iana" + }, + "application/exi": { + "source": "iana", + "extensions": ["exi"] + }, + "application/expect-ct-report+json": { + "source": "iana", + "compressible": true + }, + "application/fastinfoset": { + "source": "iana" + }, + "application/fastsoap": { + "source": "iana" + }, + "application/fdt+xml": { + "source": "iana", + "compressible": true, + "extensions": ["fdt"] + }, + "application/fhir+json": { + "source": "iana", + "compressible": true + }, + "application/fhir+xml": { + "source": "iana", + "compressible": true + }, + "application/fido.trusted-apps+json": { + "compressible": true + }, + "application/fits": { + "source": "iana" + }, + "application/flexfec": { + "source": "iana" + }, + "application/font-sfnt": { + "source": "iana" + }, + "application/font-tdpfr": { + "source": "iana", + "extensions": ["pfr"] + }, + "application/font-woff": { + "source": "iana", + "compressible": false + }, + "application/framework-attributes+xml": { + "source": "iana", + "compressible": true + }, + "application/geo+json": { + "source": "iana", + "compressible": true, + "extensions": ["geojson"] + }, + "application/geo+json-seq": { + "source": "iana" + }, + "application/geopackage+sqlite3": { + "source": "iana" + }, + "application/geoxacml+xml": { + "source": "iana", + "compressible": true + }, + "application/gltf-buffer": { + "source": "iana" + }, + "application/gml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["gml"] + }, + "application/gpx+xml": { + "source": "apache", + "compressible": true, + "extensions": ["gpx"] + }, + "application/gxf": { + "source": "apache", + "extensions": ["gxf"] + }, + "application/gzip": { + "source": "iana", + "compressible": false, + "extensions": ["gz"] + }, + "application/h224": { + "source": "iana" + }, + "application/held+xml": { + "source": "iana", + "compressible": true + }, + "application/hjson": { + "extensions": ["hjson"] + }, + "application/http": { + "source": "iana" + }, + "application/hyperstudio": { + "source": "iana", + "extensions": ["stk"] + }, + "application/ibe-key-request+xml": { + "source": "iana", + "compressible": true + }, + "application/ibe-pkg-reply+xml": { + "source": "iana", + "compressible": true + }, + "application/ibe-pp-data": { + "source": "iana" + }, + "application/iges": { + "source": "iana" + }, + "application/im-iscomposing+xml": { + "source": "iana", + "compressible": true + }, + "application/index": { + "source": "iana" + }, + "application/index.cmd": { + "source": "iana" + }, + "application/index.obj": { + "source": "iana" + }, + "application/index.response": { + "source": "iana" + }, + "application/index.vnd": { + "source": "iana" + }, + "application/inkml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["ink","inkml"] + }, + "application/iotp": { + "source": "iana" + }, + "application/ipfix": { + "source": "iana", + "extensions": ["ipfix"] + }, + "application/ipp": { + "source": "iana" + }, + "application/isup": { + "source": "iana" + }, + "application/its+xml": { + "source": "iana", + "compressible": true, + "extensions": ["its"] + }, + "application/java-archive": { + "source": "apache", + "compressible": false, + "extensions": ["jar","war","ear"] + }, + "application/java-serialized-object": { + "source": "apache", + "compressible": false, + "extensions": ["ser"] + }, + "application/java-vm": { + "source": "apache", + "compressible": false, + "extensions": ["class"] + }, + "application/javascript": { + "source": "iana", + "charset": "UTF-8", + "compressible": true, + "extensions": ["js","mjs"] + }, + "application/jf2feed+json": { + "source": "iana", + "compressible": true + }, + "application/jose": { + "source": "iana" + }, + "application/jose+json": { + "source": "iana", + "compressible": true + }, + "application/jrd+json": { + "source": "iana", + "compressible": true + }, + "application/json": { + "source": "iana", + "charset": "UTF-8", + "compressible": true, + "extensions": ["json","map"] + }, + "application/json-patch+json": { + "source": "iana", + "compressible": true + }, + "application/json-seq": { + "source": "iana" + }, + "application/json5": { + "extensions": ["json5"] + }, + "application/jsonml+json": { + "source": "apache", + "compressible": true, + "extensions": ["jsonml"] + }, + "application/jwk+json": { + "source": "iana", + "compressible": true + }, + "application/jwk-set+json": { + "source": "iana", + "compressible": true + }, + "application/jwt": { + "source": "iana" + }, + "application/kpml-request+xml": { + "source": "iana", + "compressible": true + }, + "application/kpml-response+xml": { + "source": "iana", + "compressible": true + }, + "application/ld+json": { + "source": "iana", + "compressible": true, + "extensions": ["jsonld"] + }, + "application/lgr+xml": { + "source": "iana", + "compressible": true, + "extensions": ["lgr"] + }, + "application/link-format": { + "source": "iana" + }, + "application/load-control+xml": { + "source": "iana", + "compressible": true + }, + "application/lost+xml": { + "source": "iana", + "compressible": true, + "extensions": ["lostxml"] + }, + "application/lostsync+xml": { + "source": "iana", + "compressible": true + }, + "application/lxf": { + "source": "iana" + }, + "application/mac-binhex40": { + "source": "iana", + "extensions": ["hqx"] + }, + "application/mac-compactpro": { + "source": "apache", + "extensions": ["cpt"] + }, + "application/macwriteii": { + "source": "iana" + }, + "application/mads+xml": { + "source": "iana", + "compressible": true, + "extensions": ["mads"] + }, + "application/manifest+json": { + "charset": "UTF-8", + "compressible": true, + "extensions": ["webmanifest"] + }, + "application/marc": { + "source": "iana", + "extensions": ["mrc"] + }, + "application/marcxml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["mrcx"] + }, + "application/mathematica": { + "source": "iana", + "extensions": ["ma","nb","mb"] + }, + "application/mathml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["mathml"] + }, + "application/mathml-content+xml": { + "source": "iana", + "compressible": true + }, + "application/mathml-presentation+xml": { + "source": "iana", + "compressible": true + }, + "application/mbms-associated-procedure-description+xml": { + "source": "iana", + "compressible": true + }, + "application/mbms-deregister+xml": { + "source": "iana", + "compressible": true + }, + "application/mbms-envelope+xml": { + "source": "iana", + "compressible": true + }, + "application/mbms-msk+xml": { + "source": "iana", + "compressible": true + }, + "application/mbms-msk-response+xml": { + "source": "iana", + "compressible": true + }, + "application/mbms-protection-description+xml": { + "source": "iana", + "compressible": true + }, + "application/mbms-reception-report+xml": { + "source": "iana", + "compressible": true + }, + "application/mbms-register+xml": { + "source": "iana", + "compressible": true + }, + "application/mbms-register-response+xml": { + "source": "iana", + "compressible": true + }, + "application/mbms-schedule+xml": { + "source": "iana", + "compressible": true + }, + "application/mbms-user-service-description+xml": { + "source": "iana", + "compressible": true + }, + "application/mbox": { + "source": "iana", + "extensions": ["mbox"] + }, + "application/media-policy-dataset+xml": { + "source": "iana", + "compressible": true + }, + "application/media_control+xml": { + "source": "iana", + "compressible": true + }, + "application/mediaservercontrol+xml": { + "source": "iana", + "compressible": true, + "extensions": ["mscml"] + }, + "application/merge-patch+json": { + "source": "iana", + "compressible": true + }, + "application/metalink+xml": { + "source": "apache", + "compressible": true, + "extensions": ["metalink"] + }, + "application/metalink4+xml": { + "source": "iana", + "compressible": true, + "extensions": ["meta4"] + }, + "application/mets+xml": { + "source": "iana", + "compressible": true, + "extensions": ["mets"] + }, + "application/mf4": { + "source": "iana" + }, + "application/mikey": { + "source": "iana" + }, + "application/mipc": { + "source": "iana" + }, + "application/mmt-aei+xml": { + "source": "iana", + "compressible": true, + "extensions": ["maei"] + }, + "application/mmt-usd+xml": { + "source": "iana", + "compressible": true, + "extensions": ["musd"] + }, + "application/mods+xml": { + "source": "iana", + "compressible": true, + "extensions": ["mods"] + }, + "application/moss-keys": { + "source": "iana" + }, + "application/moss-signature": { + "source": "iana" + }, + "application/mosskey-data": { + "source": "iana" + }, + "application/mosskey-request": { + "source": "iana" + }, + "application/mp21": { + "source": "iana", + "extensions": ["m21","mp21"] + }, + "application/mp4": { + "source": "iana", + "extensions": ["mp4s","m4p"] + }, + "application/mpeg4-generic": { + "source": "iana" + }, + "application/mpeg4-iod": { + "source": "iana" + }, + "application/mpeg4-iod-xmt": { + "source": "iana" + }, + "application/mrb-consumer+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xdf"] + }, + "application/mrb-publish+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xdf"] + }, + "application/msc-ivr+xml": { + "source": "iana", + "compressible": true + }, + "application/msc-mixer+xml": { + "source": "iana", + "compressible": true + }, + "application/msword": { + "source": "iana", + "compressible": false, + "extensions": ["doc","dot"] + }, + "application/mud+json": { + "source": "iana", + "compressible": true + }, + "application/multipart-core": { + "source": "iana" + }, + "application/mxf": { + "source": "iana", + "extensions": ["mxf"] + }, + "application/n-quads": { + "source": "iana", + "extensions": ["nq"] + }, + "application/n-triples": { + "source": "iana", + "extensions": ["nt"] + }, + "application/nasdata": { + "source": "iana" + }, + "application/news-checkgroups": { + "source": "iana" + }, + "application/news-groupinfo": { + "source": "iana" + }, + "application/news-transmission": { + "source": "iana" + }, + "application/nlsml+xml": { + "source": "iana", + "compressible": true + }, + "application/node": { + "source": "iana" + }, + "application/nss": { + "source": "iana" + }, + "application/ocsp-request": { + "source": "iana" + }, + "application/ocsp-response": { + "source": "iana" + }, + "application/octet-stream": { + "source": "iana", + "compressible": false, + "extensions": ["bin","dms","lrf","mar","so","dist","distz","pkg","bpk","dump","elc","deploy","exe","dll","deb","dmg","iso","img","msi","msp","msm","buffer"] + }, + "application/oda": { + "source": "iana", + "extensions": ["oda"] + }, + "application/odm+xml": { + "source": "iana", + "compressible": true + }, + "application/odx": { + "source": "iana" + }, + "application/oebps-package+xml": { + "source": "iana", + "compressible": true, + "extensions": ["opf"] + }, + "application/ogg": { + "source": "iana", + "compressible": false, + "extensions": ["ogx"] + }, + "application/omdoc+xml": { + "source": "apache", + "compressible": true, + "extensions": ["omdoc"] + }, + "application/onenote": { + "source": "apache", + "extensions": ["onetoc","onetoc2","onetmp","onepkg"] + }, + "application/oscore": { + "source": "iana" + }, + "application/oxps": { + "source": "iana", + "extensions": ["oxps"] + }, + "application/p2p-overlay+xml": { + "source": "iana", + "compressible": true, + "extensions": ["relo"] + }, + "application/parityfec": { + "source": "iana" + }, + "application/passport": { + "source": "iana" + }, + "application/patch-ops-error+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xer"] + }, + "application/pdf": { + "source": "iana", + "compressible": false, + "extensions": ["pdf"] + }, + "application/pdx": { + "source": "iana" + }, + "application/pem-certificate-chain": { + "source": "iana" + }, + "application/pgp-encrypted": { + "source": "iana", + "compressible": false, + "extensions": ["pgp"] + }, + "application/pgp-keys": { + "source": "iana" + }, + "application/pgp-signature": { + "source": "iana", + "extensions": ["asc","sig"] + }, + "application/pics-rules": { + "source": "apache", + "extensions": ["prf"] + }, + "application/pidf+xml": { + "source": "iana", + "compressible": true + }, + "application/pidf-diff+xml": { + "source": "iana", + "compressible": true + }, + "application/pkcs10": { + "source": "iana", + "extensions": ["p10"] + }, + "application/pkcs12": { + "source": "iana" + }, + "application/pkcs7-mime": { + "source": "iana", + "extensions": ["p7m","p7c"] + }, + "application/pkcs7-signature": { + "source": "iana", + "extensions": ["p7s"] + }, + "application/pkcs8": { + "source": "iana", + "extensions": ["p8"] + }, + "application/pkcs8-encrypted": { + "source": "iana" + }, + "application/pkix-attr-cert": { + "source": "iana", + "extensions": ["ac"] + }, + "application/pkix-cert": { + "source": "iana", + "extensions": ["cer"] + }, + "application/pkix-crl": { + "source": "iana", + "extensions": ["crl"] + }, + "application/pkix-pkipath": { + "source": "iana", + "extensions": ["pkipath"] + }, + "application/pkixcmp": { + "source": "iana", + "extensions": ["pki"] + }, + "application/pls+xml": { + "source": "iana", + "compressible": true, + "extensions": ["pls"] + }, + "application/poc-settings+xml": { + "source": "iana", + "compressible": true + }, + "application/postscript": { + "source": "iana", + "compressible": true, + "extensions": ["ai","eps","ps"] + }, + "application/ppsp-tracker+json": { + "source": "iana", + "compressible": true + }, + "application/problem+json": { + "source": "iana", + "compressible": true + }, + "application/problem+xml": { + "source": "iana", + "compressible": true + }, + "application/provenance+xml": { + "source": "iana", + "compressible": true, + "extensions": ["provx"] + }, + "application/prs.alvestrand.titrax-sheet": { + "source": "iana" + }, + "application/prs.cww": { + "source": "iana", + "extensions": ["cww"] + }, + "application/prs.hpub+zip": { + "source": "iana", + "compressible": false + }, + "application/prs.nprend": { + "source": "iana" + }, + "application/prs.plucker": { + "source": "iana" + }, + "application/prs.rdf-xml-crypt": { + "source": "iana" + }, + "application/prs.xsf+xml": { + "source": "iana", + "compressible": true + }, + "application/pskc+xml": { + "source": "iana", + "compressible": true, + "extensions": ["pskcxml"] + }, + "application/qsig": { + "source": "iana" + }, + "application/raml+yaml": { + "compressible": true, + "extensions": ["raml"] + }, + "application/raptorfec": { + "source": "iana" + }, + "application/rdap+json": { + "source": "iana", + "compressible": true + }, + "application/rdf+xml": { + "source": "iana", + "compressible": true, + "extensions": ["rdf","owl"] + }, + "application/reginfo+xml": { + "source": "iana", + "compressible": true, + "extensions": ["rif"] + }, + "application/relax-ng-compact-syntax": { + "source": "iana", + "extensions": ["rnc"] + }, + "application/remote-printing": { + "source": "iana" + }, + "application/reputon+json": { + "source": "iana", + "compressible": true + }, + "application/resource-lists+xml": { + "source": "iana", + "compressible": true, + "extensions": ["rl"] + }, + "application/resource-lists-diff+xml": { + "source": "iana", + "compressible": true, + "extensions": ["rld"] + }, + "application/rfc+xml": { + "source": "iana", + "compressible": true + }, + "application/riscos": { + "source": "iana" + }, + "application/rlmi+xml": { + "source": "iana", + "compressible": true + }, + "application/rls-services+xml": { + "source": "iana", + "compressible": true, + "extensions": ["rs"] + }, + "application/route-apd+xml": { + "source": "iana", + "compressible": true, + "extensions": ["rapd"] + }, + "application/route-s-tsid+xml": { + "source": "iana", + "compressible": true, + "extensions": ["sls"] + }, + "application/route-usd+xml": { + "source": "iana", + "compressible": true, + "extensions": ["rusd"] + }, + "application/rpki-ghostbusters": { + "source": "iana", + "extensions": ["gbr"] + }, + "application/rpki-manifest": { + "source": "iana", + "extensions": ["mft"] + }, + "application/rpki-publication": { + "source": "iana" + }, + "application/rpki-roa": { + "source": "iana", + "extensions": ["roa"] + }, + "application/rpki-updown": { + "source": "iana" + }, + "application/rsd+xml": { + "source": "apache", + "compressible": true, + "extensions": ["rsd"] + }, + "application/rss+xml": { + "source": "apache", + "compressible": true, + "extensions": ["rss"] + }, + "application/rtf": { + "source": "iana", + "compressible": true, + "extensions": ["rtf"] + }, + "application/rtploopback": { + "source": "iana" + }, + "application/rtx": { + "source": "iana" + }, + "application/samlassertion+xml": { + "source": "iana", + "compressible": true + }, + "application/samlmetadata+xml": { + "source": "iana", + "compressible": true + }, + "application/sbml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["sbml"] + }, + "application/scaip+xml": { + "source": "iana", + "compressible": true + }, + "application/scim+json": { + "source": "iana", + "compressible": true + }, + "application/scvp-cv-request": { + "source": "iana", + "extensions": ["scq"] + }, + "application/scvp-cv-response": { + "source": "iana", + "extensions": ["scs"] + }, + "application/scvp-vp-request": { + "source": "iana", + "extensions": ["spq"] + }, + "application/scvp-vp-response": { + "source": "iana", + "extensions": ["spp"] + }, + "application/sdp": { + "source": "iana", + "extensions": ["sdp"] + }, + "application/secevent+jwt": { + "source": "iana" + }, + "application/senml+cbor": { + "source": "iana" + }, + "application/senml+json": { + "source": "iana", + "compressible": true + }, + "application/senml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["senmlx"] + }, + "application/senml-exi": { + "source": "iana" + }, + "application/sensml+cbor": { + "source": "iana" + }, + "application/sensml+json": { + "source": "iana", + "compressible": true + }, + "application/sensml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["sensmlx"] + }, + "application/sensml-exi": { + "source": "iana" + }, + "application/sep+xml": { + "source": "iana", + "compressible": true + }, + "application/sep-exi": { + "source": "iana" + }, + "application/session-info": { + "source": "iana" + }, + "application/set-payment": { + "source": "iana" + }, + "application/set-payment-initiation": { + "source": "iana", + "extensions": ["setpay"] + }, + "application/set-registration": { + "source": "iana" + }, + "application/set-registration-initiation": { + "source": "iana", + "extensions": ["setreg"] + }, + "application/sgml": { + "source": "iana" + }, + "application/sgml-open-catalog": { + "source": "iana" + }, + "application/shf+xml": { + "source": "iana", + "compressible": true, + "extensions": ["shf"] + }, + "application/sieve": { + "source": "iana", + "extensions": ["siv","sieve"] + }, + "application/simple-filter+xml": { + "source": "iana", + "compressible": true + }, + "application/simple-message-summary": { + "source": "iana" + }, + "application/simplesymbolcontainer": { + "source": "iana" + }, + "application/sipc": { + "source": "iana" + }, + "application/slate": { + "source": "iana" + }, + "application/smil": { + "source": "iana" + }, + "application/smil+xml": { + "source": "iana", + "compressible": true, + "extensions": ["smi","smil"] + }, + "application/smpte336m": { + "source": "iana" + }, + "application/soap+fastinfoset": { + "source": "iana" + }, + "application/soap+xml": { + "source": "iana", + "compressible": true + }, + "application/sparql-query": { + "source": "iana", + "extensions": ["rq"] + }, + "application/sparql-results+xml": { + "source": "iana", + "compressible": true, + "extensions": ["srx"] + }, + "application/spirits-event+xml": { + "source": "iana", + "compressible": true + }, + "application/sql": { + "source": "iana" + }, + "application/srgs": { + "source": "iana", + "extensions": ["gram"] + }, + "application/srgs+xml": { + "source": "iana", + "compressible": true, + "extensions": ["grxml"] + }, + "application/sru+xml": { + "source": "iana", + "compressible": true, + "extensions": ["sru"] + }, + "application/ssdl+xml": { + "source": "apache", + "compressible": true, + "extensions": ["ssdl"] + }, + "application/ssml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["ssml"] + }, + "application/stix+json": { + "source": "iana", + "compressible": true + }, + "application/swid+xml": { + "source": "iana", + "compressible": true, + "extensions": ["swidtag"] + }, + "application/tamp-apex-update": { + "source": "iana" + }, + "application/tamp-apex-update-confirm": { + "source": "iana" + }, + "application/tamp-community-update": { + "source": "iana" + }, + "application/tamp-community-update-confirm": { + "source": "iana" + }, + "application/tamp-error": { + "source": "iana" + }, + "application/tamp-sequence-adjust": { + "source": "iana" + }, + "application/tamp-sequence-adjust-confirm": { + "source": "iana" + }, + "application/tamp-status-query": { + "source": "iana" + }, + "application/tamp-status-response": { + "source": "iana" + }, + "application/tamp-update": { + "source": "iana" + }, + "application/tamp-update-confirm": { + "source": "iana" + }, + "application/tar": { + "compressible": true + }, + "application/taxii+json": { + "source": "iana", + "compressible": true + }, + "application/tei+xml": { + "source": "iana", + "compressible": true, + "extensions": ["tei","teicorpus"] + }, + "application/tetra_isi": { + "source": "iana" + }, + "application/thraud+xml": { + "source": "iana", + "compressible": true, + "extensions": ["tfi"] + }, + "application/timestamp-query": { + "source": "iana" + }, + "application/timestamp-reply": { + "source": "iana" + }, + "application/timestamped-data": { + "source": "iana", + "extensions": ["tsd"] + }, + "application/tlsrpt+gzip": { + "source": "iana" + }, + "application/tlsrpt+json": { + "source": "iana", + "compressible": true + }, + "application/tnauthlist": { + "source": "iana" + }, + "application/toml": { + "compressible": true, + "extensions": ["toml"] + }, + "application/trickle-ice-sdpfrag": { + "source": "iana" + }, + "application/trig": { + "source": "iana" + }, + "application/ttml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["ttml"] + }, + "application/tve-trigger": { + "source": "iana" + }, + "application/tzif": { + "source": "iana" + }, + "application/tzif-leap": { + "source": "iana" + }, + "application/ulpfec": { + "source": "iana" + }, + "application/urc-grpsheet+xml": { + "source": "iana", + "compressible": true + }, + "application/urc-ressheet+xml": { + "source": "iana", + "compressible": true, + "extensions": ["rsheet"] + }, + "application/urc-targetdesc+xml": { + "source": "iana", + "compressible": true + }, + "application/urc-uisocketdesc+xml": { + "source": "iana", + "compressible": true + }, + "application/vcard+json": { + "source": "iana", + "compressible": true + }, + "application/vcard+xml": { + "source": "iana", + "compressible": true + }, + "application/vemmi": { + "source": "iana" + }, + "application/vividence.scriptfile": { + "source": "apache" + }, + "application/vnd.1000minds.decision-model+xml": { + "source": "iana", + "compressible": true, + "extensions": ["1km"] + }, + "application/vnd.3gpp-prose+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp-prose-pc3ch+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp-v2x-local-service-information": { + "source": "iana" + }, + "application/vnd.3gpp.access-transfer-events+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.bsf+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.gmop+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mc-signalling-ear": { + "source": "iana" + }, + "application/vnd.3gpp.mcdata-affiliation-command+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcdata-info+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcdata-payload": { + "source": "iana" + }, + "application/vnd.3gpp.mcdata-service-config+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcdata-signalling": { + "source": "iana" + }, + "application/vnd.3gpp.mcdata-ue-config+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcdata-user-profile+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcptt-affiliation-command+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcptt-floor-request+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcptt-info+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcptt-location-info+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcptt-mbms-usage-info+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcptt-service-config+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcptt-signed+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcptt-ue-config+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcptt-ue-init-config+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcptt-user-profile+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcvideo-affiliation-command+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcvideo-affiliation-info+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcvideo-info+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcvideo-location-info+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcvideo-mbms-usage-info+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcvideo-service-config+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcvideo-transmission-request+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcvideo-ue-config+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mcvideo-user-profile+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.mid-call+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.pic-bw-large": { + "source": "iana", + "extensions": ["plb"] + }, + "application/vnd.3gpp.pic-bw-small": { + "source": "iana", + "extensions": ["psb"] + }, + "application/vnd.3gpp.pic-bw-var": { + "source": "iana", + "extensions": ["pvb"] + }, + "application/vnd.3gpp.sms": { + "source": "iana" + }, + "application/vnd.3gpp.sms+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.srvcc-ext+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.srvcc-info+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.state-and-event-info+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp.ussd+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp2.bcmcsinfo+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.3gpp2.sms": { + "source": "iana" + }, + "application/vnd.3gpp2.tcap": { + "source": "iana", + "extensions": ["tcap"] + }, + "application/vnd.3lightssoftware.imagescal": { + "source": "iana" + }, + "application/vnd.3m.post-it-notes": { + "source": "iana", + "extensions": ["pwn"] + }, + "application/vnd.accpac.simply.aso": { + "source": "iana", + "extensions": ["aso"] + }, + "application/vnd.accpac.simply.imp": { + "source": "iana", + "extensions": ["imp"] + }, + "application/vnd.acucobol": { + "source": "iana", + "extensions": ["acu"] + }, + "application/vnd.acucorp": { + "source": "iana", + "extensions": ["atc","acutc"] + }, + "application/vnd.adobe.air-application-installer-package+zip": { + "source": "apache", + "compressible": false, + "extensions": ["air"] + }, + "application/vnd.adobe.flash.movie": { + "source": "iana" + }, + "application/vnd.adobe.formscentral.fcdt": { + "source": "iana", + "extensions": ["fcdt"] + }, + "application/vnd.adobe.fxp": { + "source": "iana", + "extensions": ["fxp","fxpl"] + }, + "application/vnd.adobe.partial-upload": { + "source": "iana" + }, + "application/vnd.adobe.xdp+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xdp"] + }, + "application/vnd.adobe.xfdf": { + "source": "iana", + "extensions": ["xfdf"] + }, + "application/vnd.aether.imp": { + "source": "iana" + }, + "application/vnd.afpc.afplinedata": { + "source": "iana" + }, + "application/vnd.afpc.afplinedata-pagedef": { + "source": "iana" + }, + "application/vnd.afpc.foca-charset": { + "source": "iana" + }, + "application/vnd.afpc.foca-codedfont": { + "source": "iana" + }, + "application/vnd.afpc.foca-codepage": { + "source": "iana" + }, + "application/vnd.afpc.modca": { + "source": "iana" + }, + "application/vnd.afpc.modca-formdef": { + "source": "iana" + }, + "application/vnd.afpc.modca-mediummap": { + "source": "iana" + }, + "application/vnd.afpc.modca-objectcontainer": { + "source": "iana" + }, + "application/vnd.afpc.modca-overlay": { + "source": "iana" + }, + "application/vnd.afpc.modca-pagesegment": { + "source": "iana" + }, + "application/vnd.ah-barcode": { + "source": "iana" + }, + "application/vnd.ahead.space": { + "source": "iana", + "extensions": ["ahead"] + }, + "application/vnd.airzip.filesecure.azf": { + "source": "iana", + "extensions": ["azf"] + }, + "application/vnd.airzip.filesecure.azs": { + "source": "iana", + "extensions": ["azs"] + }, + "application/vnd.amadeus+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.amazon.ebook": { + "source": "apache", + "extensions": ["azw"] + }, + "application/vnd.amazon.mobi8-ebook": { + "source": "iana" + }, + "application/vnd.americandynamics.acc": { + "source": "iana", + "extensions": ["acc"] + }, + "application/vnd.amiga.ami": { + "source": "iana", + "extensions": ["ami"] + }, + "application/vnd.amundsen.maze+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.android.ota": { + "source": "iana" + }, + "application/vnd.android.package-archive": { + "source": "apache", + "compressible": false, + "extensions": ["apk"] + }, + "application/vnd.anki": { + "source": "iana" + }, + "application/vnd.anser-web-certificate-issue-initiation": { + "source": "iana", + "extensions": ["cii"] + }, + "application/vnd.anser-web-funds-transfer-initiation": { + "source": "apache", + "extensions": ["fti"] + }, + "application/vnd.antix.game-component": { + "source": "iana", + "extensions": ["atx"] + }, + "application/vnd.apache.thrift.binary": { + "source": "iana" + }, + "application/vnd.apache.thrift.compact": { + "source": "iana" + }, + "application/vnd.apache.thrift.json": { + "source": "iana" + }, + "application/vnd.api+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.aplextor.warrp+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.apothekende.reservation+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.apple.installer+xml": { + "source": "iana", + "compressible": true, + "extensions": ["mpkg"] + }, + "application/vnd.apple.keynote": { + "source": "iana", + "extensions": ["keynote"] + }, + "application/vnd.apple.mpegurl": { + "source": "iana", + "extensions": ["m3u8"] + }, + "application/vnd.apple.numbers": { + "source": "iana", + "extensions": ["numbers"] + }, + "application/vnd.apple.pages": { + "source": "iana", + "extensions": ["pages"] + }, + "application/vnd.apple.pkpass": { + "compressible": false, + "extensions": ["pkpass"] + }, + "application/vnd.arastra.swi": { + "source": "iana" + }, + "application/vnd.aristanetworks.swi": { + "source": "iana", + "extensions": ["swi"] + }, + "application/vnd.artisan+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.artsquare": { + "source": "iana" + }, + "application/vnd.astraea-software.iota": { + "source": "iana", + "extensions": ["iota"] + }, + "application/vnd.audiograph": { + "source": "iana", + "extensions": ["aep"] + }, + "application/vnd.autopackage": { + "source": "iana" + }, + "application/vnd.avalon+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.avistar+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.balsamiq.bmml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["bmml"] + }, + "application/vnd.balsamiq.bmpr": { + "source": "iana" + }, + "application/vnd.banana-accounting": { + "source": "iana" + }, + "application/vnd.bbf.usp.error": { + "source": "iana" + }, + "application/vnd.bbf.usp.msg": { + "source": "iana" + }, + "application/vnd.bbf.usp.msg+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.bekitzur-stech+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.bint.med-content": { + "source": "iana" + }, + "application/vnd.biopax.rdf+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.blink-idb-value-wrapper": { + "source": "iana" + }, + "application/vnd.blueice.multipass": { + "source": "iana", + "extensions": ["mpm"] + }, + "application/vnd.bluetooth.ep.oob": { + "source": "iana" + }, + "application/vnd.bluetooth.le.oob": { + "source": "iana" + }, + "application/vnd.bmi": { + "source": "iana", + "extensions": ["bmi"] + }, + "application/vnd.bpf": { + "source": "iana" + }, + "application/vnd.bpf3": { + "source": "iana" + }, + "application/vnd.businessobjects": { + "source": "iana", + "extensions": ["rep"] + }, + "application/vnd.byu.uapi+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.cab-jscript": { + "source": "iana" + }, + "application/vnd.canon-cpdl": { + "source": "iana" + }, + "application/vnd.canon-lips": { + "source": "iana" + }, + "application/vnd.capasystems-pg+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.cendio.thinlinc.clientconf": { + "source": "iana" + }, + "application/vnd.century-systems.tcp_stream": { + "source": "iana" + }, + "application/vnd.chemdraw+xml": { + "source": "iana", + "compressible": true, + "extensions": ["cdxml"] + }, + "application/vnd.chess-pgn": { + "source": "iana" + }, + "application/vnd.chipnuts.karaoke-mmd": { + "source": "iana", + "extensions": ["mmd"] + }, + "application/vnd.ciedi": { + "source": "iana" + }, + "application/vnd.cinderella": { + "source": "iana", + "extensions": ["cdy"] + }, + "application/vnd.cirpack.isdn-ext": { + "source": "iana" + }, + "application/vnd.citationstyles.style+xml": { + "source": "iana", + "compressible": true, + "extensions": ["csl"] + }, + "application/vnd.claymore": { + "source": "iana", + "extensions": ["cla"] + }, + "application/vnd.cloanto.rp9": { + "source": "iana", + "extensions": ["rp9"] + }, + "application/vnd.clonk.c4group": { + "source": "iana", + "extensions": ["c4g","c4d","c4f","c4p","c4u"] + }, + "application/vnd.cluetrust.cartomobile-config": { + "source": "iana", + "extensions": ["c11amc"] + }, + "application/vnd.cluetrust.cartomobile-config-pkg": { + "source": "iana", + "extensions": ["c11amz"] + }, + "application/vnd.coffeescript": { + "source": "iana" + }, + "application/vnd.collabio.xodocuments.document": { + "source": "iana" + }, + "application/vnd.collabio.xodocuments.document-template": { + "source": "iana" + }, + "application/vnd.collabio.xodocuments.presentation": { + "source": "iana" + }, + "application/vnd.collabio.xodocuments.presentation-template": { + "source": "iana" + }, + "application/vnd.collabio.xodocuments.spreadsheet": { + "source": "iana" + }, + "application/vnd.collabio.xodocuments.spreadsheet-template": { + "source": "iana" + }, + "application/vnd.collection+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.collection.doc+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.collection.next+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.comicbook+zip": { + "source": "iana", + "compressible": false + }, + "application/vnd.comicbook-rar": { + "source": "iana" + }, + "application/vnd.commerce-battelle": { + "source": "iana" + }, + "application/vnd.commonspace": { + "source": "iana", + "extensions": ["csp"] + }, + "application/vnd.contact.cmsg": { + "source": "iana", + "extensions": ["cdbcmsg"] + }, + "application/vnd.coreos.ignition+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.cosmocaller": { + "source": "iana", + "extensions": ["cmc"] + }, + "application/vnd.crick.clicker": { + "source": "iana", + "extensions": ["clkx"] + }, + "application/vnd.crick.clicker.keyboard": { + "source": "iana", + "extensions": ["clkk"] + }, + "application/vnd.crick.clicker.palette": { + "source": "iana", + "extensions": ["clkp"] + }, + "application/vnd.crick.clicker.template": { + "source": "iana", + "extensions": ["clkt"] + }, + "application/vnd.crick.clicker.wordbank": { + "source": "iana", + "extensions": ["clkw"] + }, + "application/vnd.criticaltools.wbs+xml": { + "source": "iana", + "compressible": true, + "extensions": ["wbs"] + }, + "application/vnd.cryptii.pipe+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.crypto-shade-file": { + "source": "iana" + }, + "application/vnd.ctc-posml": { + "source": "iana", + "extensions": ["pml"] + }, + "application/vnd.ctct.ws+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.cups-pdf": { + "source": "iana" + }, + "application/vnd.cups-postscript": { + "source": "iana" + }, + "application/vnd.cups-ppd": { + "source": "iana", + "extensions": ["ppd"] + }, + "application/vnd.cups-raster": { + "source": "iana" + }, + "application/vnd.cups-raw": { + "source": "iana" + }, + "application/vnd.curl": { + "source": "iana" + }, + "application/vnd.curl.car": { + "source": "apache", + "extensions": ["car"] + }, + "application/vnd.curl.pcurl": { + "source": "apache", + "extensions": ["pcurl"] + }, + "application/vnd.cyan.dean.root+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.cybank": { + "source": "iana" + }, + "application/vnd.d2l.coursepackage1p0+zip": { + "source": "iana", + "compressible": false + }, + "application/vnd.dart": { + "source": "iana", + "compressible": true, + "extensions": ["dart"] + }, + "application/vnd.data-vision.rdz": { + "source": "iana", + "extensions": ["rdz"] + }, + "application/vnd.datapackage+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.dataresource+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.debian.binary-package": { + "source": "iana" + }, + "application/vnd.dece.data": { + "source": "iana", + "extensions": ["uvf","uvvf","uvd","uvvd"] + }, + "application/vnd.dece.ttml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["uvt","uvvt"] + }, + "application/vnd.dece.unspecified": { + "source": "iana", + "extensions": ["uvx","uvvx"] + }, + "application/vnd.dece.zip": { + "source": "iana", + "extensions": ["uvz","uvvz"] + }, + "application/vnd.denovo.fcselayout-link": { + "source": "iana", + "extensions": ["fe_launch"] + }, + "application/vnd.desmume.movie": { + "source": "iana" + }, + "application/vnd.dir-bi.plate-dl-nosuffix": { + "source": "iana" + }, + "application/vnd.dm.delegation+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.dna": { + "source": "iana", + "extensions": ["dna"] + }, + "application/vnd.document+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.dolby.mlp": { + "source": "apache", + "extensions": ["mlp"] + }, + "application/vnd.dolby.mobile.1": { + "source": "iana" + }, + "application/vnd.dolby.mobile.2": { + "source": "iana" + }, + "application/vnd.doremir.scorecloud-binary-document": { + "source": "iana" + }, + "application/vnd.dpgraph": { + "source": "iana", + "extensions": ["dpg"] + }, + "application/vnd.dreamfactory": { + "source": "iana", + "extensions": ["dfac"] + }, + "application/vnd.drive+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.ds-keypoint": { + "source": "apache", + "extensions": ["kpxx"] + }, + "application/vnd.dtg.local": { + "source": "iana" + }, + "application/vnd.dtg.local.flash": { + "source": "iana" + }, + "application/vnd.dtg.local.html": { + "source": "iana" + }, + "application/vnd.dvb.ait": { + "source": "iana", + "extensions": ["ait"] + }, + "application/vnd.dvb.dvbj": { + "source": "iana" + }, + "application/vnd.dvb.esgcontainer": { + "source": "iana" + }, + "application/vnd.dvb.ipdcdftnotifaccess": { + "source": "iana" + }, + "application/vnd.dvb.ipdcesgaccess": { + "source": "iana" + }, + "application/vnd.dvb.ipdcesgaccess2": { + "source": "iana" + }, + "application/vnd.dvb.ipdcesgpdd": { + "source": "iana" + }, + "application/vnd.dvb.ipdcroaming": { + "source": "iana" + }, + "application/vnd.dvb.iptv.alfec-base": { + "source": "iana" + }, + "application/vnd.dvb.iptv.alfec-enhancement": { + "source": "iana" + }, + "application/vnd.dvb.notif-aggregate-root+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.dvb.notif-container+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.dvb.notif-generic+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.dvb.notif-ia-msglist+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.dvb.notif-ia-registration-request+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.dvb.notif-ia-registration-response+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.dvb.notif-init+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.dvb.pfr": { + "source": "iana" + }, + "application/vnd.dvb.service": { + "source": "iana", + "extensions": ["svc"] + }, + "application/vnd.dxr": { + "source": "iana" + }, + "application/vnd.dynageo": { + "source": "iana", + "extensions": ["geo"] + }, + "application/vnd.dzr": { + "source": "iana" + }, + "application/vnd.easykaraoke.cdgdownload": { + "source": "iana" + }, + "application/vnd.ecdis-update": { + "source": "iana" + }, + "application/vnd.ecip.rlp": { + "source": "iana" + }, + "application/vnd.ecowin.chart": { + "source": "iana", + "extensions": ["mag"] + }, + "application/vnd.ecowin.filerequest": { + "source": "iana" + }, + "application/vnd.ecowin.fileupdate": { + "source": "iana" + }, + "application/vnd.ecowin.series": { + "source": "iana" + }, + "application/vnd.ecowin.seriesrequest": { + "source": "iana" + }, + "application/vnd.ecowin.seriesupdate": { + "source": "iana" + }, + "application/vnd.efi.img": { + "source": "iana" + }, + "application/vnd.efi.iso": { + "source": "iana" + }, + "application/vnd.emclient.accessrequest+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.enliven": { + "source": "iana", + "extensions": ["nml"] + }, + "application/vnd.enphase.envoy": { + "source": "iana" + }, + "application/vnd.eprints.data+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.epson.esf": { + "source": "iana", + "extensions": ["esf"] + }, + "application/vnd.epson.msf": { + "source": "iana", + "extensions": ["msf"] + }, + "application/vnd.epson.quickanime": { + "source": "iana", + "extensions": ["qam"] + }, + "application/vnd.epson.salt": { + "source": "iana", + "extensions": ["slt"] + }, + "application/vnd.epson.ssf": { + "source": "iana", + "extensions": ["ssf"] + }, + "application/vnd.ericsson.quickcall": { + "source": "iana" + }, + "application/vnd.espass-espass+zip": { + "source": "iana", + "compressible": false + }, + "application/vnd.eszigno3+xml": { + "source": "iana", + "compressible": true, + "extensions": ["es3","et3"] + }, + "application/vnd.etsi.aoc+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.asic-e+zip": { + "source": "iana", + "compressible": false + }, + "application/vnd.etsi.asic-s+zip": { + "source": "iana", + "compressible": false + }, + "application/vnd.etsi.cug+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.iptvcommand+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.iptvdiscovery+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.iptvprofile+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.iptvsad-bc+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.iptvsad-cod+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.iptvsad-npvr+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.iptvservice+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.iptvsync+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.iptvueprofile+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.mcid+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.mheg5": { + "source": "iana" + }, + "application/vnd.etsi.overload-control-policy-dataset+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.pstn+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.sci+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.simservs+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.timestamp-token": { + "source": "iana" + }, + "application/vnd.etsi.tsl+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.etsi.tsl.der": { + "source": "iana" + }, + "application/vnd.eudora.data": { + "source": "iana" + }, + "application/vnd.evolv.ecig.profile": { + "source": "iana" + }, + "application/vnd.evolv.ecig.settings": { + "source": "iana" + }, + "application/vnd.evolv.ecig.theme": { + "source": "iana" + }, + "application/vnd.exstream-empower+zip": { + "source": "iana", + "compressible": false + }, + "application/vnd.exstream-package": { + "source": "iana" + }, + "application/vnd.ezpix-album": { + "source": "iana", + "extensions": ["ez2"] + }, + "application/vnd.ezpix-package": { + "source": "iana", + "extensions": ["ez3"] + }, + "application/vnd.f-secure.mobile": { + "source": "iana" + }, + "application/vnd.fastcopy-disk-image": { + "source": "iana" + }, + "application/vnd.fdf": { + "source": "iana", + "extensions": ["fdf"] + }, + "application/vnd.fdsn.mseed": { + "source": "iana", + "extensions": ["mseed"] + }, + "application/vnd.fdsn.seed": { + "source": "iana", + "extensions": ["seed","dataless"] + }, + "application/vnd.ffsns": { + "source": "iana" + }, + "application/vnd.ficlab.flb+zip": { + "source": "iana", + "compressible": false + }, + "application/vnd.filmit.zfc": { + "source": "iana" + }, + "application/vnd.fints": { + "source": "iana" + }, + "application/vnd.firemonkeys.cloudcell": { + "source": "iana" + }, + "application/vnd.flographit": { + "source": "iana", + "extensions": ["gph"] + }, + "application/vnd.fluxtime.clip": { + "source": "iana", + "extensions": ["ftc"] + }, + "application/vnd.font-fontforge-sfd": { + "source": "iana" + }, + "application/vnd.framemaker": { + "source": "iana", + "extensions": ["fm","frame","maker","book"] + }, + "application/vnd.frogans.fnc": { + "source": "iana", + "extensions": ["fnc"] + }, + "application/vnd.frogans.ltf": { + "source": "iana", + "extensions": ["ltf"] + }, + "application/vnd.fsc.weblaunch": { + "source": "iana", + "extensions": ["fsc"] + }, + "application/vnd.fujitsu.oasys": { + "source": "iana", + "extensions": ["oas"] + }, + "application/vnd.fujitsu.oasys2": { + "source": "iana", + "extensions": ["oa2"] + }, + "application/vnd.fujitsu.oasys3": { + "source": "iana", + "extensions": ["oa3"] + }, + "application/vnd.fujitsu.oasysgp": { + "source": "iana", + "extensions": ["fg5"] + }, + "application/vnd.fujitsu.oasysprs": { + "source": "iana", + "extensions": ["bh2"] + }, + "application/vnd.fujixerox.art-ex": { + "source": "iana" + }, + "application/vnd.fujixerox.art4": { + "source": "iana" + }, + "application/vnd.fujixerox.ddd": { + "source": "iana", + "extensions": ["ddd"] + }, + "application/vnd.fujixerox.docuworks": { + "source": "iana", + "extensions": ["xdw"] + }, + "application/vnd.fujixerox.docuworks.binder": { + "source": "iana", + "extensions": ["xbd"] + }, + "application/vnd.fujixerox.docuworks.container": { + "source": "iana" + }, + "application/vnd.fujixerox.hbpl": { + "source": "iana" + }, + "application/vnd.fut-misnet": { + "source": "iana" + }, + "application/vnd.futoin+cbor": { + "source": "iana" + }, + "application/vnd.futoin+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.fuzzysheet": { + "source": "iana", + "extensions": ["fzs"] + }, + "application/vnd.genomatix.tuxedo": { + "source": "iana", + "extensions": ["txd"] + }, + "application/vnd.gentics.grd+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.geo+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.geocube+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.geogebra.file": { + "source": "iana", + "extensions": ["ggb"] + }, + "application/vnd.geogebra.tool": { + "source": "iana", + "extensions": ["ggt"] + }, + "application/vnd.geometry-explorer": { + "source": "iana", + "extensions": ["gex","gre"] + }, + "application/vnd.geonext": { + "source": "iana", + "extensions": ["gxt"] + }, + "application/vnd.geoplan": { + "source": "iana", + "extensions": ["g2w"] + }, + "application/vnd.geospace": { + "source": "iana", + "extensions": ["g3w"] + }, + "application/vnd.gerber": { + "source": "iana" + }, + "application/vnd.globalplatform.card-content-mgt": { + "source": "iana" + }, + "application/vnd.globalplatform.card-content-mgt-response": { + "source": "iana" + }, + "application/vnd.gmx": { + "source": "iana", + "extensions": ["gmx"] + }, + "application/vnd.google-apps.document": { + "compressible": false, + "extensions": ["gdoc"] + }, + "application/vnd.google-apps.presentation": { + "compressible": false, + "extensions": ["gslides"] + }, + "application/vnd.google-apps.spreadsheet": { + "compressible": false, + "extensions": ["gsheet"] + }, + "application/vnd.google-earth.kml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["kml"] + }, + "application/vnd.google-earth.kmz": { + "source": "iana", + "compressible": false, + "extensions": ["kmz"] + }, + "application/vnd.gov.sk.e-form+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.gov.sk.e-form+zip": { + "source": "iana", + "compressible": false + }, + "application/vnd.gov.sk.xmldatacontainer+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.grafeq": { + "source": "iana", + "extensions": ["gqf","gqs"] + }, + "application/vnd.gridmp": { + "source": "iana" + }, + "application/vnd.groove-account": { + "source": "iana", + "extensions": ["gac"] + }, + "application/vnd.groove-help": { + "source": "iana", + "extensions": ["ghf"] + }, + "application/vnd.groove-identity-message": { + "source": "iana", + "extensions": ["gim"] + }, + "application/vnd.groove-injector": { + "source": "iana", + "extensions": ["grv"] + }, + "application/vnd.groove-tool-message": { + "source": "iana", + "extensions": ["gtm"] + }, + "application/vnd.groove-tool-template": { + "source": "iana", + "extensions": ["tpl"] + }, + "application/vnd.groove-vcard": { + "source": "iana", + "extensions": ["vcg"] + }, + "application/vnd.hal+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.hal+xml": { + "source": "iana", + "compressible": true, + "extensions": ["hal"] + }, + "application/vnd.handheld-entertainment+xml": { + "source": "iana", + "compressible": true, + "extensions": ["zmm"] + }, + "application/vnd.hbci": { + "source": "iana", + "extensions": ["hbci"] + }, + "application/vnd.hc+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.hcl-bireports": { + "source": "iana" + }, + "application/vnd.hdt": { + "source": "iana" + }, + "application/vnd.heroku+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.hhe.lesson-player": { + "source": "iana", + "extensions": ["les"] + }, + "application/vnd.hp-hpgl": { + "source": "iana", + "extensions": ["hpgl"] + }, + "application/vnd.hp-hpid": { + "source": "iana", + "extensions": ["hpid"] + }, + "application/vnd.hp-hps": { + "source": "iana", + "extensions": ["hps"] + }, + "application/vnd.hp-jlyt": { + "source": "iana", + "extensions": ["jlt"] + }, + "application/vnd.hp-pcl": { + "source": "iana", + "extensions": ["pcl"] + }, + "application/vnd.hp-pclxl": { + "source": "iana", + "extensions": ["pclxl"] + }, + "application/vnd.httphone": { + "source": "iana" + }, + "application/vnd.hydrostatix.sof-data": { + "source": "iana", + "extensions": ["sfd-hdstx"] + }, + "application/vnd.hyper+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.hyper-item+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.hyperdrive+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.hzn-3d-crossword": { + "source": "iana" + }, + "application/vnd.ibm.afplinedata": { + "source": "iana" + }, + "application/vnd.ibm.electronic-media": { + "source": "iana" + }, + "application/vnd.ibm.minipay": { + "source": "iana", + "extensions": ["mpy"] + }, + "application/vnd.ibm.modcap": { + "source": "iana", + "extensions": ["afp","listafp","list3820"] + }, + "application/vnd.ibm.rights-management": { + "source": "iana", + "extensions": ["irm"] + }, + "application/vnd.ibm.secure-container": { + "source": "iana", + "extensions": ["sc"] + }, + "application/vnd.iccprofile": { + "source": "iana", + "extensions": ["icc","icm"] + }, + "application/vnd.ieee.1905": { + "source": "iana" + }, + "application/vnd.igloader": { + "source": "iana", + "extensions": ["igl"] + }, + "application/vnd.imagemeter.folder+zip": { + "source": "iana", + "compressible": false + }, + "application/vnd.imagemeter.image+zip": { + "source": "iana", + "compressible": false + }, + "application/vnd.immervision-ivp": { + "source": "iana", + "extensions": ["ivp"] + }, + "application/vnd.immervision-ivu": { + "source": "iana", + "extensions": ["ivu"] + }, + "application/vnd.ims.imsccv1p1": { + "source": "iana" + }, + "application/vnd.ims.imsccv1p2": { + "source": "iana" + }, + "application/vnd.ims.imsccv1p3": { + "source": "iana" + }, + "application/vnd.ims.lis.v2.result+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.ims.lti.v2.toolconsumerprofile+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.ims.lti.v2.toolproxy+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.ims.lti.v2.toolproxy.id+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.ims.lti.v2.toolsettings+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.ims.lti.v2.toolsettings.simple+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.informedcontrol.rms+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.informix-visionary": { + "source": "iana" + }, + "application/vnd.infotech.project": { + "source": "iana" + }, + "application/vnd.infotech.project+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.innopath.wamp.notification": { + "source": "iana" + }, + "application/vnd.insors.igm": { + "source": "iana", + "extensions": ["igm"] + }, + "application/vnd.intercon.formnet": { + "source": "iana", + "extensions": ["xpw","xpx"] + }, + "application/vnd.intergeo": { + "source": "iana", + "extensions": ["i2g"] + }, + "application/vnd.intertrust.digibox": { + "source": "iana" + }, + "application/vnd.intertrust.nncp": { + "source": "iana" + }, + "application/vnd.intu.qbo": { + "source": "iana", + "extensions": ["qbo"] + }, + "application/vnd.intu.qfx": { + "source": "iana", + "extensions": ["qfx"] + }, + "application/vnd.iptc.g2.catalogitem+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.iptc.g2.conceptitem+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.iptc.g2.knowledgeitem+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.iptc.g2.newsitem+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.iptc.g2.newsmessage+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.iptc.g2.packageitem+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.iptc.g2.planningitem+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.ipunplugged.rcprofile": { + "source": "iana", + "extensions": ["rcprofile"] + }, + "application/vnd.irepository.package+xml": { + "source": "iana", + "compressible": true, + "extensions": ["irp"] + }, + "application/vnd.is-xpr": { + "source": "iana", + "extensions": ["xpr"] + }, + "application/vnd.isac.fcs": { + "source": "iana", + "extensions": ["fcs"] + }, + "application/vnd.iso11783-10+zip": { + "source": "iana", + "compressible": false + }, + "application/vnd.jam": { + "source": "iana", + "extensions": ["jam"] + }, + "application/vnd.japannet-directory-service": { + "source": "iana" + }, + "application/vnd.japannet-jpnstore-wakeup": { + "source": "iana" + }, + "application/vnd.japannet-payment-wakeup": { + "source": "iana" + }, + "application/vnd.japannet-registration": { + "source": "iana" + }, + "application/vnd.japannet-registration-wakeup": { + "source": "iana" + }, + "application/vnd.japannet-setstore-wakeup": { + "source": "iana" + }, + "application/vnd.japannet-verification": { + "source": "iana" + }, + "application/vnd.japannet-verification-wakeup": { + "source": "iana" + }, + "application/vnd.jcp.javame.midlet-rms": { + "source": "iana", + "extensions": ["rms"] + }, + "application/vnd.jisp": { + "source": "iana", + "extensions": ["jisp"] + }, + "application/vnd.joost.joda-archive": { + "source": "iana", + "extensions": ["joda"] + }, + "application/vnd.jsk.isdn-ngn": { + "source": "iana" + }, + "application/vnd.kahootz": { + "source": "iana", + "extensions": ["ktz","ktr"] + }, + "application/vnd.kde.karbon": { + "source": "iana", + "extensions": ["karbon"] + }, + "application/vnd.kde.kchart": { + "source": "iana", + "extensions": ["chrt"] + }, + "application/vnd.kde.kformula": { + "source": "iana", + "extensions": ["kfo"] + }, + "application/vnd.kde.kivio": { + "source": "iana", + "extensions": ["flw"] + }, + "application/vnd.kde.kontour": { + "source": "iana", + "extensions": ["kon"] + }, + "application/vnd.kde.kpresenter": { + "source": "iana", + "extensions": ["kpr","kpt"] + }, + "application/vnd.kde.kspread": { + "source": "iana", + "extensions": ["ksp"] + }, + "application/vnd.kde.kword": { + "source": "iana", + "extensions": ["kwd","kwt"] + }, + "application/vnd.kenameaapp": { + "source": "iana", + "extensions": ["htke"] + }, + "application/vnd.kidspiration": { + "source": "iana", + "extensions": ["kia"] + }, + "application/vnd.kinar": { + "source": "iana", + "extensions": ["kne","knp"] + }, + "application/vnd.koan": { + "source": "iana", + "extensions": ["skp","skd","skt","skm"] + }, + "application/vnd.kodak-descriptor": { + "source": "iana", + "extensions": ["sse"] + }, + "application/vnd.las": { + "source": "iana" + }, + "application/vnd.las.las+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.las.las+xml": { + "source": "iana", + "compressible": true, + "extensions": ["lasxml"] + }, + "application/vnd.laszip": { + "source": "iana" + }, + "application/vnd.leap+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.liberty-request+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.llamagraphics.life-balance.desktop": { + "source": "iana", + "extensions": ["lbd"] + }, + "application/vnd.llamagraphics.life-balance.exchange+xml": { + "source": "iana", + "compressible": true, + "extensions": ["lbe"] + }, + "application/vnd.logipipe.circuit+zip": { + "source": "iana", + "compressible": false + }, + "application/vnd.loom": { + "source": "iana" + }, + "application/vnd.lotus-1-2-3": { + "source": "iana", + "extensions": ["123"] + }, + "application/vnd.lotus-approach": { + "source": "iana", + "extensions": ["apr"] + }, + "application/vnd.lotus-freelance": { + "source": "iana", + "extensions": ["pre"] + }, + "application/vnd.lotus-notes": { + "source": "iana", + "extensions": ["nsf"] + }, + "application/vnd.lotus-organizer": { + "source": "iana", + "extensions": ["org"] + }, + "application/vnd.lotus-screencam": { + "source": "iana", + "extensions": ["scm"] + }, + "application/vnd.lotus-wordpro": { + "source": "iana", + "extensions": ["lwp"] + }, + "application/vnd.macports.portpkg": { + "source": "iana", + "extensions": ["portpkg"] + }, + "application/vnd.mapbox-vector-tile": { + "source": "iana" + }, + "application/vnd.marlin.drm.actiontoken+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.marlin.drm.conftoken+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.marlin.drm.license+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.marlin.drm.mdcf": { + "source": "iana" + }, + "application/vnd.mason+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.maxmind.maxmind-db": { + "source": "iana" + }, + "application/vnd.mcd": { + "source": "iana", + "extensions": ["mcd"] + }, + "application/vnd.medcalcdata": { + "source": "iana", + "extensions": ["mc1"] + }, + "application/vnd.mediastation.cdkey": { + "source": "iana", + "extensions": ["cdkey"] + }, + "application/vnd.meridian-slingshot": { + "source": "iana" + }, + "application/vnd.mfer": { + "source": "iana", + "extensions": ["mwf"] + }, + "application/vnd.mfmp": { + "source": "iana", + "extensions": ["mfm"] + }, + "application/vnd.micro+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.micrografx.flo": { + "source": "iana", + "extensions": ["flo"] + }, + "application/vnd.micrografx.igx": { + "source": "iana", + "extensions": ["igx"] + }, + "application/vnd.microsoft.portable-executable": { + "source": "iana" + }, + "application/vnd.microsoft.windows.thumbnail-cache": { + "source": "iana" + }, + "application/vnd.miele+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.mif": { + "source": "iana", + "extensions": ["mif"] + }, + "application/vnd.minisoft-hp3000-save": { + "source": "iana" + }, + "application/vnd.mitsubishi.misty-guard.trustweb": { + "source": "iana" + }, + "application/vnd.mobius.daf": { + "source": "iana", + "extensions": ["daf"] + }, + "application/vnd.mobius.dis": { + "source": "iana", + "extensions": ["dis"] + }, + "application/vnd.mobius.mbk": { + "source": "iana", + "extensions": ["mbk"] + }, + "application/vnd.mobius.mqy": { + "source": "iana", + "extensions": ["mqy"] + }, + "application/vnd.mobius.msl": { + "source": "iana", + "extensions": ["msl"] + }, + "application/vnd.mobius.plc": { + "source": "iana", + "extensions": ["plc"] + }, + "application/vnd.mobius.txf": { + "source": "iana", + "extensions": ["txf"] + }, + "application/vnd.mophun.application": { + "source": "iana", + "extensions": ["mpn"] + }, + "application/vnd.mophun.certificate": { + "source": "iana", + "extensions": ["mpc"] + }, + "application/vnd.motorola.flexsuite": { + "source": "iana" + }, + "application/vnd.motorola.flexsuite.adsi": { + "source": "iana" + }, + "application/vnd.motorola.flexsuite.fis": { + "source": "iana" + }, + "application/vnd.motorola.flexsuite.gotap": { + "source": "iana" + }, + "application/vnd.motorola.flexsuite.kmr": { + "source": "iana" + }, + "application/vnd.motorola.flexsuite.ttc": { + "source": "iana" + }, + "application/vnd.motorola.flexsuite.wem": { + "source": "iana" + }, + "application/vnd.motorola.iprm": { + "source": "iana" + }, + "application/vnd.mozilla.xul+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xul"] + }, + "application/vnd.ms-3mfdocument": { + "source": "iana" + }, + "application/vnd.ms-artgalry": { + "source": "iana", + "extensions": ["cil"] + }, + "application/vnd.ms-asf": { + "source": "iana" + }, + "application/vnd.ms-cab-compressed": { + "source": "iana", + "extensions": ["cab"] + }, + "application/vnd.ms-color.iccprofile": { + "source": "apache" + }, + "application/vnd.ms-excel": { + "source": "iana", + "compressible": false, + "extensions": ["xls","xlm","xla","xlc","xlt","xlw"] + }, + "application/vnd.ms-excel.addin.macroenabled.12": { + "source": "iana", + "extensions": ["xlam"] + }, + "application/vnd.ms-excel.sheet.binary.macroenabled.12": { + "source": "iana", + "extensions": ["xlsb"] + }, + "application/vnd.ms-excel.sheet.macroenabled.12": { + "source": "iana", + "extensions": ["xlsm"] + }, + "application/vnd.ms-excel.template.macroenabled.12": { + "source": "iana", + "extensions": ["xltm"] + }, + "application/vnd.ms-fontobject": { + "source": "iana", + "compressible": true, + "extensions": ["eot"] + }, + "application/vnd.ms-htmlhelp": { + "source": "iana", + "extensions": ["chm"] + }, + "application/vnd.ms-ims": { + "source": "iana", + "extensions": ["ims"] + }, + "application/vnd.ms-lrm": { + "source": "iana", + "extensions": ["lrm"] + }, + "application/vnd.ms-office.activex+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.ms-officetheme": { + "source": "iana", + "extensions": ["thmx"] + }, + "application/vnd.ms-opentype": { + "source": "apache", + "compressible": true + }, + "application/vnd.ms-outlook": { + "compressible": false, + "extensions": ["msg"] + }, + "application/vnd.ms-package.obfuscated-opentype": { + "source": "apache" + }, + "application/vnd.ms-pki.seccat": { + "source": "apache", + "extensions": ["cat"] + }, + "application/vnd.ms-pki.stl": { + "source": "apache", + "extensions": ["stl"] + }, + "application/vnd.ms-playready.initiator+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.ms-powerpoint": { + "source": "iana", + "compressible": false, + "extensions": ["ppt","pps","pot"] + }, + "application/vnd.ms-powerpoint.addin.macroenabled.12": { + "source": "iana", + "extensions": ["ppam"] + }, + "application/vnd.ms-powerpoint.presentation.macroenabled.12": { + "source": "iana", + "extensions": ["pptm"] + }, + "application/vnd.ms-powerpoint.slide.macroenabled.12": { + "source": "iana", + "extensions": ["sldm"] + }, + "application/vnd.ms-powerpoint.slideshow.macroenabled.12": { + "source": "iana", + "extensions": ["ppsm"] + }, + "application/vnd.ms-powerpoint.template.macroenabled.12": { + "source": "iana", + "extensions": ["potm"] + }, + "application/vnd.ms-printdevicecapabilities+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.ms-printing.printticket+xml": { + "source": "apache", + "compressible": true + }, + "application/vnd.ms-printschematicket+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.ms-project": { + "source": "iana", + "extensions": ["mpp","mpt"] + }, + "application/vnd.ms-tnef": { + "source": "iana" + }, + "application/vnd.ms-windows.devicepairing": { + "source": "iana" + }, + "application/vnd.ms-windows.nwprinting.oob": { + "source": "iana" + }, + "application/vnd.ms-windows.printerpairing": { + "source": "iana" + }, + "application/vnd.ms-windows.wsd.oob": { + "source": "iana" + }, + "application/vnd.ms-wmdrm.lic-chlg-req": { + "source": "iana" + }, + "application/vnd.ms-wmdrm.lic-resp": { + "source": "iana" + }, + "application/vnd.ms-wmdrm.meter-chlg-req": { + "source": "iana" + }, + "application/vnd.ms-wmdrm.meter-resp": { + "source": "iana" + }, + "application/vnd.ms-word.document.macroenabled.12": { + "source": "iana", + "extensions": ["docm"] + }, + "application/vnd.ms-word.template.macroenabled.12": { + "source": "iana", + "extensions": ["dotm"] + }, + "application/vnd.ms-works": { + "source": "iana", + "extensions": ["wps","wks","wcm","wdb"] + }, + "application/vnd.ms-wpl": { + "source": "iana", + "extensions": ["wpl"] + }, + "application/vnd.ms-xpsdocument": { + "source": "iana", + "compressible": false, + "extensions": ["xps"] + }, + "application/vnd.msa-disk-image": { + "source": "iana" + }, + "application/vnd.mseq": { + "source": "iana", + "extensions": ["mseq"] + }, + "application/vnd.msign": { + "source": "iana" + }, + "application/vnd.multiad.creator": { + "source": "iana" + }, + "application/vnd.multiad.creator.cif": { + "source": "iana" + }, + "application/vnd.music-niff": { + "source": "iana" + }, + "application/vnd.musician": { + "source": "iana", + "extensions": ["mus"] + }, + "application/vnd.muvee.style": { + "source": "iana", + "extensions": ["msty"] + }, + "application/vnd.mynfc": { + "source": "iana", + "extensions": ["taglet"] + }, + "application/vnd.ncd.control": { + "source": "iana" + }, + "application/vnd.ncd.reference": { + "source": "iana" + }, + "application/vnd.nearst.inv+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.nervana": { + "source": "iana" + }, + "application/vnd.netfpx": { + "source": "iana" + }, + "application/vnd.neurolanguage.nlu": { + "source": "iana", + "extensions": ["nlu"] + }, + "application/vnd.nimn": { + "source": "iana" + }, + "application/vnd.nintendo.nitro.rom": { + "source": "iana" + }, + "application/vnd.nintendo.snes.rom": { + "source": "iana" + }, + "application/vnd.nitf": { + "source": "iana", + "extensions": ["ntf","nitf"] + }, + "application/vnd.noblenet-directory": { + "source": "iana", + "extensions": ["nnd"] + }, + "application/vnd.noblenet-sealer": { + "source": "iana", + "extensions": ["nns"] + }, + "application/vnd.noblenet-web": { + "source": "iana", + "extensions": ["nnw"] + }, + "application/vnd.nokia.catalogs": { + "source": "iana" + }, + "application/vnd.nokia.conml+wbxml": { + "source": "iana" + }, + "application/vnd.nokia.conml+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.nokia.iptv.config+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.nokia.isds-radio-presets": { + "source": "iana" + }, + "application/vnd.nokia.landmark+wbxml": { + "source": "iana" + }, + "application/vnd.nokia.landmark+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.nokia.landmarkcollection+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.nokia.n-gage.ac+xml": { + "source": "iana", + "compressible": true, + "extensions": ["ac"] + }, + "application/vnd.nokia.n-gage.data": { + "source": "iana", + "extensions": ["ngdat"] + }, + "application/vnd.nokia.n-gage.symbian.install": { + "source": "iana", + "extensions": ["n-gage"] + }, + "application/vnd.nokia.ncd": { + "source": "iana" + }, + "application/vnd.nokia.pcd+wbxml": { + "source": "iana" + }, + "application/vnd.nokia.pcd+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.nokia.radio-preset": { + "source": "iana", + "extensions": ["rpst"] + }, + "application/vnd.nokia.radio-presets": { + "source": "iana", + "extensions": ["rpss"] + }, + "application/vnd.novadigm.edm": { + "source": "iana", + "extensions": ["edm"] + }, + "application/vnd.novadigm.edx": { + "source": "iana", + "extensions": ["edx"] + }, + "application/vnd.novadigm.ext": { + "source": "iana", + "extensions": ["ext"] + }, + "application/vnd.ntt-local.content-share": { + "source": "iana" + }, + "application/vnd.ntt-local.file-transfer": { + "source": "iana" + }, + "application/vnd.ntt-local.ogw_remote-access": { + "source": "iana" + }, + "application/vnd.ntt-local.sip-ta_remote": { + "source": "iana" + }, + "application/vnd.ntt-local.sip-ta_tcp_stream": { + "source": "iana" + }, + "application/vnd.oasis.opendocument.chart": { + "source": "iana", + "extensions": ["odc"] + }, + "application/vnd.oasis.opendocument.chart-template": { + "source": "iana", + "extensions": ["otc"] + }, + "application/vnd.oasis.opendocument.database": { + "source": "iana", + "extensions": ["odb"] + }, + "application/vnd.oasis.opendocument.formula": { + "source": "iana", + "extensions": ["odf"] + }, + "application/vnd.oasis.opendocument.formula-template": { + "source": "iana", + "extensions": ["odft"] + }, + "application/vnd.oasis.opendocument.graphics": { + "source": "iana", + "compressible": false, + "extensions": ["odg"] + }, + "application/vnd.oasis.opendocument.graphics-template": { + "source": "iana", + "extensions": ["otg"] + }, + "application/vnd.oasis.opendocument.image": { + "source": "iana", + "extensions": ["odi"] + }, + "application/vnd.oasis.opendocument.image-template": { + "source": "iana", + "extensions": ["oti"] + }, + "application/vnd.oasis.opendocument.presentation": { + "source": "iana", + "compressible": false, + "extensions": ["odp"] + }, + "application/vnd.oasis.opendocument.presentation-template": { + "source": "iana", + "extensions": ["otp"] + }, + "application/vnd.oasis.opendocument.spreadsheet": { + "source": "iana", + "compressible": false, + "extensions": ["ods"] + }, + "application/vnd.oasis.opendocument.spreadsheet-template": { + "source": "iana", + "extensions": ["ots"] + }, + "application/vnd.oasis.opendocument.text": { + "source": "iana", + "compressible": false, + "extensions": ["odt"] + }, + "application/vnd.oasis.opendocument.text-master": { + "source": "iana", + "extensions": ["odm"] + }, + "application/vnd.oasis.opendocument.text-template": { + "source": "iana", + "extensions": ["ott"] + }, + "application/vnd.oasis.opendocument.text-web": { + "source": "iana", + "extensions": ["oth"] + }, + "application/vnd.obn": { + "source": "iana" + }, + "application/vnd.ocf+cbor": { + "source": "iana" + }, + "application/vnd.oftn.l10n+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.oipf.contentaccessdownload+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oipf.contentaccessstreaming+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oipf.cspg-hexbinary": { + "source": "iana" + }, + "application/vnd.oipf.dae.svg+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oipf.dae.xhtml+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oipf.mippvcontrolmessage+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oipf.pae.gem": { + "source": "iana" + }, + "application/vnd.oipf.spdiscovery+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oipf.spdlist+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oipf.ueprofile+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oipf.userprofile+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.olpc-sugar": { + "source": "iana", + "extensions": ["xo"] + }, + "application/vnd.oma-scws-config": { + "source": "iana" + }, + "application/vnd.oma-scws-http-request": { + "source": "iana" + }, + "application/vnd.oma-scws-http-response": { + "source": "iana" + }, + "application/vnd.oma.bcast.associated-procedure-parameter+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.bcast.drm-trigger+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.bcast.imd+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.bcast.ltkm": { + "source": "iana" + }, + "application/vnd.oma.bcast.notification+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.bcast.provisioningtrigger": { + "source": "iana" + }, + "application/vnd.oma.bcast.sgboot": { + "source": "iana" + }, + "application/vnd.oma.bcast.sgdd+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.bcast.sgdu": { + "source": "iana" + }, + "application/vnd.oma.bcast.simple-symbol-container": { + "source": "iana" + }, + "application/vnd.oma.bcast.smartcard-trigger+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.bcast.sprov+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.bcast.stkm": { + "source": "iana" + }, + "application/vnd.oma.cab-address-book+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.cab-feature-handler+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.cab-pcc+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.cab-subs-invite+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.cab-user-prefs+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.dcd": { + "source": "iana" + }, + "application/vnd.oma.dcdc": { + "source": "iana" + }, + "application/vnd.oma.dd2+xml": { + "source": "iana", + "compressible": true, + "extensions": ["dd2"] + }, + "application/vnd.oma.drm.risd+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.group-usage-list+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.lwm2m+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.lwm2m+tlv": { + "source": "iana" + }, + "application/vnd.oma.pal+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.poc.detailed-progress-report+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.poc.final-report+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.poc.groups+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.poc.invocation-descriptor+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.poc.optimized-progress-report+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.push": { + "source": "iana" + }, + "application/vnd.oma.scidm.messages+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oma.xcap-directory+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.omads-email+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.omads-file+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.omads-folder+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.omaloc-supl-init": { + "source": "iana" + }, + "application/vnd.onepager": { + "source": "iana" + }, + "application/vnd.onepagertamp": { + "source": "iana" + }, + "application/vnd.onepagertamx": { + "source": "iana" + }, + "application/vnd.onepagertat": { + "source": "iana" + }, + "application/vnd.onepagertatp": { + "source": "iana" + }, + "application/vnd.onepagertatx": { + "source": "iana" + }, + "application/vnd.openblox.game+xml": { + "source": "iana", + "compressible": true, + "extensions": ["obgx"] + }, + "application/vnd.openblox.game-binary": { + "source": "iana" + }, + "application/vnd.openeye.oeb": { + "source": "iana" + }, + "application/vnd.openofficeorg.extension": { + "source": "apache", + "extensions": ["oxt"] + }, + "application/vnd.openstreetmap.data+xml": { + "source": "iana", + "compressible": true, + "extensions": ["osm"] + }, + "application/vnd.openxmlformats-officedocument.custom-properties+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.customxmlproperties+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.drawing+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.drawingml.chart+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.extended-properties+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.comments+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.presentation": { + "source": "iana", + "compressible": false, + "extensions": ["pptx"] + }, + "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.presprops+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.slide": { + "source": "iana", + "extensions": ["sldx"] + }, + "application/vnd.openxmlformats-officedocument.presentationml.slide+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.slideshow": { + "source": "iana", + "extensions": ["ppsx"] + }, + "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.tags+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.template": { + "source": "iana", + "extensions": ["potx"] + }, + "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": { + "source": "iana", + "compressible": false, + "extensions": ["xlsx"] + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.template": { + "source": "iana", + "extensions": ["xltx"] + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.theme+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.themeoverride+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.vmldrawing": { + "source": "iana" + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": { + "source": "iana", + "compressible": false, + "extensions": ["docx"] + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.template": { + "source": "iana", + "extensions": ["dotx"] + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-package.core-properties+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.openxmlformats-package.relationships+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oracle.resource+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.orange.indata": { + "source": "iana" + }, + "application/vnd.osa.netdeploy": { + "source": "iana" + }, + "application/vnd.osgeo.mapguide.package": { + "source": "iana", + "extensions": ["mgp"] + }, + "application/vnd.osgi.bundle": { + "source": "iana" + }, + "application/vnd.osgi.dp": { + "source": "iana", + "extensions": ["dp"] + }, + "application/vnd.osgi.subsystem": { + "source": "iana", + "extensions": ["esa"] + }, + "application/vnd.otps.ct-kip+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.oxli.countgraph": { + "source": "iana" + }, + "application/vnd.pagerduty+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.palm": { + "source": "iana", + "extensions": ["pdb","pqa","oprc"] + }, + "application/vnd.panoply": { + "source": "iana" + }, + "application/vnd.paos.xml": { + "source": "iana" + }, + "application/vnd.patentdive": { + "source": "iana" + }, + "application/vnd.patientecommsdoc": { + "source": "iana" + }, + "application/vnd.pawaafile": { + "source": "iana", + "extensions": ["paw"] + }, + "application/vnd.pcos": { + "source": "iana" + }, + "application/vnd.pg.format": { + "source": "iana", + "extensions": ["str"] + }, + "application/vnd.pg.osasli": { + "source": "iana", + "extensions": ["ei6"] + }, + "application/vnd.piaccess.application-licence": { + "source": "iana" + }, + "application/vnd.picsel": { + "source": "iana", + "extensions": ["efif"] + }, + "application/vnd.pmi.widget": { + "source": "iana", + "extensions": ["wg"] + }, + "application/vnd.poc.group-advertisement+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.pocketlearn": { + "source": "iana", + "extensions": ["plf"] + }, + "application/vnd.powerbuilder6": { + "source": "iana", + "extensions": ["pbd"] + }, + "application/vnd.powerbuilder6-s": { + "source": "iana" + }, + "application/vnd.powerbuilder7": { + "source": "iana" + }, + "application/vnd.powerbuilder7-s": { + "source": "iana" + }, + "application/vnd.powerbuilder75": { + "source": "iana" + }, + "application/vnd.powerbuilder75-s": { + "source": "iana" + }, + "application/vnd.preminet": { + "source": "iana" + }, + "application/vnd.previewsystems.box": { + "source": "iana", + "extensions": ["box"] + }, + "application/vnd.proteus.magazine": { + "source": "iana", + "extensions": ["mgz"] + }, + "application/vnd.psfs": { + "source": "iana" + }, + "application/vnd.publishare-delta-tree": { + "source": "iana", + "extensions": ["qps"] + }, + "application/vnd.pvi.ptid1": { + "source": "iana", + "extensions": ["ptid"] + }, + "application/vnd.pwg-multiplexed": { + "source": "iana" + }, + "application/vnd.pwg-xhtml-print+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.qualcomm.brew-app-res": { + "source": "iana" + }, + "application/vnd.quarantainenet": { + "source": "iana" + }, + "application/vnd.quark.quarkxpress": { + "source": "iana", + "extensions": ["qxd","qxt","qwd","qwt","qxl","qxb"] + }, + "application/vnd.quobject-quoxdocument": { + "source": "iana" + }, + "application/vnd.radisys.moml+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-audit+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-audit-conf+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-audit-conn+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-audit-dialog+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-audit-stream+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-conf+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-dialog+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-dialog-base+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-dialog-fax-detect+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-dialog-fax-sendrecv+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-dialog-group+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-dialog-speech+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.radisys.msml-dialog-transform+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.rainstor.data": { + "source": "iana" + }, + "application/vnd.rapid": { + "source": "iana" + }, + "application/vnd.rar": { + "source": "iana" + }, + "application/vnd.realvnc.bed": { + "source": "iana", + "extensions": ["bed"] + }, + "application/vnd.recordare.musicxml": { + "source": "iana", + "extensions": ["mxl"] + }, + "application/vnd.recordare.musicxml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["musicxml"] + }, + "application/vnd.renlearn.rlprint": { + "source": "iana" + }, + "application/vnd.restful+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.rig.cryptonote": { + "source": "iana", + "extensions": ["cryptonote"] + }, + "application/vnd.rim.cod": { + "source": "apache", + "extensions": ["cod"] + }, + "application/vnd.rn-realmedia": { + "source": "apache", + "extensions": ["rm"] + }, + "application/vnd.rn-realmedia-vbr": { + "source": "apache", + "extensions": ["rmvb"] + }, + "application/vnd.route66.link66+xml": { + "source": "iana", + "compressible": true, + "extensions": ["link66"] + }, + "application/vnd.rs-274x": { + "source": "iana" + }, + "application/vnd.ruckus.download": { + "source": "iana" + }, + "application/vnd.s3sms": { + "source": "iana" + }, + "application/vnd.sailingtracker.track": { + "source": "iana", + "extensions": ["st"] + }, + "application/vnd.sbm.cid": { + "source": "iana" + }, + "application/vnd.sbm.mid2": { + "source": "iana" + }, + "application/vnd.scribus": { + "source": "iana" + }, + "application/vnd.sealed.3df": { + "source": "iana" + }, + "application/vnd.sealed.csf": { + "source": "iana" + }, + "application/vnd.sealed.doc": { + "source": "iana" + }, + "application/vnd.sealed.eml": { + "source": "iana" + }, + "application/vnd.sealed.mht": { + "source": "iana" + }, + "application/vnd.sealed.net": { + "source": "iana" + }, + "application/vnd.sealed.ppt": { + "source": "iana" + }, + "application/vnd.sealed.tiff": { + "source": "iana" + }, + "application/vnd.sealed.xls": { + "source": "iana" + }, + "application/vnd.sealedmedia.softseal.html": { + "source": "iana" + }, + "application/vnd.sealedmedia.softseal.pdf": { + "source": "iana" + }, + "application/vnd.seemail": { + "source": "iana", + "extensions": ["see"] + }, + "application/vnd.sema": { + "source": "iana", + "extensions": ["sema"] + }, + "application/vnd.semd": { + "source": "iana", + "extensions": ["semd"] + }, + "application/vnd.semf": { + "source": "iana", + "extensions": ["semf"] + }, + "application/vnd.shade-save-file": { + "source": "iana" + }, + "application/vnd.shana.informed.formdata": { + "source": "iana", + "extensions": ["ifm"] + }, + "application/vnd.shana.informed.formtemplate": { + "source": "iana", + "extensions": ["itp"] + }, + "application/vnd.shana.informed.interchange": { + "source": "iana", + "extensions": ["iif"] + }, + "application/vnd.shana.informed.package": { + "source": "iana", + "extensions": ["ipk"] + }, + "application/vnd.shootproof+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.shopkick+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.sigrok.session": { + "source": "iana" + }, + "application/vnd.simtech-mindmapper": { + "source": "iana", + "extensions": ["twd","twds"] + }, + "application/vnd.siren+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.smaf": { + "source": "iana", + "extensions": ["mmf"] + }, + "application/vnd.smart.notebook": { + "source": "iana" + }, + "application/vnd.smart.teacher": { + "source": "iana", + "extensions": ["teacher"] + }, + "application/vnd.software602.filler.form+xml": { + "source": "iana", + "compressible": true, + "extensions": ["fo"] + }, + "application/vnd.software602.filler.form-xml-zip": { + "source": "iana" + }, + "application/vnd.solent.sdkm+xml": { + "source": "iana", + "compressible": true, + "extensions": ["sdkm","sdkd"] + }, + "application/vnd.spotfire.dxp": { + "source": "iana", + "extensions": ["dxp"] + }, + "application/vnd.spotfire.sfs": { + "source": "iana", + "extensions": ["sfs"] + }, + "application/vnd.sqlite3": { + "source": "iana" + }, + "application/vnd.sss-cod": { + "source": "iana" + }, + "application/vnd.sss-dtf": { + "source": "iana" + }, + "application/vnd.sss-ntf": { + "source": "iana" + }, + "application/vnd.stardivision.calc": { + "source": "apache", + "extensions": ["sdc"] + }, + "application/vnd.stardivision.draw": { + "source": "apache", + "extensions": ["sda"] + }, + "application/vnd.stardivision.impress": { + "source": "apache", + "extensions": ["sdd"] + }, + "application/vnd.stardivision.math": { + "source": "apache", + "extensions": ["smf"] + }, + "application/vnd.stardivision.writer": { + "source": "apache", + "extensions": ["sdw","vor"] + }, + "application/vnd.stardivision.writer-global": { + "source": "apache", + "extensions": ["sgl"] + }, + "application/vnd.stepmania.package": { + "source": "iana", + "extensions": ["smzip"] + }, + "application/vnd.stepmania.stepchart": { + "source": "iana", + "extensions": ["sm"] + }, + "application/vnd.street-stream": { + "source": "iana" + }, + "application/vnd.sun.wadl+xml": { + "source": "iana", + "compressible": true, + "extensions": ["wadl"] + }, + "application/vnd.sun.xml.calc": { + "source": "apache", + "extensions": ["sxc"] + }, + "application/vnd.sun.xml.calc.template": { + "source": "apache", + "extensions": ["stc"] + }, + "application/vnd.sun.xml.draw": { + "source": "apache", + "extensions": ["sxd"] + }, + "application/vnd.sun.xml.draw.template": { + "source": "apache", + "extensions": ["std"] + }, + "application/vnd.sun.xml.impress": { + "source": "apache", + "extensions": ["sxi"] + }, + "application/vnd.sun.xml.impress.template": { + "source": "apache", + "extensions": ["sti"] + }, + "application/vnd.sun.xml.math": { + "source": "apache", + "extensions": ["sxm"] + }, + "application/vnd.sun.xml.writer": { + "source": "apache", + "extensions": ["sxw"] + }, + "application/vnd.sun.xml.writer.global": { + "source": "apache", + "extensions": ["sxg"] + }, + "application/vnd.sun.xml.writer.template": { + "source": "apache", + "extensions": ["stw"] + }, + "application/vnd.sus-calendar": { + "source": "iana", + "extensions": ["sus","susp"] + }, + "application/vnd.svd": { + "source": "iana", + "extensions": ["svd"] + }, + "application/vnd.swiftview-ics": { + "source": "iana" + }, + "application/vnd.symbian.install": { + "source": "apache", + "extensions": ["sis","sisx"] + }, + "application/vnd.syncml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xsm"] + }, + "application/vnd.syncml.dm+wbxml": { + "source": "iana", + "extensions": ["bdm"] + }, + "application/vnd.syncml.dm+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xdm"] + }, + "application/vnd.syncml.dm.notification": { + "source": "iana" + }, + "application/vnd.syncml.dmddf+wbxml": { + "source": "iana" + }, + "application/vnd.syncml.dmddf+xml": { + "source": "iana", + "compressible": true, + "extensions": ["ddf"] + }, + "application/vnd.syncml.dmtnds+wbxml": { + "source": "iana" + }, + "application/vnd.syncml.dmtnds+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.syncml.ds.notification": { + "source": "iana" + }, + "application/vnd.tableschema+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.tao.intent-module-archive": { + "source": "iana", + "extensions": ["tao"] + }, + "application/vnd.tcpdump.pcap": { + "source": "iana", + "extensions": ["pcap","cap","dmp"] + }, + "application/vnd.think-cell.ppttc+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.tmd.mediaflex.api+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.tml": { + "source": "iana" + }, + "application/vnd.tmobile-livetv": { + "source": "iana", + "extensions": ["tmo"] + }, + "application/vnd.tri.onesource": { + "source": "iana" + }, + "application/vnd.trid.tpt": { + "source": "iana", + "extensions": ["tpt"] + }, + "application/vnd.triscape.mxs": { + "source": "iana", + "extensions": ["mxs"] + }, + "application/vnd.trueapp": { + "source": "iana", + "extensions": ["tra"] + }, + "application/vnd.truedoc": { + "source": "iana" + }, + "application/vnd.ubisoft.webplayer": { + "source": "iana" + }, + "application/vnd.ufdl": { + "source": "iana", + "extensions": ["ufd","ufdl"] + }, + "application/vnd.uiq.theme": { + "source": "iana", + "extensions": ["utz"] + }, + "application/vnd.umajin": { + "source": "iana", + "extensions": ["umj"] + }, + "application/vnd.unity": { + "source": "iana", + "extensions": ["unityweb"] + }, + "application/vnd.uoml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["uoml"] + }, + "application/vnd.uplanet.alert": { + "source": "iana" + }, + "application/vnd.uplanet.alert-wbxml": { + "source": "iana" + }, + "application/vnd.uplanet.bearer-choice": { + "source": "iana" + }, + "application/vnd.uplanet.bearer-choice-wbxml": { + "source": "iana" + }, + "application/vnd.uplanet.cacheop": { + "source": "iana" + }, + "application/vnd.uplanet.cacheop-wbxml": { + "source": "iana" + }, + "application/vnd.uplanet.channel": { + "source": "iana" + }, + "application/vnd.uplanet.channel-wbxml": { + "source": "iana" + }, + "application/vnd.uplanet.list": { + "source": "iana" + }, + "application/vnd.uplanet.list-wbxml": { + "source": "iana" + }, + "application/vnd.uplanet.listcmd": { + "source": "iana" + }, + "application/vnd.uplanet.listcmd-wbxml": { + "source": "iana" + }, + "application/vnd.uplanet.signal": { + "source": "iana" + }, + "application/vnd.uri-map": { + "source": "iana" + }, + "application/vnd.valve.source.material": { + "source": "iana" + }, + "application/vnd.vcx": { + "source": "iana", + "extensions": ["vcx"] + }, + "application/vnd.vd-study": { + "source": "iana" + }, + "application/vnd.vectorworks": { + "source": "iana" + }, + "application/vnd.vel+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.verimatrix.vcas": { + "source": "iana" + }, + "application/vnd.veryant.thin": { + "source": "iana" + }, + "application/vnd.ves.encrypted": { + "source": "iana" + }, + "application/vnd.vidsoft.vidconference": { + "source": "iana" + }, + "application/vnd.visio": { + "source": "iana", + "extensions": ["vsd","vst","vss","vsw"] + }, + "application/vnd.visionary": { + "source": "iana", + "extensions": ["vis"] + }, + "application/vnd.vividence.scriptfile": { + "source": "iana" + }, + "application/vnd.vsf": { + "source": "iana", + "extensions": ["vsf"] + }, + "application/vnd.wap.sic": { + "source": "iana" + }, + "application/vnd.wap.slc": { + "source": "iana" + }, + "application/vnd.wap.wbxml": { + "source": "iana", + "extensions": ["wbxml"] + }, + "application/vnd.wap.wmlc": { + "source": "iana", + "extensions": ["wmlc"] + }, + "application/vnd.wap.wmlscriptc": { + "source": "iana", + "extensions": ["wmlsc"] + }, + "application/vnd.webturbo": { + "source": "iana", + "extensions": ["wtb"] + }, + "application/vnd.wfa.p2p": { + "source": "iana" + }, + "application/vnd.wfa.wsc": { + "source": "iana" + }, + "application/vnd.windows.devicepairing": { + "source": "iana" + }, + "application/vnd.wmc": { + "source": "iana" + }, + "application/vnd.wmf.bootstrap": { + "source": "iana" + }, + "application/vnd.wolfram.mathematica": { + "source": "iana" + }, + "application/vnd.wolfram.mathematica.package": { + "source": "iana" + }, + "application/vnd.wolfram.player": { + "source": "iana", + "extensions": ["nbp"] + }, + "application/vnd.wordperfect": { + "source": "iana", + "extensions": ["wpd"] + }, + "application/vnd.wqd": { + "source": "iana", + "extensions": ["wqd"] + }, + "application/vnd.wrq-hp3000-labelled": { + "source": "iana" + }, + "application/vnd.wt.stf": { + "source": "iana", + "extensions": ["stf"] + }, + "application/vnd.wv.csp+wbxml": { + "source": "iana" + }, + "application/vnd.wv.csp+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.wv.ssp+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.xacml+json": { + "source": "iana", + "compressible": true + }, + "application/vnd.xara": { + "source": "iana", + "extensions": ["xar"] + }, + "application/vnd.xfdl": { + "source": "iana", + "extensions": ["xfdl"] + }, + "application/vnd.xfdl.webform": { + "source": "iana" + }, + "application/vnd.xmi+xml": { + "source": "iana", + "compressible": true + }, + "application/vnd.xmpie.cpkg": { + "source": "iana" + }, + "application/vnd.xmpie.dpkg": { + "source": "iana" + }, + "application/vnd.xmpie.plan": { + "source": "iana" + }, + "application/vnd.xmpie.ppkg": { + "source": "iana" + }, + "application/vnd.xmpie.xlim": { + "source": "iana" + }, + "application/vnd.yamaha.hv-dic": { + "source": "iana", + "extensions": ["hvd"] + }, + "application/vnd.yamaha.hv-script": { + "source": "iana", + "extensions": ["hvs"] + }, + "application/vnd.yamaha.hv-voice": { + "source": "iana", + "extensions": ["hvp"] + }, + "application/vnd.yamaha.openscoreformat": { + "source": "iana", + "extensions": ["osf"] + }, + "application/vnd.yamaha.openscoreformat.osfpvg+xml": { + "source": "iana", + "compressible": true, + "extensions": ["osfpvg"] + }, + "application/vnd.yamaha.remote-setup": { + "source": "iana" + }, + "application/vnd.yamaha.smaf-audio": { + "source": "iana", + "extensions": ["saf"] + }, + "application/vnd.yamaha.smaf-phrase": { + "source": "iana", + "extensions": ["spf"] + }, + "application/vnd.yamaha.through-ngn": { + "source": "iana" + }, + "application/vnd.yamaha.tunnel-udpencap": { + "source": "iana" + }, + "application/vnd.yaoweme": { + "source": "iana" + }, + "application/vnd.yellowriver-custom-menu": { + "source": "iana", + "extensions": ["cmp"] + }, + "application/vnd.youtube.yt": { + "source": "iana" + }, + "application/vnd.zul": { + "source": "iana", + "extensions": ["zir","zirz"] + }, + "application/vnd.zzazz.deck+xml": { + "source": "iana", + "compressible": true, + "extensions": ["zaz"] + }, + "application/voicexml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["vxml"] + }, + "application/voucher-cms+json": { + "source": "iana", + "compressible": true + }, + "application/vq-rtcpxr": { + "source": "iana" + }, + "application/wasm": { + "compressible": true, + "extensions": ["wasm"] + }, + "application/watcherinfo+xml": { + "source": "iana", + "compressible": true + }, + "application/webpush-options+json": { + "source": "iana", + "compressible": true + }, + "application/whoispp-query": { + "source": "iana" + }, + "application/whoispp-response": { + "source": "iana" + }, + "application/widget": { + "source": "iana", + "extensions": ["wgt"] + }, + "application/winhlp": { + "source": "apache", + "extensions": ["hlp"] + }, + "application/wita": { + "source": "iana" + }, + "application/wordperfect5.1": { + "source": "iana" + }, + "application/wsdl+xml": { + "source": "iana", + "compressible": true, + "extensions": ["wsdl"] + }, + "application/wspolicy+xml": { + "source": "iana", + "compressible": true, + "extensions": ["wspolicy"] + }, + "application/x-7z-compressed": { + "source": "apache", + "compressible": false, + "extensions": ["7z"] + }, + "application/x-abiword": { + "source": "apache", + "extensions": ["abw"] + }, + "application/x-ace-compressed": { + "source": "apache", + "extensions": ["ace"] + }, + "application/x-amf": { + "source": "apache" + }, + "application/x-apple-diskimage": { + "source": "apache", + "extensions": ["dmg"] + }, + "application/x-arj": { + "compressible": false, + "extensions": ["arj"] + }, + "application/x-authorware-bin": { + "source": "apache", + "extensions": ["aab","x32","u32","vox"] + }, + "application/x-authorware-map": { + "source": "apache", + "extensions": ["aam"] + }, + "application/x-authorware-seg": { + "source": "apache", + "extensions": ["aas"] + }, + "application/x-bcpio": { + "source": "apache", + "extensions": ["bcpio"] + }, + "application/x-bdoc": { + "compressible": false, + "extensions": ["bdoc"] + }, + "application/x-bittorrent": { + "source": "apache", + "extensions": ["torrent"] + }, + "application/x-blorb": { + "source": "apache", + "extensions": ["blb","blorb"] + }, + "application/x-bzip": { + "source": "apache", + "compressible": false, + "extensions": ["bz"] + }, + "application/x-bzip2": { + "source": "apache", + "compressible": false, + "extensions": ["bz2","boz"] + }, + "application/x-cbr": { + "source": "apache", + "extensions": ["cbr","cba","cbt","cbz","cb7"] + }, + "application/x-cdlink": { + "source": "apache", + "extensions": ["vcd"] + }, + "application/x-cfs-compressed": { + "source": "apache", + "extensions": ["cfs"] + }, + "application/x-chat": { + "source": "apache", + "extensions": ["chat"] + }, + "application/x-chess-pgn": { + "source": "apache", + "extensions": ["pgn"] + }, + "application/x-chrome-extension": { + "extensions": ["crx"] + }, + "application/x-cocoa": { + "source": "nginx", + "extensions": ["cco"] + }, + "application/x-compress": { + "source": "apache" + }, + "application/x-conference": { + "source": "apache", + "extensions": ["nsc"] + }, + "application/x-cpio": { + "source": "apache", + "extensions": ["cpio"] + }, + "application/x-csh": { + "source": "apache", + "extensions": ["csh"] + }, + "application/x-deb": { + "compressible": false + }, + "application/x-debian-package": { + "source": "apache", + "extensions": ["deb","udeb"] + }, + "application/x-dgc-compressed": { + "source": "apache", + "extensions": ["dgc"] + }, + "application/x-director": { + "source": "apache", + "extensions": ["dir","dcr","dxr","cst","cct","cxt","w3d","fgd","swa"] + }, + "application/x-doom": { + "source": "apache", + "extensions": ["wad"] + }, + "application/x-dtbncx+xml": { + "source": "apache", + "compressible": true, + "extensions": ["ncx"] + }, + "application/x-dtbook+xml": { + "source": "apache", + "compressible": true, + "extensions": ["dtb"] + }, + "application/x-dtbresource+xml": { + "source": "apache", + "compressible": true, + "extensions": ["res"] + }, + "application/x-dvi": { + "source": "apache", + "compressible": false, + "extensions": ["dvi"] + }, + "application/x-envoy": { + "source": "apache", + "extensions": ["evy"] + }, + "application/x-eva": { + "source": "apache", + "extensions": ["eva"] + }, + "application/x-font-bdf": { + "source": "apache", + "extensions": ["bdf"] + }, + "application/x-font-dos": { + "source": "apache" + }, + "application/x-font-framemaker": { + "source": "apache" + }, + "application/x-font-ghostscript": { + "source": "apache", + "extensions": ["gsf"] + }, + "application/x-font-libgrx": { + "source": "apache" + }, + "application/x-font-linux-psf": { + "source": "apache", + "extensions": ["psf"] + }, + "application/x-font-pcf": { + "source": "apache", + "extensions": ["pcf"] + }, + "application/x-font-snf": { + "source": "apache", + "extensions": ["snf"] + }, + "application/x-font-speedo": { + "source": "apache" + }, + "application/x-font-sunos-news": { + "source": "apache" + }, + "application/x-font-type1": { + "source": "apache", + "extensions": ["pfa","pfb","pfm","afm"] + }, + "application/x-font-vfont": { + "source": "apache" + }, + "application/x-freearc": { + "source": "apache", + "extensions": ["arc"] + }, + "application/x-futuresplash": { + "source": "apache", + "extensions": ["spl"] + }, + "application/x-gca-compressed": { + "source": "apache", + "extensions": ["gca"] + }, + "application/x-glulx": { + "source": "apache", + "extensions": ["ulx"] + }, + "application/x-gnumeric": { + "source": "apache", + "extensions": ["gnumeric"] + }, + "application/x-gramps-xml": { + "source": "apache", + "extensions": ["gramps"] + }, + "application/x-gtar": { + "source": "apache", + "extensions": ["gtar"] + }, + "application/x-gzip": { + "source": "apache" + }, + "application/x-hdf": { + "source": "apache", + "extensions": ["hdf"] + }, + "application/x-httpd-php": { + "compressible": true, + "extensions": ["php"] + }, + "application/x-install-instructions": { + "source": "apache", + "extensions": ["install"] + }, + "application/x-iso9660-image": { + "source": "apache", + "extensions": ["iso"] + }, + "application/x-java-archive-diff": { + "source": "nginx", + "extensions": ["jardiff"] + }, + "application/x-java-jnlp-file": { + "source": "apache", + "compressible": false, + "extensions": ["jnlp"] + }, + "application/x-javascript": { + "compressible": true + }, + "application/x-keepass2": { + "extensions": ["kdbx"] + }, + "application/x-latex": { + "source": "apache", + "compressible": false, + "extensions": ["latex"] + }, + "application/x-lua-bytecode": { + "extensions": ["luac"] + }, + "application/x-lzh-compressed": { + "source": "apache", + "extensions": ["lzh","lha"] + }, + "application/x-makeself": { + "source": "nginx", + "extensions": ["run"] + }, + "application/x-mie": { + "source": "apache", + "extensions": ["mie"] + }, + "application/x-mobipocket-ebook": { + "source": "apache", + "extensions": ["prc","mobi"] + }, + "application/x-mpegurl": { + "compressible": false + }, + "application/x-ms-application": { + "source": "apache", + "extensions": ["application"] + }, + "application/x-ms-shortcut": { + "source": "apache", + "extensions": ["lnk"] + }, + "application/x-ms-wmd": { + "source": "apache", + "extensions": ["wmd"] + }, + "application/x-ms-wmz": { + "source": "apache", + "extensions": ["wmz"] + }, + "application/x-ms-xbap": { + "source": "apache", + "extensions": ["xbap"] + }, + "application/x-msaccess": { + "source": "apache", + "extensions": ["mdb"] + }, + "application/x-msbinder": { + "source": "apache", + "extensions": ["obd"] + }, + "application/x-mscardfile": { + "source": "apache", + "extensions": ["crd"] + }, + "application/x-msclip": { + "source": "apache", + "extensions": ["clp"] + }, + "application/x-msdos-program": { + "extensions": ["exe"] + }, + "application/x-msdownload": { + "source": "apache", + "extensions": ["exe","dll","com","bat","msi"] + }, + "application/x-msmediaview": { + "source": "apache", + "extensions": ["mvb","m13","m14"] + }, + "application/x-msmetafile": { + "source": "apache", + "extensions": ["wmf","wmz","emf","emz"] + }, + "application/x-msmoney": { + "source": "apache", + "extensions": ["mny"] + }, + "application/x-mspublisher": { + "source": "apache", + "extensions": ["pub"] + }, + "application/x-msschedule": { + "source": "apache", + "extensions": ["scd"] + }, + "application/x-msterminal": { + "source": "apache", + "extensions": ["trm"] + }, + "application/x-mswrite": { + "source": "apache", + "extensions": ["wri"] + }, + "application/x-netcdf": { + "source": "apache", + "extensions": ["nc","cdf"] + }, + "application/x-ns-proxy-autoconfig": { + "compressible": true, + "extensions": ["pac"] + }, + "application/x-nzb": { + "source": "apache", + "extensions": ["nzb"] + }, + "application/x-perl": { + "source": "nginx", + "extensions": ["pl","pm"] + }, + "application/x-pilot": { + "source": "nginx", + "extensions": ["prc","pdb"] + }, + "application/x-pkcs12": { + "source": "apache", + "compressible": false, + "extensions": ["p12","pfx"] + }, + "application/x-pkcs7-certificates": { + "source": "apache", + "extensions": ["p7b","spc"] + }, + "application/x-pkcs7-certreqresp": { + "source": "apache", + "extensions": ["p7r"] + }, + "application/x-rar-compressed": { + "source": "apache", + "compressible": false, + "extensions": ["rar"] + }, + "application/x-redhat-package-manager": { + "source": "nginx", + "extensions": ["rpm"] + }, + "application/x-research-info-systems": { + "source": "apache", + "extensions": ["ris"] + }, + "application/x-sea": { + "source": "nginx", + "extensions": ["sea"] + }, + "application/x-sh": { + "source": "apache", + "compressible": true, + "extensions": ["sh"] + }, + "application/x-shar": { + "source": "apache", + "extensions": ["shar"] + }, + "application/x-shockwave-flash": { + "source": "apache", + "compressible": false, + "extensions": ["swf"] + }, + "application/x-silverlight-app": { + "source": "apache", + "extensions": ["xap"] + }, + "application/x-sql": { + "source": "apache", + "extensions": ["sql"] + }, + "application/x-stuffit": { + "source": "apache", + "compressible": false, + "extensions": ["sit"] + }, + "application/x-stuffitx": { + "source": "apache", + "extensions": ["sitx"] + }, + "application/x-subrip": { + "source": "apache", + "extensions": ["srt"] + }, + "application/x-sv4cpio": { + "source": "apache", + "extensions": ["sv4cpio"] + }, + "application/x-sv4crc": { + "source": "apache", + "extensions": ["sv4crc"] + }, + "application/x-t3vm-image": { + "source": "apache", + "extensions": ["t3"] + }, + "application/x-tads": { + "source": "apache", + "extensions": ["gam"] + }, + "application/x-tar": { + "source": "apache", + "compressible": true, + "extensions": ["tar"] + }, + "application/x-tcl": { + "source": "apache", + "extensions": ["tcl","tk"] + }, + "application/x-tex": { + "source": "apache", + "extensions": ["tex"] + }, + "application/x-tex-tfm": { + "source": "apache", + "extensions": ["tfm"] + }, + "application/x-texinfo": { + "source": "apache", + "extensions": ["texinfo","texi"] + }, + "application/x-tgif": { + "source": "apache", + "extensions": ["obj"] + }, + "application/x-ustar": { + "source": "apache", + "extensions": ["ustar"] + }, + "application/x-virtualbox-hdd": { + "compressible": true, + "extensions": ["hdd"] + }, + "application/x-virtualbox-ova": { + "compressible": true, + "extensions": ["ova"] + }, + "application/x-virtualbox-ovf": { + "compressible": true, + "extensions": ["ovf"] + }, + "application/x-virtualbox-vbox": { + "compressible": true, + "extensions": ["vbox"] + }, + "application/x-virtualbox-vbox-extpack": { + "compressible": false, + "extensions": ["vbox-extpack"] + }, + "application/x-virtualbox-vdi": { + "compressible": true, + "extensions": ["vdi"] + }, + "application/x-virtualbox-vhd": { + "compressible": true, + "extensions": ["vhd"] + }, + "application/x-virtualbox-vmdk": { + "compressible": true, + "extensions": ["vmdk"] + }, + "application/x-wais-source": { + "source": "apache", + "extensions": ["src"] + }, + "application/x-web-app-manifest+json": { + "compressible": true, + "extensions": ["webapp"] + }, + "application/x-www-form-urlencoded": { + "source": "iana", + "compressible": true + }, + "application/x-x509-ca-cert": { + "source": "apache", + "extensions": ["der","crt","pem"] + }, + "application/x-xfig": { + "source": "apache", + "extensions": ["fig"] + }, + "application/x-xliff+xml": { + "source": "apache", + "compressible": true, + "extensions": ["xlf"] + }, + "application/x-xpinstall": { + "source": "apache", + "compressible": false, + "extensions": ["xpi"] + }, + "application/x-xz": { + "source": "apache", + "extensions": ["xz"] + }, + "application/x-zmachine": { + "source": "apache", + "extensions": ["z1","z2","z3","z4","z5","z6","z7","z8"] + }, + "application/x400-bp": { + "source": "iana" + }, + "application/xacml+xml": { + "source": "iana", + "compressible": true + }, + "application/xaml+xml": { + "source": "apache", + "compressible": true, + "extensions": ["xaml"] + }, + "application/xcap-att+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xav"] + }, + "application/xcap-caps+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xca"] + }, + "application/xcap-diff+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xdf"] + }, + "application/xcap-el+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xel"] + }, + "application/xcap-error+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xer"] + }, + "application/xcap-ns+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xns"] + }, + "application/xcon-conference-info+xml": { + "source": "iana", + "compressible": true + }, + "application/xcon-conference-info-diff+xml": { + "source": "iana", + "compressible": true + }, + "application/xenc+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xenc"] + }, + "application/xhtml+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xhtml","xht"] + }, + "application/xhtml-voice+xml": { + "source": "apache", + "compressible": true + }, + "application/xliff+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xlf"] + }, + "application/xml": { + "source": "iana", + "compressible": true, + "extensions": ["xml","xsl","xsd","rng"] + }, + "application/xml-dtd": { + "source": "iana", + "compressible": true, + "extensions": ["dtd"] + }, + "application/xml-external-parsed-entity": { + "source": "iana" + }, + "application/xml-patch+xml": { + "source": "iana", + "compressible": true + }, + "application/xmpp+xml": { + "source": "iana", + "compressible": true + }, + "application/xop+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xop"] + }, + "application/xproc+xml": { + "source": "apache", + "compressible": true, + "extensions": ["xpl"] + }, + "application/xslt+xml": { + "source": "iana", + "compressible": true, + "extensions": ["xslt"] + }, + "application/xspf+xml": { + "source": "apache", + "compressible": true, + "extensions": ["xspf"] + }, + "application/xv+xml": { + "source": "iana", + "compressible": true, + "extensions": ["mxml","xhvml","xvml","xvm"] + }, + "application/yang": { + "source": "iana", + "extensions": ["yang"] + }, + "application/yang-data+json": { + "source": "iana", + "compressible": true + }, + "application/yang-data+xml": { + "source": "iana", + "compressible": true + }, + "application/yang-patch+json": { + "source": "iana", + "compressible": true + }, + "application/yang-patch+xml": { + "source": "iana", + "compressible": true + }, + "application/yin+xml": { + "source": "iana", + "compressible": true, + "extensions": ["yin"] + }, + "application/zip": { + "source": "iana", + "compressible": false, + "extensions": ["zip"] + }, + "application/zlib": { + "source": "iana" + }, + "application/zstd": { + "source": "iana" + }, + "audio/1d-interleaved-parityfec": { + "source": "iana" + }, + "audio/32kadpcm": { + "source": "iana" + }, + "audio/3gpp": { + "source": "iana", + "compressible": false, + "extensions": ["3gpp"] + }, + "audio/3gpp2": { + "source": "iana" + }, + "audio/aac": { + "source": "iana" + }, + "audio/ac3": { + "source": "iana" + }, + "audio/adpcm": { + "source": "apache", + "extensions": ["adp"] + }, + "audio/amr": { + "source": "iana" + }, + "audio/amr-wb": { + "source": "iana" + }, + "audio/amr-wb+": { + "source": "iana" + }, + "audio/aptx": { + "source": "iana" + }, + "audio/asc": { + "source": "iana" + }, + "audio/atrac-advanced-lossless": { + "source": "iana" + }, + "audio/atrac-x": { + "source": "iana" + }, + "audio/atrac3": { + "source": "iana" + }, + "audio/basic": { + "source": "iana", + "compressible": false, + "extensions": ["au","snd"] + }, + "audio/bv16": { + "source": "iana" + }, + "audio/bv32": { + "source": "iana" + }, + "audio/clearmode": { + "source": "iana" + }, + "audio/cn": { + "source": "iana" + }, + "audio/dat12": { + "source": "iana" + }, + "audio/dls": { + "source": "iana" + }, + "audio/dsr-es201108": { + "source": "iana" + }, + "audio/dsr-es202050": { + "source": "iana" + }, + "audio/dsr-es202211": { + "source": "iana" + }, + "audio/dsr-es202212": { + "source": "iana" + }, + "audio/dv": { + "source": "iana" + }, + "audio/dvi4": { + "source": "iana" + }, + "audio/eac3": { + "source": "iana" + }, + "audio/encaprtp": { + "source": "iana" + }, + "audio/evrc": { + "source": "iana" + }, + "audio/evrc-qcp": { + "source": "iana" + }, + "audio/evrc0": { + "source": "iana" + }, + "audio/evrc1": { + "source": "iana" + }, + "audio/evrcb": { + "source": "iana" + }, + "audio/evrcb0": { + "source": "iana" + }, + "audio/evrcb1": { + "source": "iana" + }, + "audio/evrcnw": { + "source": "iana" + }, + "audio/evrcnw0": { + "source": "iana" + }, + "audio/evrcnw1": { + "source": "iana" + }, + "audio/evrcwb": { + "source": "iana" + }, + "audio/evrcwb0": { + "source": "iana" + }, + "audio/evrcwb1": { + "source": "iana" + }, + "audio/evs": { + "source": "iana" + }, + "audio/flexfec": { + "source": "iana" + }, + "audio/fwdred": { + "source": "iana" + }, + "audio/g711-0": { + "source": "iana" + }, + "audio/g719": { + "source": "iana" + }, + "audio/g722": { + "source": "iana" + }, + "audio/g7221": { + "source": "iana" + }, + "audio/g723": { + "source": "iana" + }, + "audio/g726-16": { + "source": "iana" + }, + "audio/g726-24": { + "source": "iana" + }, + "audio/g726-32": { + "source": "iana" + }, + "audio/g726-40": { + "source": "iana" + }, + "audio/g728": { + "source": "iana" + }, + "audio/g729": { + "source": "iana" + }, + "audio/g7291": { + "source": "iana" + }, + "audio/g729d": { + "source": "iana" + }, + "audio/g729e": { + "source": "iana" + }, + "audio/gsm": { + "source": "iana" + }, + "audio/gsm-efr": { + "source": "iana" + }, + "audio/gsm-hr-08": { + "source": "iana" + }, + "audio/ilbc": { + "source": "iana" + }, + "audio/ip-mr_v2.5": { + "source": "iana" + }, + "audio/isac": { + "source": "apache" + }, + "audio/l16": { + "source": "iana" + }, + "audio/l20": { + "source": "iana" + }, + "audio/l24": { + "source": "iana", + "compressible": false + }, + "audio/l8": { + "source": "iana" + }, + "audio/lpc": { + "source": "iana" + }, + "audio/melp": { + "source": "iana" + }, + "audio/melp1200": { + "source": "iana" + }, + "audio/melp2400": { + "source": "iana" + }, + "audio/melp600": { + "source": "iana" + }, + "audio/midi": { + "source": "apache", + "extensions": ["mid","midi","kar","rmi"] + }, + "audio/mobile-xmf": { + "source": "iana", + "extensions": ["mxmf"] + }, + "audio/mp3": { + "compressible": false, + "extensions": ["mp3"] + }, + "audio/mp4": { + "source": "iana", + "compressible": false, + "extensions": ["m4a","mp4a"] + }, + "audio/mp4a-latm": { + "source": "iana" + }, + "audio/mpa": { + "source": "iana" + }, + "audio/mpa-robust": { + "source": "iana" + }, + "audio/mpeg": { + "source": "iana", + "compressible": false, + "extensions": ["mpga","mp2","mp2a","mp3","m2a","m3a"] + }, + "audio/mpeg4-generic": { + "source": "iana" + }, + "audio/musepack": { + "source": "apache" + }, + "audio/ogg": { + "source": "iana", + "compressible": false, + "extensions": ["oga","ogg","spx"] + }, + "audio/opus": { + "source": "iana" + }, + "audio/parityfec": { + "source": "iana" + }, + "audio/pcma": { + "source": "iana" + }, + "audio/pcma-wb": { + "source": "iana" + }, + "audio/pcmu": { + "source": "iana" + }, + "audio/pcmu-wb": { + "source": "iana" + }, + "audio/prs.sid": { + "source": "iana" + }, + "audio/qcelp": { + "source": "iana" + }, + "audio/raptorfec": { + "source": "iana" + }, + "audio/red": { + "source": "iana" + }, + "audio/rtp-enc-aescm128": { + "source": "iana" + }, + "audio/rtp-midi": { + "source": "iana" + }, + "audio/rtploopback": { + "source": "iana" + }, + "audio/rtx": { + "source": "iana" + }, + "audio/s3m": { + "source": "apache", + "extensions": ["s3m"] + }, + "audio/silk": { + "source": "apache", + "extensions": ["sil"] + }, + "audio/smv": { + "source": "iana" + }, + "audio/smv-qcp": { + "source": "iana" + }, + "audio/smv0": { + "source": "iana" + }, + "audio/sp-midi": { + "source": "iana" + }, + "audio/speex": { + "source": "iana" + }, + "audio/t140c": { + "source": "iana" + }, + "audio/t38": { + "source": "iana" + }, + "audio/telephone-event": { + "source": "iana" + }, + "audio/tetra_acelp": { + "source": "iana" + }, + "audio/tone": { + "source": "iana" + }, + "audio/uemclip": { + "source": "iana" + }, + "audio/ulpfec": { + "source": "iana" + }, + "audio/usac": { + "source": "iana" + }, + "audio/vdvi": { + "source": "iana" + }, + "audio/vmr-wb": { + "source": "iana" + }, + "audio/vnd.3gpp.iufp": { + "source": "iana" + }, + "audio/vnd.4sb": { + "source": "iana" + }, + "audio/vnd.audiokoz": { + "source": "iana" + }, + "audio/vnd.celp": { + "source": "iana" + }, + "audio/vnd.cisco.nse": { + "source": "iana" + }, + "audio/vnd.cmles.radio-events": { + "source": "iana" + }, + "audio/vnd.cns.anp1": { + "source": "iana" + }, + "audio/vnd.cns.inf1": { + "source": "iana" + }, + "audio/vnd.dece.audio": { + "source": "iana", + "extensions": ["uva","uvva"] + }, + "audio/vnd.digital-winds": { + "source": "iana", + "extensions": ["eol"] + }, + "audio/vnd.dlna.adts": { + "source": "iana" + }, + "audio/vnd.dolby.heaac.1": { + "source": "iana" + }, + "audio/vnd.dolby.heaac.2": { + "source": "iana" + }, + "audio/vnd.dolby.mlp": { + "source": "iana" + }, + "audio/vnd.dolby.mps": { + "source": "iana" + }, + "audio/vnd.dolby.pl2": { + "source": "iana" + }, + "audio/vnd.dolby.pl2x": { + "source": "iana" + }, + "audio/vnd.dolby.pl2z": { + "source": "iana" + }, + "audio/vnd.dolby.pulse.1": { + "source": "iana" + }, + "audio/vnd.dra": { + "source": "iana", + "extensions": ["dra"] + }, + "audio/vnd.dts": { + "source": "iana", + "extensions": ["dts"] + }, + "audio/vnd.dts.hd": { + "source": "iana", + "extensions": ["dtshd"] + }, + "audio/vnd.dts.uhd": { + "source": "iana" + }, + "audio/vnd.dvb.file": { + "source": "iana" + }, + "audio/vnd.everad.plj": { + "source": "iana" + }, + "audio/vnd.hns.audio": { + "source": "iana" + }, + "audio/vnd.lucent.voice": { + "source": "iana", + "extensions": ["lvp"] + }, + "audio/vnd.ms-playready.media.pya": { + "source": "iana", + "extensions": ["pya"] + }, + "audio/vnd.nokia.mobile-xmf": { + "source": "iana" + }, + "audio/vnd.nortel.vbk": { + "source": "iana" + }, + "audio/vnd.nuera.ecelp4800": { + "source": "iana", + "extensions": ["ecelp4800"] + }, + "audio/vnd.nuera.ecelp7470": { + "source": "iana", + "extensions": ["ecelp7470"] + }, + "audio/vnd.nuera.ecelp9600": { + "source": "iana", + "extensions": ["ecelp9600"] + }, + "audio/vnd.octel.sbc": { + "source": "iana" + }, + "audio/vnd.presonus.multitrack": { + "source": "iana" + }, + "audio/vnd.qcelp": { + "source": "iana" + }, + "audio/vnd.rhetorex.32kadpcm": { + "source": "iana" + }, + "audio/vnd.rip": { + "source": "iana", + "extensions": ["rip"] + }, + "audio/vnd.rn-realaudio": { + "compressible": false + }, + "audio/vnd.sealedmedia.softseal.mpeg": { + "source": "iana" + }, + "audio/vnd.vmx.cvsd": { + "source": "iana" + }, + "audio/vnd.wave": { + "compressible": false + }, + "audio/vorbis": { + "source": "iana", + "compressible": false + }, + "audio/vorbis-config": { + "source": "iana" + }, + "audio/wav": { + "compressible": false, + "extensions": ["wav"] + }, + "audio/wave": { + "compressible": false, + "extensions": ["wav"] + }, + "audio/webm": { + "source": "apache", + "compressible": false, + "extensions": ["weba"] + }, + "audio/x-aac": { + "source": "apache", + "compressible": false, + "extensions": ["aac"] + }, + "audio/x-aiff": { + "source": "apache", + "extensions": ["aif","aiff","aifc"] + }, + "audio/x-caf": { + "source": "apache", + "compressible": false, + "extensions": ["caf"] + }, + "audio/x-flac": { + "source": "apache", + "extensions": ["flac"] + }, + "audio/x-m4a": { + "source": "nginx", + "extensions": ["m4a"] + }, + "audio/x-matroska": { + "source": "apache", + "extensions": ["mka"] + }, + "audio/x-mpegurl": { + "source": "apache", + "extensions": ["m3u"] + }, + "audio/x-ms-wax": { + "source": "apache", + "extensions": ["wax"] + }, + "audio/x-ms-wma": { + "source": "apache", + "extensions": ["wma"] + }, + "audio/x-pn-realaudio": { + "source": "apache", + "extensions": ["ram","ra"] + }, + "audio/x-pn-realaudio-plugin": { + "source": "apache", + "extensions": ["rmp"] + }, + "audio/x-realaudio": { + "source": "nginx", + "extensions": ["ra"] + }, + "audio/x-tta": { + "source": "apache" + }, + "audio/x-wav": { + "source": "apache", + "extensions": ["wav"] + }, + "audio/xm": { + "source": "apache", + "extensions": ["xm"] + }, + "chemical/x-cdx": { + "source": "apache", + "extensions": ["cdx"] + }, + "chemical/x-cif": { + "source": "apache", + "extensions": ["cif"] + }, + "chemical/x-cmdf": { + "source": "apache", + "extensions": ["cmdf"] + }, + "chemical/x-cml": { + "source": "apache", + "extensions": ["cml"] + }, + "chemical/x-csml": { + "source": "apache", + "extensions": ["csml"] + }, + "chemical/x-pdb": { + "source": "apache" + }, + "chemical/x-xyz": { + "source": "apache", + "extensions": ["xyz"] + }, + "font/collection": { + "source": "iana", + "extensions": ["ttc"] + }, + "font/otf": { + "source": "iana", + "compressible": true, + "extensions": ["otf"] + }, + "font/sfnt": { + "source": "iana" + }, + "font/ttf": { + "source": "iana", + "compressible": true, + "extensions": ["ttf"] + }, + "font/woff": { + "source": "iana", + "extensions": ["woff"] + }, + "font/woff2": { + "source": "iana", + "extensions": ["woff2"] + }, + "image/aces": { + "source": "iana", + "extensions": ["exr"] + }, + "image/apng": { + "compressible": false, + "extensions": ["apng"] + }, + "image/avci": { + "source": "iana" + }, + "image/avcs": { + "source": "iana" + }, + "image/bmp": { + "source": "iana", + "compressible": true, + "extensions": ["bmp"] + }, + "image/cgm": { + "source": "iana", + "extensions": ["cgm"] + }, + "image/dicom-rle": { + "source": "iana", + "extensions": ["drle"] + }, + "image/emf": { + "source": "iana", + "extensions": ["emf"] + }, + "image/fits": { + "source": "iana", + "extensions": ["fits"] + }, + "image/g3fax": { + "source": "iana", + "extensions": ["g3"] + }, + "image/gif": { + "source": "iana", + "compressible": false, + "extensions": ["gif"] + }, + "image/heic": { + "source": "iana", + "extensions": ["heic"] + }, + "image/heic-sequence": { + "source": "iana", + "extensions": ["heics"] + }, + "image/heif": { + "source": "iana", + "extensions": ["heif"] + }, + "image/heif-sequence": { + "source": "iana", + "extensions": ["heifs"] + }, + "image/hej2k": { + "source": "iana", + "extensions": ["hej2"] + }, + "image/hsj2": { + "source": "iana", + "extensions": ["hsj2"] + }, + "image/ief": { + "source": "iana", + "extensions": ["ief"] + }, + "image/jls": { + "source": "iana", + "extensions": ["jls"] + }, + "image/jp2": { + "source": "iana", + "compressible": false, + "extensions": ["jp2","jpg2"] + }, + "image/jpeg": { + "source": "iana", + "compressible": false, + "extensions": ["jpeg","jpg","jpe"] + }, + "image/jph": { + "source": "iana", + "extensions": ["jph"] + }, + "image/jphc": { + "source": "iana", + "extensions": ["jhc"] + }, + "image/jpm": { + "source": "iana", + "compressible": false, + "extensions": ["jpm"] + }, + "image/jpx": { + "source": "iana", + "compressible": false, + "extensions": ["jpx","jpf"] + }, + "image/jxr": { + "source": "iana", + "extensions": ["jxr"] + }, + "image/jxra": { + "source": "iana", + "extensions": ["jxra"] + }, + "image/jxrs": { + "source": "iana", + "extensions": ["jxrs"] + }, + "image/jxs": { + "source": "iana", + "extensions": ["jxs"] + }, + "image/jxsc": { + "source": "iana", + "extensions": ["jxsc"] + }, + "image/jxsi": { + "source": "iana", + "extensions": ["jxsi"] + }, + "image/jxss": { + "source": "iana", + "extensions": ["jxss"] + }, + "image/ktx": { + "source": "iana", + "extensions": ["ktx"] + }, + "image/naplps": { + "source": "iana" + }, + "image/pjpeg": { + "compressible": false + }, + "image/png": { + "source": "iana", + "compressible": false, + "extensions": ["png"] + }, + "image/prs.btif": { + "source": "iana", + "extensions": ["btif"] + }, + "image/prs.pti": { + "source": "iana", + "extensions": ["pti"] + }, + "image/pwg-raster": { + "source": "iana" + }, + "image/sgi": { + "source": "apache", + "extensions": ["sgi"] + }, + "image/svg+xml": { + "source": "iana", + "compressible": true, + "extensions": ["svg","svgz"] + }, + "image/t38": { + "source": "iana", + "extensions": ["t38"] + }, + "image/tiff": { + "source": "iana", + "compressible": false, + "extensions": ["tif","tiff"] + }, + "image/tiff-fx": { + "source": "iana", + "extensions": ["tfx"] + }, + "image/vnd.adobe.photoshop": { + "source": "iana", + "compressible": true, + "extensions": ["psd"] + }, + "image/vnd.airzip.accelerator.azv": { + "source": "iana", + "extensions": ["azv"] + }, + "image/vnd.cns.inf2": { + "source": "iana" + }, + "image/vnd.dece.graphic": { + "source": "iana", + "extensions": ["uvi","uvvi","uvg","uvvg"] + }, + "image/vnd.djvu": { + "source": "iana", + "extensions": ["djvu","djv"] + }, + "image/vnd.dvb.subtitle": { + "source": "iana", + "extensions": ["sub"] + }, + "image/vnd.dwg": { + "source": "iana", + "extensions": ["dwg"] + }, + "image/vnd.dxf": { + "source": "iana", + "extensions": ["dxf"] + }, + "image/vnd.fastbidsheet": { + "source": "iana", + "extensions": ["fbs"] + }, + "image/vnd.fpx": { + "source": "iana", + "extensions": ["fpx"] + }, + "image/vnd.fst": { + "source": "iana", + "extensions": ["fst"] + }, + "image/vnd.fujixerox.edmics-mmr": { + "source": "iana", + "extensions": ["mmr"] + }, + "image/vnd.fujixerox.edmics-rlc": { + "source": "iana", + "extensions": ["rlc"] + }, + "image/vnd.globalgraphics.pgb": { + "source": "iana" + }, + "image/vnd.microsoft.icon": { + "source": "iana", + "extensions": ["ico"] + }, + "image/vnd.mix": { + "source": "iana" + }, + "image/vnd.mozilla.apng": { + "source": "iana" + }, + "image/vnd.ms-dds": { + "extensions": ["dds"] + }, + "image/vnd.ms-modi": { + "source": "iana", + "extensions": ["mdi"] + }, + "image/vnd.ms-photo": { + "source": "apache", + "extensions": ["wdp"] + }, + "image/vnd.net-fpx": { + "source": "iana", + "extensions": ["npx"] + }, + "image/vnd.radiance": { + "source": "iana" + }, + "image/vnd.sealed.png": { + "source": "iana" + }, + "image/vnd.sealedmedia.softseal.gif": { + "source": "iana" + }, + "image/vnd.sealedmedia.softseal.jpg": { + "source": "iana" + }, + "image/vnd.svf": { + "source": "iana" + }, + "image/vnd.tencent.tap": { + "source": "iana", + "extensions": ["tap"] + }, + "image/vnd.valve.source.texture": { + "source": "iana", + "extensions": ["vtf"] + }, + "image/vnd.wap.wbmp": { + "source": "iana", + "extensions": ["wbmp"] + }, + "image/vnd.xiff": { + "source": "iana", + "extensions": ["xif"] + }, + "image/vnd.zbrush.pcx": { + "source": "iana", + "extensions": ["pcx"] + }, + "image/webp": { + "source": "apache", + "extensions": ["webp"] + }, + "image/wmf": { + "source": "iana", + "extensions": ["wmf"] + }, + "image/x-3ds": { + "source": "apache", + "extensions": ["3ds"] + }, + "image/x-cmu-raster": { + "source": "apache", + "extensions": ["ras"] + }, + "image/x-cmx": { + "source": "apache", + "extensions": ["cmx"] + }, + "image/x-freehand": { + "source": "apache", + "extensions": ["fh","fhc","fh4","fh5","fh7"] + }, + "image/x-icon": { + "source": "apache", + "compressible": true, + "extensions": ["ico"] + }, + "image/x-jng": { + "source": "nginx", + "extensions": ["jng"] + }, + "image/x-mrsid-image": { + "source": "apache", + "extensions": ["sid"] + }, + "image/x-ms-bmp": { + "source": "nginx", + "compressible": true, + "extensions": ["bmp"] + }, + "image/x-pcx": { + "source": "apache", + "extensions": ["pcx"] + }, + "image/x-pict": { + "source": "apache", + "extensions": ["pic","pct"] + }, + "image/x-portable-anymap": { + "source": "apache", + "extensions": ["pnm"] + }, + "image/x-portable-bitmap": { + "source": "apache", + "extensions": ["pbm"] + }, + "image/x-portable-graymap": { + "source": "apache", + "extensions": ["pgm"] + }, + "image/x-portable-pixmap": { + "source": "apache", + "extensions": ["ppm"] + }, + "image/x-rgb": { + "source": "apache", + "extensions": ["rgb"] + }, + "image/x-tga": { + "source": "apache", + "extensions": ["tga"] + }, + "image/x-xbitmap": { + "source": "apache", + "extensions": ["xbm"] + }, + "image/x-xcf": { + "compressible": false + }, + "image/x-xpixmap": { + "source": "apache", + "extensions": ["xpm"] + }, + "image/x-xwindowdump": { + "source": "apache", + "extensions": ["xwd"] + }, + "message/cpim": { + "source": "iana" + }, + "message/delivery-status": { + "source": "iana" + }, + "message/disposition-notification": { + "source": "iana", + "extensions": [ + "disposition-notification" + ] + }, + "message/external-body": { + "source": "iana" + }, + "message/feedback-report": { + "source": "iana" + }, + "message/global": { + "source": "iana", + "extensions": ["u8msg"] + }, + "message/global-delivery-status": { + "source": "iana", + "extensions": ["u8dsn"] + }, + "message/global-disposition-notification": { + "source": "iana", + "extensions": ["u8mdn"] + }, + "message/global-headers": { + "source": "iana", + "extensions": ["u8hdr"] + }, + "message/http": { + "source": "iana", + "compressible": false + }, + "message/imdn+xml": { + "source": "iana", + "compressible": true + }, + "message/news": { + "source": "iana" + }, + "message/partial": { + "source": "iana", + "compressible": false + }, + "message/rfc822": { + "source": "iana", + "compressible": true, + "extensions": ["eml","mime"] + }, + "message/s-http": { + "source": "iana" + }, + "message/sip": { + "source": "iana" + }, + "message/sipfrag": { + "source": "iana" + }, + "message/tracking-status": { + "source": "iana" + }, + "message/vnd.si.simp": { + "source": "iana" + }, + "message/vnd.wfa.wsc": { + "source": "iana", + "extensions": ["wsc"] + }, + "model/3mf": { + "source": "iana", + "extensions": ["3mf"] + }, + "model/gltf+json": { + "source": "iana", + "compressible": true, + "extensions": ["gltf"] + }, + "model/gltf-binary": { + "source": "iana", + "compressible": true, + "extensions": ["glb"] + }, + "model/iges": { + "source": "iana", + "compressible": false, + "extensions": ["igs","iges"] + }, + "model/mesh": { + "source": "iana", + "compressible": false, + "extensions": ["msh","mesh","silo"] + }, + "model/stl": { + "source": "iana", + "extensions": ["stl"] + }, + "model/vnd.collada+xml": { + "source": "iana", + "compressible": true, + "extensions": ["dae"] + }, + "model/vnd.dwf": { + "source": "iana", + "extensions": ["dwf"] + }, + "model/vnd.flatland.3dml": { + "source": "iana" + }, + "model/vnd.gdl": { + "source": "iana", + "extensions": ["gdl"] + }, + "model/vnd.gs-gdl": { + "source": "apache" + }, + "model/vnd.gs.gdl": { + "source": "iana" + }, + "model/vnd.gtw": { + "source": "iana", + "extensions": ["gtw"] + }, + "model/vnd.moml+xml": { + "source": "iana", + "compressible": true + }, + "model/vnd.mts": { + "source": "iana", + "extensions": ["mts"] + }, + "model/vnd.opengex": { + "source": "iana", + "extensions": ["ogex"] + }, + "model/vnd.parasolid.transmit.binary": { + "source": "iana", + "extensions": ["x_b"] + }, + "model/vnd.parasolid.transmit.text": { + "source": "iana", + "extensions": ["x_t"] + }, + "model/vnd.rosette.annotated-data-model": { + "source": "iana" + }, + "model/vnd.usdz+zip": { + "source": "iana", + "compressible": false, + "extensions": ["usdz"] + }, + "model/vnd.valve.source.compiled-map": { + "source": "iana", + "extensions": ["bsp"] + }, + "model/vnd.vtu": { + "source": "iana", + "extensions": ["vtu"] + }, + "model/vrml": { + "source": "iana", + "compressible": false, + "extensions": ["wrl","vrml"] + }, + "model/x3d+binary": { + "source": "apache", + "compressible": false, + "extensions": ["x3db","x3dbz"] + }, + "model/x3d+fastinfoset": { + "source": "iana", + "extensions": ["x3db"] + }, + "model/x3d+vrml": { + "source": "apache", + "compressible": false, + "extensions": ["x3dv","x3dvz"] + }, + "model/x3d+xml": { + "source": "iana", + "compressible": true, + "extensions": ["x3d","x3dz"] + }, + "model/x3d-vrml": { + "source": "iana", + "extensions": ["x3dv"] + }, + "multipart/alternative": { + "source": "iana", + "compressible": false + }, + "multipart/appledouble": { + "source": "iana" + }, + "multipart/byteranges": { + "source": "iana" + }, + "multipart/digest": { + "source": "iana" + }, + "multipart/encrypted": { + "source": "iana", + "compressible": false + }, + "multipart/form-data": { + "source": "iana", + "compressible": false + }, + "multipart/header-set": { + "source": "iana" + }, + "multipart/mixed": { + "source": "iana" + }, + "multipart/multilingual": { + "source": "iana" + }, + "multipart/parallel": { + "source": "iana" + }, + "multipart/related": { + "source": "iana", + "compressible": false + }, + "multipart/report": { + "source": "iana" + }, + "multipart/signed": { + "source": "iana", + "compressible": false + }, + "multipart/vnd.bint.med-plus": { + "source": "iana" + }, + "multipart/voice-message": { + "source": "iana" + }, + "multipart/x-mixed-replace": { + "source": "iana" + }, + "text/1d-interleaved-parityfec": { + "source": "iana" + }, + "text/cache-manifest": { + "source": "iana", + "compressible": true, + "extensions": ["appcache","manifest"] + }, + "text/calendar": { + "source": "iana", + "extensions": ["ics","ifb"] + }, + "text/calender": { + "compressible": true + }, + "text/cmd": { + "compressible": true + }, + "text/coffeescript": { + "extensions": ["coffee","litcoffee"] + }, + "text/css": { + "source": "iana", + "charset": "UTF-8", + "compressible": true, + "extensions": ["css"] + }, + "text/csv": { + "source": "iana", + "compressible": true, + "extensions": ["csv"] + }, + "text/csv-schema": { + "source": "iana" + }, + "text/directory": { + "source": "iana" + }, + "text/dns": { + "source": "iana" + }, + "text/ecmascript": { + "source": "iana" + }, + "text/encaprtp": { + "source": "iana" + }, + "text/enriched": { + "source": "iana" + }, + "text/flexfec": { + "source": "iana" + }, + "text/fwdred": { + "source": "iana" + }, + "text/grammar-ref-list": { + "source": "iana" + }, + "text/html": { + "source": "iana", + "compressible": true, + "extensions": ["html","htm","shtml"] + }, + "text/jade": { + "extensions": ["jade"] + }, + "text/javascript": { + "source": "iana", + "compressible": true + }, + "text/jcr-cnd": { + "source": "iana" + }, + "text/jsx": { + "compressible": true, + "extensions": ["jsx"] + }, + "text/less": { + "compressible": true, + "extensions": ["less"] + }, + "text/markdown": { + "source": "iana", + "compressible": true, + "extensions": ["markdown","md"] + }, + "text/mathml": { + "source": "nginx", + "extensions": ["mml"] + }, + "text/mdx": { + "compressible": true, + "extensions": ["mdx"] + }, + "text/mizar": { + "source": "iana" + }, + "text/n3": { + "source": "iana", + "compressible": true, + "extensions": ["n3"] + }, + "text/parameters": { + "source": "iana" + }, + "text/parityfec": { + "source": "iana" + }, + "text/plain": { + "source": "iana", + "compressible": true, + "extensions": ["txt","text","conf","def","list","log","in","ini"] + }, + "text/provenance-notation": { + "source": "iana" + }, + "text/prs.fallenstein.rst": { + "source": "iana" + }, + "text/prs.lines.tag": { + "source": "iana", + "extensions": ["dsc"] + }, + "text/prs.prop.logic": { + "source": "iana" + }, + "text/raptorfec": { + "source": "iana" + }, + "text/red": { + "source": "iana" + }, + "text/rfc822-headers": { + "source": "iana" + }, + "text/richtext": { + "source": "iana", + "compressible": true, + "extensions": ["rtx"] + }, + "text/rtf": { + "source": "iana", + "compressible": true, + "extensions": ["rtf"] + }, + "text/rtp-enc-aescm128": { + "source": "iana" + }, + "text/rtploopback": { + "source": "iana" + }, + "text/rtx": { + "source": "iana" + }, + "text/sgml": { + "source": "iana", + "extensions": ["sgml","sgm"] + }, + "text/shex": { + "extensions": ["shex"] + }, + "text/slim": { + "extensions": ["slim","slm"] + }, + "text/strings": { + "source": "iana" + }, + "text/stylus": { + "extensions": ["stylus","styl"] + }, + "text/t140": { + "source": "iana" + }, + "text/tab-separated-values": { + "source": "iana", + "compressible": true, + "extensions": ["tsv"] + }, + "text/troff": { + "source": "iana", + "extensions": ["t","tr","roff","man","me","ms"] + }, + "text/turtle": { + "source": "iana", + "charset": "UTF-8", + "extensions": ["ttl"] + }, + "text/ulpfec": { + "source": "iana" + }, + "text/uri-list": { + "source": "iana", + "compressible": true, + "extensions": ["uri","uris","urls"] + }, + "text/vcard": { + "source": "iana", + "compressible": true, + "extensions": ["vcard"] + }, + "text/vnd.a": { + "source": "iana" + }, + "text/vnd.abc": { + "source": "iana" + }, + "text/vnd.ascii-art": { + "source": "iana" + }, + "text/vnd.curl": { + "source": "iana", + "extensions": ["curl"] + }, + "text/vnd.curl.dcurl": { + "source": "apache", + "extensions": ["dcurl"] + }, + "text/vnd.curl.mcurl": { + "source": "apache", + "extensions": ["mcurl"] + }, + "text/vnd.curl.scurl": { + "source": "apache", + "extensions": ["scurl"] + }, + "text/vnd.debian.copyright": { + "source": "iana" + }, + "text/vnd.dmclientscript": { + "source": "iana" + }, + "text/vnd.dvb.subtitle": { + "source": "iana", + "extensions": ["sub"] + }, + "text/vnd.esmertec.theme-descriptor": { + "source": "iana" + }, + "text/vnd.ficlab.flt": { + "source": "iana" + }, + "text/vnd.fly": { + "source": "iana", + "extensions": ["fly"] + }, + "text/vnd.fmi.flexstor": { + "source": "iana", + "extensions": ["flx"] + }, + "text/vnd.gml": { + "source": "iana" + }, + "text/vnd.graphviz": { + "source": "iana", + "extensions": ["gv"] + }, + "text/vnd.hgl": { + "source": "iana" + }, + "text/vnd.in3d.3dml": { + "source": "iana", + "extensions": ["3dml"] + }, + "text/vnd.in3d.spot": { + "source": "iana", + "extensions": ["spot"] + }, + "text/vnd.iptc.newsml": { + "source": "iana" + }, + "text/vnd.iptc.nitf": { + "source": "iana" + }, + "text/vnd.latex-z": { + "source": "iana" + }, + "text/vnd.motorola.reflex": { + "source": "iana" + }, + "text/vnd.ms-mediapackage": { + "source": "iana" + }, + "text/vnd.net2phone.commcenter.command": { + "source": "iana" + }, + "text/vnd.radisys.msml-basic-layout": { + "source": "iana" + }, + "text/vnd.senx.warpscript": { + "source": "iana" + }, + "text/vnd.si.uricatalogue": { + "source": "iana" + }, + "text/vnd.sosi": { + "source": "iana" + }, + "text/vnd.sun.j2me.app-descriptor": { + "source": "iana", + "extensions": ["jad"] + }, + "text/vnd.trolltech.linguist": { + "source": "iana" + }, + "text/vnd.wap.si": { + "source": "iana" + }, + "text/vnd.wap.sl": { + "source": "iana" + }, + "text/vnd.wap.wml": { + "source": "iana", + "extensions": ["wml"] + }, + "text/vnd.wap.wmlscript": { + "source": "iana", + "extensions": ["wmls"] + }, + "text/vtt": { + "source": "iana", + "charset": "UTF-8", + "compressible": true, + "extensions": ["vtt"] + }, + "text/x-asm": { + "source": "apache", + "extensions": ["s","asm"] + }, + "text/x-c": { + "source": "apache", + "extensions": ["c","cc","cxx","cpp","h","hh","dic"] + }, + "text/x-component": { + "source": "nginx", + "extensions": ["htc"] + }, + "text/x-fortran": { + "source": "apache", + "extensions": ["f","for","f77","f90"] + }, + "text/x-gwt-rpc": { + "compressible": true + }, + "text/x-handlebars-template": { + "extensions": ["hbs"] + }, + "text/x-java-source": { + "source": "apache", + "extensions": ["java"] + }, + "text/x-jquery-tmpl": { + "compressible": true + }, + "text/x-lua": { + "extensions": ["lua"] + }, + "text/x-markdown": { + "compressible": true, + "extensions": ["mkd"] + }, + "text/x-nfo": { + "source": "apache", + "extensions": ["nfo"] + }, + "text/x-opml": { + "source": "apache", + "extensions": ["opml"] + }, + "text/x-org": { + "compressible": true, + "extensions": ["org"] + }, + "text/x-pascal": { + "source": "apache", + "extensions": ["p","pas"] + }, + "text/x-processing": { + "compressible": true, + "extensions": ["pde"] + }, + "text/x-sass": { + "extensions": ["sass"] + }, + "text/x-scss": { + "extensions": ["scss"] + }, + "text/x-setext": { + "source": "apache", + "extensions": ["etx"] + }, + "text/x-sfv": { + "source": "apache", + "extensions": ["sfv"] + }, + "text/x-suse-ymp": { + "compressible": true, + "extensions": ["ymp"] + }, + "text/x-uuencode": { + "source": "apache", + "extensions": ["uu"] + }, + "text/x-vcalendar": { + "source": "apache", + "extensions": ["vcs"] + }, + "text/x-vcard": { + "source": "apache", + "extensions": ["vcf"] + }, + "text/xml": { + "source": "iana", + "compressible": true, + "extensions": ["xml"] + }, + "text/xml-external-parsed-entity": { + "source": "iana" + }, + "text/yaml": { + "extensions": ["yaml","yml"] + }, + "video/1d-interleaved-parityfec": { + "source": "iana" + }, + "video/3gpp": { + "source": "iana", + "extensions": ["3gp","3gpp"] + }, + "video/3gpp-tt": { + "source": "iana" + }, + "video/3gpp2": { + "source": "iana", + "extensions": ["3g2"] + }, + "video/bmpeg": { + "source": "iana" + }, + "video/bt656": { + "source": "iana" + }, + "video/celb": { + "source": "iana" + }, + "video/dv": { + "source": "iana" + }, + "video/encaprtp": { + "source": "iana" + }, + "video/flexfec": { + "source": "iana" + }, + "video/h261": { + "source": "iana", + "extensions": ["h261"] + }, + "video/h263": { + "source": "iana", + "extensions": ["h263"] + }, + "video/h263-1998": { + "source": "iana" + }, + "video/h263-2000": { + "source": "iana" + }, + "video/h264": { + "source": "iana", + "extensions": ["h264"] + }, + "video/h264-rcdo": { + "source": "iana" + }, + "video/h264-svc": { + "source": "iana" + }, + "video/h265": { + "source": "iana" + }, + "video/iso.segment": { + "source": "iana" + }, + "video/jpeg": { + "source": "iana", + "extensions": ["jpgv"] + }, + "video/jpeg2000": { + "source": "iana" + }, + "video/jpm": { + "source": "apache", + "extensions": ["jpm","jpgm"] + }, + "video/mj2": { + "source": "iana", + "extensions": ["mj2","mjp2"] + }, + "video/mp1s": { + "source": "iana" + }, + "video/mp2p": { + "source": "iana" + }, + "video/mp2t": { + "source": "iana", + "extensions": ["ts"] + }, + "video/mp4": { + "source": "iana", + "compressible": false, + "extensions": ["mp4","mp4v","mpg4"] + }, + "video/mp4v-es": { + "source": "iana" + }, + "video/mpeg": { + "source": "iana", + "compressible": false, + "extensions": ["mpeg","mpg","mpe","m1v","m2v"] + }, + "video/mpeg4-generic": { + "source": "iana" + }, + "video/mpv": { + "source": "iana" + }, + "video/nv": { + "source": "iana" + }, + "video/ogg": { + "source": "iana", + "compressible": false, + "extensions": ["ogv"] + }, + "video/parityfec": { + "source": "iana" + }, + "video/pointer": { + "source": "iana" + }, + "video/quicktime": { + "source": "iana", + "compressible": false, + "extensions": ["qt","mov"] + }, + "video/raptorfec": { + "source": "iana" + }, + "video/raw": { + "source": "iana" + }, + "video/rtp-enc-aescm128": { + "source": "iana" + }, + "video/rtploopback": { + "source": "iana" + }, + "video/rtx": { + "source": "iana" + }, + "video/smpte291": { + "source": "iana" + }, + "video/smpte292m": { + "source": "iana" + }, + "video/ulpfec": { + "source": "iana" + }, + "video/vc1": { + "source": "iana" + }, + "video/vc2": { + "source": "iana" + }, + "video/vnd.cctv": { + "source": "iana" + }, + "video/vnd.dece.hd": { + "source": "iana", + "extensions": ["uvh","uvvh"] + }, + "video/vnd.dece.mobile": { + "source": "iana", + "extensions": ["uvm","uvvm"] + }, + "video/vnd.dece.mp4": { + "source": "iana" + }, + "video/vnd.dece.pd": { + "source": "iana", + "extensions": ["uvp","uvvp"] + }, + "video/vnd.dece.sd": { + "source": "iana", + "extensions": ["uvs","uvvs"] + }, + "video/vnd.dece.video": { + "source": "iana", + "extensions": ["uvv","uvvv"] + }, + "video/vnd.directv.mpeg": { + "source": "iana" + }, + "video/vnd.directv.mpeg-tts": { + "source": "iana" + }, + "video/vnd.dlna.mpeg-tts": { + "source": "iana" + }, + "video/vnd.dvb.file": { + "source": "iana", + "extensions": ["dvb"] + }, + "video/vnd.fvt": { + "source": "iana", + "extensions": ["fvt"] + }, + "video/vnd.hns.video": { + "source": "iana" + }, + "video/vnd.iptvforum.1dparityfec-1010": { + "source": "iana" + }, + "video/vnd.iptvforum.1dparityfec-2005": { + "source": "iana" + }, + "video/vnd.iptvforum.2dparityfec-1010": { + "source": "iana" + }, + "video/vnd.iptvforum.2dparityfec-2005": { + "source": "iana" + }, + "video/vnd.iptvforum.ttsavc": { + "source": "iana" + }, + "video/vnd.iptvforum.ttsmpeg2": { + "source": "iana" + }, + "video/vnd.motorola.video": { + "source": "iana" + }, + "video/vnd.motorola.videop": { + "source": "iana" + }, + "video/vnd.mpegurl": { + "source": "iana", + "extensions": ["mxu","m4u"] + }, + "video/vnd.ms-playready.media.pyv": { + "source": "iana", + "extensions": ["pyv"] + }, + "video/vnd.nokia.interleaved-multimedia": { + "source": "iana" + }, + "video/vnd.nokia.mp4vr": { + "source": "iana" + }, + "video/vnd.nokia.videovoip": { + "source": "iana" + }, + "video/vnd.objectvideo": { + "source": "iana" + }, + "video/vnd.radgamettools.bink": { + "source": "iana" + }, + "video/vnd.radgamettools.smacker": { + "source": "iana" + }, + "video/vnd.sealed.mpeg1": { + "source": "iana" + }, + "video/vnd.sealed.mpeg4": { + "source": "iana" + }, + "video/vnd.sealed.swf": { + "source": "iana" + }, + "video/vnd.sealedmedia.softseal.mov": { + "source": "iana" + }, + "video/vnd.uvvu.mp4": { + "source": "iana", + "extensions": ["uvu","uvvu"] + }, + "video/vnd.vivo": { + "source": "iana", + "extensions": ["viv"] + }, + "video/vnd.youtube.yt": { + "source": "iana" + }, + "video/vp8": { + "source": "iana" + }, + "video/webm": { + "source": "apache", + "compressible": false, + "extensions": ["webm"] + }, + "video/x-f4v": { + "source": "apache", + "extensions": ["f4v"] + }, + "video/x-fli": { + "source": "apache", + "extensions": ["fli"] + }, + "video/x-flv": { + "source": "apache", + "compressible": false, + "extensions": ["flv"] + }, + "video/x-m4v": { + "source": "apache", + "extensions": ["m4v"] + }, + "video/x-matroska": { + "source": "apache", + "compressible": false, + "extensions": ["mkv","mk3d","mks"] + }, + "video/x-mng": { + "source": "apache", + "extensions": ["mng"] + }, + "video/x-ms-asf": { + "source": "apache", + "extensions": ["asf","asx"] + }, + "video/x-ms-vob": { + "source": "apache", + "extensions": ["vob"] + }, + "video/x-ms-wm": { + "source": "apache", + "extensions": ["wm"] + }, + "video/x-ms-wmv": { + "source": "apache", + "compressible": false, + "extensions": ["wmv"] + }, + "video/x-ms-wmx": { + "source": "apache", + "extensions": ["wmx"] + }, + "video/x-ms-wvx": { + "source": "apache", + "extensions": ["wvx"] + }, + "video/x-msvideo": { + "source": "apache", + "extensions": ["avi"] + }, + "video/x-sgi-movie": { + "source": "apache", + "extensions": ["movie"] + }, + "video/x-smv": { + "source": "apache", + "extensions": ["smv"] + }, + "x-conference/x-cooltalk": { + "source": "apache", + "extensions": ["ice"] + }, + "x-shader/x-fragment": { + "compressible": true + }, + "x-shader/x-vertex": { + "compressible": true + } +} diff --git a/apps/dmt-search/dmt/connectome-next/lib/utils/mime/index.js b/apps/dmt-search/dmt/connectome-next/lib/utils/mime/index.js new file mode 100644 index 000000000..3ac83cf28 --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/utils/mime/index.js @@ -0,0 +1,184 @@ +/*! + * mime-types + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +/** + * Module dependencies. + * @private + */ + +var db = require('./db.json'); +// todo: improve ? +var extname = filePath => (filePath.indexOf('.') > -1 ? '.' + filePath.split('.').slice(-1)[0] : undefined); + +/** + * Module variables. + * @private + */ + +var EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/; +var TEXT_TYPE_REGEXP = /^text\//i; + +/** + * Module exports. + * @public + */ + +exports.charset = charset; +exports.charsets = { lookup: charset }; +exports.contentType = contentType; +exports.extension = extension; +exports.extensions = Object.create(null); +exports.lookup = lookup; +exports.types = Object.create(null); + +// Populate the extensions/types maps +populateMaps(exports.extensions, exports.types); + +/** + * Get the default charset for a MIME type. + * + * @param {string} type + * @return {boolean|string} + */ + +function charset(type) { + if (!type || typeof type !== 'string') { + return false; + } + + // TODO: use media-typer + var match = EXTRACT_TYPE_REGEXP.exec(type); + var mime = match && db[match[1].toLowerCase()]; + + if (mime && mime.charset) { + return mime.charset; + } + + // default text/* to utf-8 + if (match && TEXT_TYPE_REGEXP.test(match[1])) { + return 'UTF-8'; + } + + return false; +} + +/** + * Create a full Content-Type header given a MIME type or extension. + * + * @param {string} str + * @return {boolean|string} + */ + +function contentType(str) { + // TODO: should this even be in this module? + if (!str || typeof str !== 'string') { + return false; + } + + var mime = str.indexOf('/') === -1 ? exports.lookup(str) : str; + + if (!mime) { + return false; + } + + // TODO: use content-type or other module + if (mime.indexOf('charset') === -1) { + var charset = exports.charset(mime); + if (charset) mime += '; charset=' + charset.toLowerCase(); + } + + return mime; +} + +/** + * Get the default extension for a MIME type. + * + * @param {string} type + * @return {boolean|string} + */ + +function extension(type) { + if (!type || typeof type !== 'string') { + return false; + } + + // TODO: use media-typer + var match = EXTRACT_TYPE_REGEXP.exec(type); + + // get extensions + var exts = match && exports.extensions[match[1].toLowerCase()]; + + if (!exts || !exts.length) { + return false; + } + + return exts[0]; +} + +/** + * Lookup the MIME type for a file path/extension. + * + * @param {string} path + * @return {boolean|string} + */ + +function lookup(path) { + if (!path || typeof path !== 'string') { + return false; + } + + // get the extension ("ext" or ".ext" or full path) + var extension = extname('x.' + path) + .toLowerCase() + .substr(1); + + if (!extension) { + return false; + } + + return exports.types[extension] || false; +} + +/** + * Populate the extensions and types maps. + * @private + */ + +function populateMaps(extensions, types) { + // source preference (least -> most) + var preference = ['nginx', 'apache', undefined, 'iana']; + + Object.keys(db).forEach(function forEachMimeType(type) { + var mime = db[type]; + var exts = mime.extensions; + + if (!exts || !exts.length) { + return; + } + + // mime -> extensions + extensions[type] = exts; + + // extension -> mime + for (var i = 0; i < exts.length; i++) { + var extension = exts[i]; + + if (types[extension]) { + var from = preference.indexOf(db[types[extension]].source); + var to = preference.indexOf(mime.source); + + if (types[extension] !== 'application/octet-stream' && (from > to || (from === to && types[extension].substr(0, 12) === 'application/'))) { + // skip the remapping + continue; + } + } + + // set the extension -> mime + types[extension] = type; + } + }); +} diff --git a/apps/dmt-search/dmt/connectome-next/lib/utils/mime/package.json b/apps/dmt-search/dmt/connectome-next/lib/utils/mime/package.json new file mode 100644 index 000000000..3fd1658d7 --- /dev/null +++ b/apps/dmt-search/dmt/connectome-next/lib/utils/mime/package.json @@ -0,0 +1,87 @@ +{ + "_from": "mime-types", + "_id": "mime-types@2.1.26", + "_inBundle": false, + "_integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "_location": "/mime-types", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "mime-types", + "name": "mime-types", + "escapedName": "mime-types", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "_shasum": "9c921fc09b7e149a65dfdc0da4d20997200b0a06", + "_spec": "mime-types", + "_where": "/Users/david/Desktop/mime", + "bugs": { + "url": "https://github.com/jshttp/mime-types/issues" + }, + "bundleDependencies": false, + "contributors": [ + { + "name": "Douglas Christopher Wilson", + "email": "doug@somethingdoug.com" + }, + { + "name": "Jeremiah Senkpiel", + "email": "fishrock123@rocketmail.com", + "url": "https://searchbeam.jit.su" + }, + { + "name": "Jonathan Ong", + "email": "me@jongleberry.com", + "url": "http://jongleberry.com" + } + ], + "dependencies": { + "mime-db": "1.43.0" + }, + "deprecated": false, + "description": "The ultimate javascript content-type utility.", + "devDependencies": { + "eslint": "6.8.0", + "eslint-config-standard": "14.1.0", + "eslint-plugin-import": "2.19.1", + "eslint-plugin-node": "11.0.0", + "eslint-plugin-promise": "4.2.1", + "eslint-plugin-standard": "4.0.1", + "mocha": "7.0.0", + "nyc": "15.0.0" + }, + "engines": { + "node": ">= 0.6" + }, + "files": [ + "HISTORY.md", + "LICENSE", + "index.js" + ], + "homepage": "https://github.com/jshttp/mime-types#readme", + "keywords": [ + "mime", + "types" + ], + "license": "MIT", + "name": "mime-types", + "repository": { + "type": "git", + "url": "git+https://github.com/jshttp/mime-types.git" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --reporter spec test/test.js", + "test-cov": "nyc --reporter=html --reporter=text npm test", + "test-travis": "nyc --reporter=text npm test" + }, + "version": "2.1.26" +} diff --git a/apps/dmt-search/dmt/protocol/searchGUI/index.js b/apps/dmt-search/dmt/protocol/searchGUI/index.js index 5f7e6b759..b84850679 100644 --- a/apps/dmt-search/dmt/protocol/searchGUI/index.js +++ b/apps/dmt-search/dmt/protocol/searchGUI/index.js @@ -1,4 +1,4 @@ -import { SyncStore } from 'dmt/connectome-stores'; +import { SyncStore } from 'connectome/stores'; import onConnect from './onConnect.js'; diff --git a/apps/dmt-search/dmt/protocol/searchGUI/objects/search.js b/apps/dmt-search/dmt/protocol/searchGUI/objects/search.js index 8c4d971be..e1cf82a88 100644 --- a/apps/dmt-search/dmt/protocol/searchGUI/objects/search.js +++ b/apps/dmt-search/dmt/protocol/searchGUI/objects/search.js @@ -4,7 +4,7 @@ import { push } from 'dmt/notify'; import { parseSearchQuery, serializeContentRefs } from 'dmt/search'; //import getContentProviders from '../getContentProviders'; -import { fiberHandle } from 'dmt/connectome-next'; +import { fiberHandle } from 'connectome-next'; const RESULTS_LIMIT = 20; diff --git a/apps/node_modules/.package-lock.json b/apps/node_modules/.package-lock.json new file mode 100644 index 000000000..ca00725a9 --- /dev/null +++ b/apps/node_modules/.package-lock.json @@ -0,0 +1,41 @@ +{ + "name": "dmt", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "../core/node/connectome": { + "version": "0.2.9", + "dev": true, + "license": "ISC", + "dependencies": { + "browser-util-inspect": "^0.2.0", + "bufferutil": "^4.0.2", + "fast-json-patch": "^3.0.0-1", + "kleur": "^4.1.5", + "quantum-generator": "^1.9.1", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1", + "utf-8-validate": "^5.0.3", + "ws": "^7.4.5" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^16.0.0", + "@rollup/plugin-node-resolve": "^10.0.0", + "builtin-modules": "^3.1.0", + "rollup": "^2.33.3" + } + }, + "../core/node/connectome-next": { + "dev": true + }, + "node_modules/connectome": { + "resolved": "../core/node/connectome", + "link": true + }, + "node_modules/connectome-next": { + "resolved": "../core/node/connectome-next", + "link": true + } + } +} diff --git a/apps/node_modules/connectome b/apps/node_modules/connectome new file mode 120000 index 000000000..bde536dab --- /dev/null +++ b/apps/node_modules/connectome @@ -0,0 +1 @@ +../../core/node/connectome \ No newline at end of file diff --git a/apps/node_modules/connectome-next b/apps/node_modules/connectome-next new file mode 120000 index 000000000..41134d898 --- /dev/null +++ b/apps/node_modules/connectome-next @@ -0,0 +1 @@ +../../core/node/connectome-next \ No newline at end of file diff --git a/apps/package-lock.json b/apps/package-lock.json new file mode 100644 index 000000000..db5cca2fc --- /dev/null +++ b/apps/package-lock.json @@ -0,0 +1,49 @@ +{ + "name": "dmt", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dmt", + "version": "0.0.1", + "devDependencies": { + "connectome": "file:~/.dmt/core/node/connectome", + "connectome-next": "file:~/.dmt/core/node/connectome-next" + } + }, + "../core/node/connectome": { + "version": "0.2.9", + "dev": true, + "license": "ISC", + "dependencies": { + "browser-util-inspect": "^0.2.0", + "bufferutil": "^4.0.2", + "fast-json-patch": "^3.0.0-1", + "kleur": "^4.1.5", + "quantum-generator": "^1.9.1", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1", + "utf-8-validate": "^5.0.3", + "ws": "^7.4.5" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^16.0.0", + "@rollup/plugin-node-resolve": "^10.0.0", + "builtin-modules": "^3.1.0", + "rollup": "^2.33.3" + } + }, + "../core/node/connectome-next": { + "dev": true + }, + "node_modules/connectome": { + "resolved": "../core/node/connectome", + "link": true + }, + "node_modules/connectome-next": { + "resolved": "../core/node/connectome-next", + "link": true + } + } +} diff --git a/apps/package.json b/apps/package.json index 28ddc9206..1a1222af3 100644 --- a/apps/package.json +++ b/apps/package.json @@ -7,9 +7,10 @@ "exports": { "./common": "./_dmt_deps/common/index.js", "./notify": "./_dmt_deps/notify/index.js", - "./search": "./_dmt_deps/search/index.js", - "./connectome": "./_dmt_deps/connectome/index.js", - "./connectome-stores": "./_dmt_deps/connectome-stores/index.js", - "./connectome-next": "./_dmt_deps/connectome-next/index.js" + "./search": "./_dmt_deps/search/index.js" + }, + "devDependencies": { + "connectome": "file:~/.dmt/core/node/connectome", + "connectome-next": "file:~/.dmt/core/node/connectome-next" } } diff --git a/core/lib/dmt-frontend-components/src/components/Meetup.svelte b/core/lib/dmt-frontend-components/src/components/Meetup--brisi-after-kriptosola-rewrite.svelte similarity index 99% rename from core/lib/dmt-frontend-components/src/components/Meetup.svelte rename to core/lib/dmt-frontend-components/src/components/Meetup--brisi-after-kriptosola-rewrite.svelte index 4c63c1699..ccfb121c7 100644 --- a/core/lib/dmt-frontend-components/src/components/Meetup.svelte +++ b/core/lib/dmt-frontend-components/src/components/Meetup--brisi-after-kriptosola-rewrite.svelte @@ -21,7 +21,7 @@ let t_try_join = lang != 'sl' ? "You can still try to join" : "Lahko se poskusite pridružiti"; let t_now = lang != 'sl' ? "Now" : "zdaj"; let t_live = lang != 'sl' ? "Event is live" : "Trenutno poteka"; - let t_just_ended = lang != 'sl' ? "Meetup has probably already concluded but" : "Dogodek se morda že zaključuje"; + let t_just_ended = lang != 'sl' ? "Meetup has probably concluded but" : "Dogodek se morda že zaključuje"; let t_today = lang != 'sl' ? "Today" : "Danes"; let t_tomorrow = lang != 'sl' ? "Tomorrow" : "Jutri"; diff --git a/core/lib/dmt-frontend-components/src/index.js b/core/lib/dmt-frontend-components/src/index.js index e360d3b06..13c91b6cb 100644 --- a/core/lib/dmt-frontend-components/src/index.js +++ b/core/lib/dmt-frontend-components/src/index.js @@ -9,5 +9,5 @@ export { default as List } from './components/List.svelte'; export { default as ListItem } from './components/ListItem.svelte'; export { default as SearchableList } from './components/SearchableList.svelte'; export { default as Slider } from './components/Slider.svelte'; -export { default as Meetup } from './components/Meetup.svelte'; +export { default as MeetupKriptosola } from './components/Meetup--brisi-after-kriptosola-rewrite.svelte'; export { default as GuiErrors } from './components/GuiErrors.svelte'; diff --git a/core/lib/dmt-gui-kit/components/Noise.svelte b/core/lib/dmt-gui-kit/components/Noise.svelte new file mode 100644 index 000000000..b2b96ce47 --- /dev/null +++ b/core/lib/dmt-gui-kit/components/Noise.svelte @@ -0,0 +1,44 @@ +
+ + + + + + + + + + + + +
+ + diff --git a/core/lib/dmt-gui-kit/components/Noise.svelte.d.ts b/core/lib/dmt-gui-kit/components/Noise.svelte.d.ts new file mode 100644 index 000000000..dba86a235 --- /dev/null +++ b/core/lib/dmt-gui-kit/components/Noise.svelte.d.ts @@ -0,0 +1,23 @@ +/** @typedef {typeof __propDef.props} NoiseProps */ +/** @typedef {typeof __propDef.events} NoiseEvents */ +/** @typedef {typeof __propDef.slots} NoiseSlots */ +export default class Noise extends SvelteComponentTyped<{ + [x: string]: never; +}, { + [evt: string]: CustomEvent; +}, {}> { +} +export type NoiseProps = typeof __propDef.props; +export type NoiseEvents = typeof __propDef.events; +export type NoiseSlots = typeof __propDef.slots; +import { SvelteComponentTyped } from "svelte"; +declare const __propDef: { + props: { + [x: string]: never; + }; + events: { + [evt: string]: CustomEvent; + }; + slots: {}; +}; +export {}; diff --git a/core/lib/dmt-gui-kit/index.d.ts b/core/lib/dmt-gui-kit/index.d.ts index 76dd8cabe..71a27cda9 100644 --- a/core/lib/dmt-gui-kit/index.d.ts +++ b/core/lib/dmt-gui-kit/index.d.ts @@ -2,6 +2,7 @@ export { default as LogView } from './components/LogView.svelte'; export { default as Loading } from './components/Loading.svelte'; export { default as GuiErrors } from './components/GuiErrors.svelte'; export { default as SnackBar } from './components/SnackBar.svelte'; +export { default as Noise } from './components/Noise.svelte'; export { default as logStore } from './store/logStore'; export * from './store/snack'; export * from './utils/index'; diff --git a/core/lib/dmt-gui-kit/index.js b/core/lib/dmt-gui-kit/index.js index 76dd8cabe..71a27cda9 100644 --- a/core/lib/dmt-gui-kit/index.js +++ b/core/lib/dmt-gui-kit/index.js @@ -2,6 +2,7 @@ export { default as LogView } from './components/LogView.svelte'; export { default as Loading } from './components/Loading.svelte'; export { default as GuiErrors } from './components/GuiErrors.svelte'; export { default as SnackBar } from './components/SnackBar.svelte'; +export { default as Noise } from './components/Noise.svelte'; export { default as logStore } from './store/logStore'; export * from './store/snack'; export * from './utils/index'; diff --git a/core/lib/dmt-gui-kit/package.json b/core/lib/dmt-gui-kit/package.json index af7b9e1cc..b97cbff94 100644 --- a/core/lib/dmt-gui-kit/package.json +++ b/core/lib/dmt-gui-kit/package.json @@ -1,6 +1,6 @@ { "name": "dmt-gui-kit", - "version": "0.0.1", + "version": "0.0.2", "devDependencies": { "@playwright/test": "^1.28.1", "@sveltejs/adapter-auto": "^1.0.0", @@ -31,6 +31,7 @@ "./components/Loading.svelte": "./components/Loading.svelte", "./components/LogView.svelte": "./components/LogView.svelte", "./components/Logo.svelte": "./components/Logo.svelte", + "./components/Noise.svelte": "./components/Noise.svelte", "./components/SnackBar.svelte": "./components/SnackBar.svelte", "./environment": "./environment.js", "./icons/XIcon.svelte": "./icons/XIcon.svelte", diff --git a/core/node/aspect-extend/apps-load/appFrontendList.js b/core/node/aspect-extend/apps-load/appFrontendList.js index fb92decec..c565c488c 100644 --- a/core/node/aspect-extend/apps-load/appFrontendList.js +++ b/core/node/aspect-extend/apps-load/appFrontendList.js @@ -1,14 +1,14 @@ import path from 'path'; import fs from 'fs'; -import { scan, log, colors, dmtPath, dmtUserDir, dmtHerePath, isDevMachine } from 'dmt/common'; +import { scan, log, colors, dmtPath, dmtUserDir, dmtHerePath } from 'dmt/common'; export const appsDir = path.join(dmtPath, 'apps'); const userAppsDir = path.join(dmtUserDir, 'apps'); const deviceAppsDir = path.join(dmtHerePath, 'apps'); function getSubdirs(directory) { - return scan.dir(directory, { onlyDirs: true }).filter(dir => !['_dmt_deps'].includes(path.basename(dir))); + return scan.dir(directory, { onlyDirs: true }).filter(dir => !['_dmt_deps', 'node_modules'].includes(path.basename(dir))); } function systemAppList() { diff --git a/core/node/aspect-extend/apps-load/index.js b/core/node/aspect-extend/apps-load/index.js index 3bbee9266..bef093b6a 100644 --- a/core/node/aspect-extend/apps-load/index.js +++ b/core/node/aspect-extend/apps-load/index.js @@ -1,12 +1,43 @@ -import express from 'express'; - import fs from 'fs'; +import path from 'path'; -import { log, colors } from 'dmt/common'; +import { log, program } from 'dmt/common'; -import loadApps from './loadApps.js'; +import { loadApps, importComplex } from './loadApps.js'; import { appFrontendList, appsDir, allApps } from './appFrontendList.js'; +let _initialAppDefinitions; + +function reloadSSRHandler({ server, appDir }) { + return new Promise((success, reject) => { + const match = _initialAppDefinitions.find(a => a.appDir == appDir && a.hasSSRHandler); + + if (match) { + const appEntryFilePath = path.join(appDir, 'index.js'); + + importComplex(appEntryFilePath) + .then(({ handler }) => { + server.useDynamicSSR(match.appName, handler, true); + success(); + }) + .catch(e => { + program.exceptionNotify(e, `Error while reloading ${appDir} ssr handler, check log`); + reject(e); + }); + } + }); +} + +function reloadAllSSRHandlers({ server }) { + return new Promise((success, reject) => { + for (const { appDir, hasSSRHandler } of _initialAppDefinitions) { + if (hasSSRHandler) { + reloadSSRHandler({ server, appDir }).catch(reject); + } + } + }); +} + async function init(program) { program.slot('appList').set(appFrontendList()); @@ -16,6 +47,8 @@ async function init(program) { loadApps(allApps) .then(appDefinitions => { + _initialAppDefinitions = JSON.parse(JSON.stringify(appDefinitions)); + program.emit('apps_loaded', appDefinitions); }) .catch(e => { @@ -24,4 +57,4 @@ async function init(program) { }); } -export { init, appFrontendList }; +export { init, appFrontendList, reloadSSRHandler, reloadAllSSRHandlers }; diff --git a/core/node/aspect-extend/apps-load/loadApps.js b/core/node/aspect-extend/apps-load/loadApps.js index 31c57b266..2b63ee0cb 100644 --- a/core/node/aspect-extend/apps-load/loadApps.js +++ b/core/node/aspect-extend/apps-load/loadApps.js @@ -4,44 +4,54 @@ import stripAnsi from 'strip-ansi'; import { log, colors, program } from 'dmt/common'; -export default function loadApps(appList) { +export function loadApps(appList) { const promises = []; const appNames = []; + const appDirs = []; + const appEntries = []; appList.forEach(({ appDir }) => { const appEntryFilePath = path.join(appDir, 'index.js'); - const appEntryFilePathHook = path.join(appDir, 'dmt/index.js'); - if (fs.existsSync(appEntryFilePath) || fs.existsSync(appEntryFilePathHook)) { + const appEntrySubprogram = path.join(appDir, 'dmt/index.js'); + + if (fs.existsSync(appEntryFilePath) || fs.existsSync(appEntrySubprogram)) { const appName = path.basename(appDir); if (fs.existsSync(appEntryFilePath)) { appNames.push(appName); + appDirs.push(appDir); + appEntries.push(appEntryFilePath); promises.push(tryLoadApp(appEntryFilePath, appName)); } - if (fs.existsSync(appEntryFilePathHook)) { + if (fs.existsSync(appEntrySubprogram)) { appNames.push(appName); - promises.push(tryLoadApp(appEntryFilePathHook, appName)); + appDirs.push(appDir); + appEntries.push(appEntrySubprogram); + promises.push(tryLoadApp(appEntrySubprogram, appName)); } } }); return new Promise((success, reject) => { - const appDefinitions = {}; + const appDefinitions = []; Promise.all(promises).then(returnObjects => { returnObjects.forEach((result, i) => { if (result) { const appName = appNames[i]; - appDefinitions[appName] = appDefinitions[appName] || {}; + const appDir = appDirs[i]; + const appEntry = appEntries[i]; - if (result.handler) { - appDefinitions[appName].ssrHandler = result.handler; - } + const { handler, expressAppSetup } = result; - if (result.expressAppSetup) { - appDefinitions[appName].expressAppSetup = result.expressAppSetup; + let hasSSRHandler = false; + + if (handler) { + hasSSRHandler = true; } + + appDefinitions.push({ appName, appDir, appEntry, hasSSRHandler, ssrHandler: handler, expressAppSetup }); } }); @@ -77,9 +87,9 @@ async function loadApp(appEntryFilePath) { }); } -function importComplex(appEntryFilePath) { +export function importComplex(appEntryFilePath) { return new Promise((success, reject) => { - import(appEntryFilePath + `?${Math.random()}`) + import(`${appEntryFilePath}?${Math.random()}`) .then(mod => { let promiseOrData; let isPromise; diff --git a/core/node/aspect-extend/apps-serve/index.js b/core/node/aspect-extend/apps-serve/index.js index 7da3fdd79..331217b8f 100644 --- a/core/node/aspect-extend/apps-serve/index.js +++ b/core/node/aspect-extend/apps-serve/index.js @@ -21,7 +21,7 @@ function mountApps(appDefinitions, server) { return app => { const ssrApps = []; - for (const [appName, { expressAppSetup, ssrHandler }] of Object.entries(appDefinitions)) { + for (const { appName, expressAppSetup, ssrHandler } of appDefinitions) { if (ssrHandler) { server.useDynamicSSR(appName, ssrHandler); ssrApps.push(appName); diff --git a/core/node/aspect-extend/apps-serve/lib/server.js b/core/node/aspect-extend/apps-serve/lib/server.js index f0b937631..cee1021f9 100644 --- a/core/node/aspect-extend/apps-serve/lib/server.js +++ b/core/node/aspect-extend/apps-serve/lib/server.js @@ -3,7 +3,7 @@ import { log, colors, determineGUIPort } from 'dmt/common'; import express from 'express'; import fs from 'fs'; -import loadApps from '../../apps-load/loadApps.js'; +import { reloadSSRHandler, reloadAllSSRHandlers } from '../../apps-load/index.js'; import ssrProxy from './ssrProxy.js'; @@ -13,6 +13,14 @@ class Server { this.program = program; this.app = express(); + + program.on('gui:reload', () => { + log.yellow('Gui reload event received — reloading all ssr handlers'); + reloadAllSSRHandlers({ server: this }).catch(e => { + log.red('Error reloading some ssr handlers, should have received individual notifications and log entries'); + log.red(e); + }); + }); } setupRoutes(expressAppSetup) { @@ -24,16 +32,16 @@ class Server { const hasMiddleware = !!ssrMiddlewares.get(appName); ssrMiddlewares.set(appName, callback); - if (reload & !hasMiddleware) { - log.green('dmt new ssr app: ' + appName); + if (reload && !hasMiddleware) { + log.green(`💡 New SSR handler loaded: ${colors.magenta(appName)}`); } else if (reload) { - log.green('dmt ssr app reload: ' + appName); + log.cyan(`🔄 SSR handler reload — ${colors.magenta(appName)}`); } if (hasMiddleware) return; this.app - .use(`/_${appName}`, function(req, res, next) { + .use(`/_${appName}`, (req, res, next) => { const callback = ssrMiddlewares.get(appName); if (callback) { return callback(req, res, next); @@ -53,23 +61,17 @@ class Server { this.app .get('/__dmt__reload', (req, res) => { const appDir = req.query.app; + if (fs.existsSync(appDir)) { - loadApps([{ appDir }]) - .then(appDefinations => { - for (const appName in appDefinations) { - const ssrHandler = appDefinations[appName]?.ssrHandler; - if (ssrHandler) { - this.useDynamicSSR(appName, ssrHandler, true); - } - } + reloadSSRHandler({ server: this, appDir }) + .then(() => { res.end('success'); }) - .catch(err => { - log.red(err.message || err); + .catch(() => { res.end('rejected'); }); } else { - log.red('__dmt__reload appdir do not exist: ' + appDir); + log.red(`__dmt__reload appdir do not exist: ${appDir}`); res.end('rejected'); } }) diff --git a/core/node/aspect-extend/user-engine-load/modifyPackageJson.js b/core/node/aspect-extend/user-engine-load/modifyPackageJson.js index 671d65d81..1f9d796f0 100644 --- a/core/node/aspect-extend/user-engine-load/modifyPackageJson.js +++ b/core/node/aspect-extend/user-engine-load/modifyPackageJson.js @@ -8,17 +8,26 @@ export default function modifyPackageJson(userEnginePath) { const userEngineScriptsPath = path.join(dmtPath, 'etc/scripts/prepare_apps_and_user_engine/dmt_user_engine'); const exportsPath = path.join(userEngineScriptsPath, 'exports.json'); + const devDependenciesPath = path.join(userEngineScriptsPath, 'devDependencies.json'); if (fs.existsSync(exportsPath)) { const exportsJson = JSON.parse(fs.readFileSync(exportsPath).toString()); + const devDependenciesJson = JSON.parse(fs.readFileSync(devDependenciesPath).toString()); if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString()); + if (JSON.stringify(packageJson.exports) != JSON.stringify(exportsJson)) { log.magenta('Resetting named exports in DMT USER ENGINE package.json'); packageJson.exports = exportsJson; fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); } + + if (JSON.stringify(packageJson.devDependencies || {}) != JSON.stringify(devDependenciesJson)) { + log.magenta('Resetting devDependencies in DMT USER ENGINE package.json'); + packageJson.devDependencies = devDependenciesJson; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + } } } } diff --git a/core/node/common/index.js b/core/node/common/index.js index 7599cff56..9656935a3 100644 --- a/core/node/common/index.js +++ b/core/node/common/index.js @@ -14,6 +14,8 @@ import scan from './lib/scan.js'; import sets from './lib/sets.js'; import tags from './lib/tags.js'; import * as quantile from './lib/quantile.js'; +import setupProtocolConnectionsCounter from './lib/protocolHelpers/setupConnectionsCounter.js'; + import * as formatNumber from './lib/formatNumber/formatNumber.js'; import stopwatch from './lib/timeutils/stopwatch.js'; @@ -22,6 +24,7 @@ import stopwatchAdv from './lib/timeutils/stopwatchAdv.js'; import * as timeutils from './lib/timeutils/index.js'; import * as suntime from './lib/timeutils/suntime/index.js'; +import stripAnsi from 'strip-ansi'; import meetup from './lib/meetup/index.js'; import FsState from './lib/fsState.js'; @@ -441,6 +444,8 @@ export { debugMode, debugCategory, fsState, + stripAnsi, + setupProtocolConnectionsCounter, dmtStateDir, dmtPath, dmtHereEnsure, diff --git a/core/node/common/lib/dmtPreHelper.js b/core/node/common/lib/dmtPreHelper.js index f76dfc29d..d0e7fce2e 100644 --- a/core/node/common/lib/dmtPreHelper.js +++ b/core/node/common/lib/dmtPreHelper.js @@ -1,7 +1,9 @@ import fs from 'fs'; import path from 'path'; + import def from './parsers/def/parser.js'; import colors from './colors/colors.js'; + import colors2 from './colors/colors2.js'; import scan from './scan.js'; diff --git a/core/node/common/lib/protocolHelpers/setupConnectionsCounter.js b/core/node/common/lib/protocolHelpers/setupConnectionsCounter.js new file mode 100644 index 000000000..1b83a8c70 --- /dev/null +++ b/core/node/common/lib/protocolHelpers/setupConnectionsCounter.js @@ -0,0 +1,22 @@ +import { stripAnsi, colors, log, program } from 'dmt/common'; + +export default function setupConnectionsCounter({ channels, store, dmtID, protocol }) { + let maxCounter = 10; + const RISE = 30; + + channels.on('status', ({ connList }) => { + const counter = connList.length; + + if (counter > (1 + RISE / 100) * maxCounter) { + const msg = `🚀 ${colors.yellow(counter)} — new concurrent connections record for ${colors.cyan(dmtID)}/${colors.white( + protocol + )} protocol on ${colors.cyan(program.device.id)}`; + + log.write(msg); + + maxCounter = counter; + } + + store.update({ counter }); + }); +} diff --git a/core/node/common/lib/timeutils/formatMilliseconds.js b/core/node/common/lib/timeutils/formatMilliseconds.js index 3040e765d..d8be86df8 100644 --- a/core/node/common/lib/timeutils/formatMilliseconds.js +++ b/core/node/common/lib/timeutils/formatMilliseconds.js @@ -9,7 +9,7 @@ function getMinutesAndSeconds(timeMs) { return { minutes, seconds }; } -function formatMinutesAndSeconds(timeMs) { +function formatMinutesAndSeconds(timeMs, { omitSeconds = false } = {}) { const { minutes, seconds } = getMinutesAndSeconds(timeMs); let result = ''; @@ -18,7 +18,7 @@ function formatMinutesAndSeconds(timeMs) { result += `${minutes} min`; } - if (seconds != 0) { + if (seconds != 0 && !omitSeconds) { result += ` ${round(seconds, 0)}${minutes == 0 ? '' : ' '}s`; } @@ -37,11 +37,31 @@ export default function formatMilliseconds(timeMs) { } if (_seconds < 60 * 60) { - return formatMinutesAndSeconds(timeMs); + return formatMinutesAndSeconds(timeMs, { omitSeconds: _seconds >= 30 * 60 }); } - const hours = Math.floor(_seconds / 3600); + const _hours = Math.floor(_seconds / 3600); const seconds = _seconds % 3600; - return `${hours} h ${formatMinutesAndSeconds(seconds * 1000)}`.trim(); + const _days = Math.floor(_hours / 24); + const hours = _hours % 24; + + const weeks = Math.floor(_days / 7); + const days = _days % 7; + + const prepend = []; + + if (weeks) { + prepend.push(`${weeks} w`); + } + + if (days) { + prepend.push(`${days} d`); + } + + if (hours) { + prepend.push(`${hours} h`); + } + + return `${prepend.join(' ')} ${formatMinutesAndSeconds(seconds * 1000, { omitSeconds: true })}`.trim(); } diff --git a/core/node/connectome/dist/index.js b/core/node/connectome/dist/index.js index d7c5bda96..24fabb60f 100644 --- a/core/node/connectome/dist/index.js +++ b/core/node/connectome/dist/index.js @@ -3162,7 +3162,7 @@ function send({ data, connector }) { log, `Connector ${connector.endpoint} → Sending message #${connector.sentCount} ↴` ); - logger.gray(log, data); + logger.cyan(log, data); } connector.connection.websocket.send(data); @@ -3228,11 +3228,25 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec // 💡 encryptedJson data!! if (connector.verbose == 'extra') { logger.magenta(log, `Connector ${connector.endpoint} received bytes ↴`); - logger.gray(log, encryptedData); - logger.magenta( + logger.cyan(log, encryptedData); + logger.green(log, JSON.stringify(encryptedData)); + logger.gray( log, `Connector ${connector.endpoint} decrypting with shared secret ${connector.sharedSecret}...` ); + //logger.cyan(log, JSON.stringify(connector.sharedSecret)); + } + + if (!connector.sharedSecret) { + // we had this problem before -- zurich wifi -- when terminating inactive websocket + // it didn't actually close in time .. we set connector to disconnected and deleted sharedSecret + // but then a stray message json rpc return from hadshake arrived after that and couldn't be decrypted + // because it shouldn't have arrived in the first place after websocket was supposedly closed + // solution: __closed flag on all websockets.. it is set to true at the same time as calling close() + // and then any messages still coming over the wire on such closed websockets are dropped + // we hope websocket is eventually closed though (?) + // see messageCallback in establishAndMaintainConnection, this was fixed there + logger.red(log, `Connector ${connector.endpoint} missing sharedSecret - should not happen...`); } const _decryptedMessage = naclFast.secretbox.open(encryptedData, nonce, connector.sharedSecret); @@ -3244,7 +3258,7 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec const decodedMessage = naclFast.util.encodeUTF8(decryptedMessage); if (connector.verbose) { - logger.write(log, `Received message: ${decodedMessage}`); + logger.yellow(log, `Connector ${connector.endpoint} received message: ${decodedMessage}`); } try { @@ -3290,6 +3304,10 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec throw e; } } else { + if (connector.verbose) { + logger.yellow(log, `Connector ${connector.endpoint} received binary data`); + } + //const binaryData = decryptedMessage; // const sessionId = Buffer.from(binaryData.buffer, binaryData.byteOffset, 64).toString(); // const binaryPayload = Buffer.from(binaryData.buffer, binaryData.byteOffset + 64); @@ -3301,20 +3319,17 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec naclFast.util = naclUtil; +const wsOPEN = 1; + function diffieHellman({ connector, afterFirstStep = () => {} }) { - const { - clientPrivateKey, - clientPublicKey, - clientPublicKeyHex, - protocol, - tag, - endpoint, - verbose - } = connector; + const { clientPrivateKey, clientPublicKey, clientPublicKeyHex, protocol, tag, endpoint, verbose } = + connector; return new Promise((success, reject) => { - connector.remoteObject('Auth') + connector + .remoteObject('Auth') .call('exchangePubkeys', { pubkey: clientPublicKeyHex }) + //.call('exchangePubkeys', { pubkey: clientPublicKeyHex, clientWsId: connector.connection.websocket.__id }) .then(remotePubkeyHex => { const sharedSecret = naclFast.box.before(hexToBuffer(remotePubkeyHex), clientPrivateKey); @@ -3327,33 +3342,50 @@ function diffieHellman({ connector, afterFirstStep = () => {} }) { ); } - connector.remoteObject('Auth') - .call('finalizeHandshake', { protocol }) - .then(res => { - // finalizeHandshake rpc endpoint on server can cleanly retorn {error} as a result - // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) - if (res && res.error) { - console.log(res.error); - // this connection will keep hangling and no reconnect tries will be made - // since we keep websocket open just that nothing is happening - - // when we enable the protocol on the endpoint we have to restart the process - // frontend connector will get disconnected at this point, websocket will close - // and from then on it tries reconnecting again so when ws first connects - // and protocol is present , it will be a success - - // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging - } else { - success(); - - const _tag = tag ? ` (${tag})` : ''; - logger.cyan( - connector.log, - `${endpoint}${_tag} ✓ Connection [ ${protocol || '"no-name"'} ] ready` - ); - } - }) - .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + // if connection has closed at this point we don't try to send into closed + // connection, it would still work but error would be logged + if (connector.connection.websocket.readyState == wsOPEN) { + connector + .remoteObject('Auth') + .call('finalizeHandshake', { protocol }) + .then(res => { + // finalizeHandshake rpc endpoint on server can cleanly return {error} as a result + // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) + if (res && res.error) { + console.log(res.error); + // this connection will keep hangling and no reconnect tries will be made + // since we keep websocket open just that nothing is happening + + // when we enable the protocol on the endpoint we have to restart the process + // frontend connector will get disconnected at this point, websocket will close + // and from then on it tries reconnecting again so when ws first connects + // and protocol is present , it will be a success + + // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging + } else { + success(); + + const _tag = tag ? ` (${tag})` : ''; + logger.cyan( + connector.log, + `✓✓✓ ${endpoint}${_tag} ✓ Connection #${connector.connection.websocket.__id} [ ${ + protocol || '"no-name"' + } ] ready` + ); + } + }) + .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + } else { + const _tag = tag ? ` (${tag})` : ''; + logger.yellow( + connector.log, + `${endpoint}${_tag} ✖ Connection [ ${ + protocol || '"no-name"' + } ] closed just before finalizeHandshake step` + ); + // don't reject here -- because it will show some wring log message in connector + // on:ready error "will not try to reconnect" .. which is not the case here + } }) .catch(reject); }); @@ -4847,7 +4879,7 @@ const DECOMMISSION_INACTIVITY = 60000; // 1min //const DECOMMISSION_INACTIVITY = 120000; // 2min //const DECOMMISSION_INACTIVITY = 10000; // 2min -const wsOPEN = 1; +const wsOPEN$1 = 1; class Connector extends Eev { constructor({ @@ -5003,7 +5035,7 @@ class Connector extends Eev { this.successfulConnectsCount += 1; if (this.verbose) { - logger.green(this.log, `✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`); + logger.white(this.log, `✓ Connector ${this.endpoint} connected (${this.successfulConnectsCount} total reconnects)`); } const websocketId = this.connection.websocket.__id; @@ -5035,7 +5067,7 @@ class Connector extends Eev { // but sometimes we also get an open websocket after rpc timeout (not sure but this code handles it anyway, should be no problem, only better for all cases) if ( this.connection.websocket.__id == websocketId && - this.connection.websocket.readyState == wsOPEN + this.connection.websocket.readyState == wsOPEN$1 ) { //⚠️ we only show if it seems still relevant, special case // previously we had this first log output above this if statement @@ -5219,7 +5251,7 @@ function determineEndpoint({ endpoint, host, port }) { const browser$1 = typeof window !== 'undefined'; const wsCONNECTING = 0; -const wsOPEN$1 = 1; +const wsOPEN$2 = 1; //const wsCLOSING = 2; //const wsCLOSED = 3; @@ -5233,6 +5265,22 @@ const CONN_IDLE_TICKS = 3; // how long to wait for a new websocket to connect... after this we cancel it const WAIT_FOR_NEW_CONN_TICKS = 5; // 5000 ms ( = (5) * CONN_CHECK_INTERVAL ) +function addListener(name, callback, ws) { + if (browser$1) { + ws.addEventListener(name, callback); + } else { + ws.on(name, callback); + } +} + +function removeListener(name, callback, ws) { + if (browser$1) { + ws.removeEventListener(name, callback); + } else { + ws.off(name, callback); + } +} + //todo: remove 'dummy' argument once legacyLib with old MCS is history function establishAndMaintainConnection( { @@ -5272,7 +5320,8 @@ function establishAndMaintainConnection( connector.connection = { terminate() { this.websocket._removeAllCallbacks(); - this.websocket.close(); + this.websocket.__closed = true; + this.websocket.close(); // might take some time to actually close, we can get stray messages through that websocket //connector.connectStatus(undefined); connector.connectStatus(false); reconnect(); @@ -5306,14 +5355,14 @@ function checkConnection({ connector, reconnect, log }) { // decommissioned logger.yellow( log, - `${connector.endpoint} Connection decommisioned, closing websocket ${conn.websocket.__id}, will not retry again ` + `${connector.endpoint} Connection decommisioned, closing websocket #${conn.websocket.__id}, will not retry again ` ); decommission(connector); } else { // idle connection connector.emit('inactive_connection'); - logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection`); + logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection #${conn.websocket.__id}`); } conn.terminate(); @@ -5355,6 +5404,8 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb return; } + const wsId = Math.round(10 ** 5 * Math.random()).toString(); + //logger.write(log, `${endpoint} CONN_TICK`); //logger.write(log, `${endpoint} wsReadyState ${conn.currentlyTryingWS?.readyState}`); @@ -5370,9 +5421,10 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); } else if (verbose || browser$1) { - logger.write(log, `${endpoint} Created new websocket`); + logger.write(log, `${endpoint} Created new websocket #${wsId}`); } // so in case when device is online but websocket server is not running we usually @@ -5383,7 +5435,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb // (see above)... and we try with a new websocket every 4800ms again instead on every tick (800ms) const ws = new WebSocket(endpoint); - ws.__id = Math.random(); + ws.__id = wsId; conn.currentlyTryingWS = ws; conn.currentlyTryingWS._waitForConnectCounter = 0; @@ -5403,7 +5455,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } if (verbose || browser$1) { - logger.write(log, `${endpoint} Websocket open`); + logger.write(log, `${endpoint} Websocket #${wsId} open`); } conn.currentlyTryingWS = null; @@ -5416,14 +5468,14 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb }; ws._removeAllCallbacks = () => { - ws.removeEventListener('open', openCallback); + // logger.red( + // log, + // `${connector.endpoint} removing 1 callback (open) on ws #${ws.__id} [ ${connector.protocol} ]` + // ); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('open', openCallback); - } else { - ws.on('open', openCallback); - } + addListener('open', openCallback, ws); } function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, verbose }) { @@ -5440,7 +5492,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; const closeCallback = () => { - logger.write(log, `${connector.endpoint} ✖ Connection closed`); + //❗❗❗❗ -- can get stray messages even here!! after close callback ws implementation lets a few (one) messages through!! + // this only happened on LAN ... + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+167ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 ✖ Connection #28485 [ dmt ] closed' + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+01ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 Created new websocket #17068' + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+338ms) ∞ 1.0.0.1 consecutiveUnresolvedTimeout after 2x unresolved promise + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+43ms) ∞ lanServerConn — "ws://192.168.0.10:7780 connection #28485 [ dmt ] received msg '��\x19X���9�߈�V^L�#�b��)\x02�\r��n\x06^?U�v�\x00�ͻ>����k~�A(^�\t�İP�=���X*���'" + // maybe not needed anymore after listeners issue was fixed ..... + ws.__closed = true; + + logger.blue(log, `${connector.endpoint} ✖ Connection #${ws.__id} [ ${connector.protocol} ] closed`); if (connector.decommissioned) { connector.connectStatus(false); @@ -5453,6 +5514,7 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v // flip side is that there is such small delay between when we stop some process and when red x appears... but it's quite ok! // we do however disable all commands immediately ... so: show red X when connect status is FALSE excusively and disable all gui actions when it's NOT TRUE (false or undefined) connector.connectStatus(undefined); + reconnect(); //setTimeout(reconnect, MAX_RECONNECT_DELAY_AFTER_WS_CLOSE * Math.random()); // turns out we don't really need to do these delays, works fine without }; @@ -5466,11 +5528,26 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v const msg = browser$1 ? _msg.data : _msg; + if (ws.__closed) { + // if (msg != 'pong') { + // logger.red( + // log, + // `${connector.endpoint} Already closed connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + // } + return; + } + if (msg == 'pong') { connector.emit('pong'); return; } + // logger.red( + // log, + // `${connector.endpoint} connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + let jsonData; try { @@ -5486,22 +5563,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; ws._removeAllCallbacks = () => { - ws.removeEventListener('error', errorCallback); - ws.removeEventListener('close', closeCallback); - ws.removeEventListener('message', messageCallback); - - ws.removeEventListener('open', openCallback); + // logger.red(log, `${connector.endpoint} removing 4 callbacks on ws #${ws.__id} [ ${connector.protocol} ]`); + removeListener('error', errorCallback, ws); + removeListener('close', closeCallback, ws); + removeListener('message', messageCallback, ws); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('error', errorCallback); - ws.addEventListener('close', closeCallback); - ws.addEventListener('message', messageCallback); - } else { - ws.on('error', errorCallback); - ws.on('close', closeCallback); - ws.on('message', messageCallback); - } + addListener('error', errorCallback, ws); + addListener('close', closeCallback, ws); + addListener('message', messageCallback, ws); } function decommission(connector) { @@ -5509,21 +5580,23 @@ function decommission(connector) { if (conn.currentlyTryingWS) { conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); conn.currentlyTryingWS = null; } - if (conn.ws) { - conn.ws._removeAllCallbacks(); - conn.ws.close(); - conn.ws = null; + if (conn.websocket) { + conn.websocket._removeAllCallbacks(); + conn.websocket.__closed = true; + conn.websocket.close(); + conn.websocket = null; } connector.connectStatus(false); } function socketConnected(conn) { - return conn.websocket && conn.websocket.readyState == wsOPEN$1; + return conn.websocket && conn.websocket.readyState == wsOPEN$2 && !conn.websocket.__closed; // when terminating connection, might be useful -- check } function connectionIdle(conn) { diff --git a/core/node/connectome/dist/index.mjs b/core/node/connectome/dist/index.mjs index c48a92f76..6c2df75f7 100644 --- a/core/node/connectome/dist/index.mjs +++ b/core/node/connectome/dist/index.mjs @@ -3158,7 +3158,7 @@ function send({ data, connector }) { log, `Connector ${connector.endpoint} → Sending message #${connector.sentCount} ↴` ); - logger.gray(log, data); + logger.cyan(log, data); } connector.connection.websocket.send(data); @@ -3224,11 +3224,25 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec // 💡 encryptedJson data!! if (connector.verbose == 'extra') { logger.magenta(log, `Connector ${connector.endpoint} received bytes ↴`); - logger.gray(log, encryptedData); - logger.magenta( + logger.cyan(log, encryptedData); + logger.green(log, JSON.stringify(encryptedData)); + logger.gray( log, `Connector ${connector.endpoint} decrypting with shared secret ${connector.sharedSecret}...` ); + //logger.cyan(log, JSON.stringify(connector.sharedSecret)); + } + + if (!connector.sharedSecret) { + // we had this problem before -- zurich wifi -- when terminating inactive websocket + // it didn't actually close in time .. we set connector to disconnected and deleted sharedSecret + // but then a stray message json rpc return from hadshake arrived after that and couldn't be decrypted + // because it shouldn't have arrived in the first place after websocket was supposedly closed + // solution: __closed flag on all websockets.. it is set to true at the same time as calling close() + // and then any messages still coming over the wire on such closed websockets are dropped + // we hope websocket is eventually closed though (?) + // see messageCallback in establishAndMaintainConnection, this was fixed there + logger.red(log, `Connector ${connector.endpoint} missing sharedSecret - should not happen...`); } const _decryptedMessage = naclFast.secretbox.open(encryptedData, nonce, connector.sharedSecret); @@ -3240,7 +3254,7 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec const decodedMessage = naclFast.util.encodeUTF8(decryptedMessage); if (connector.verbose) { - logger.write(log, `Received message: ${decodedMessage}`); + logger.yellow(log, `Connector ${connector.endpoint} received message: ${decodedMessage}`); } try { @@ -3286,6 +3300,10 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec throw e; } } else { + if (connector.verbose) { + logger.yellow(log, `Connector ${connector.endpoint} received binary data`); + } + //const binaryData = decryptedMessage; // const sessionId = Buffer.from(binaryData.buffer, binaryData.byteOffset, 64).toString(); // const binaryPayload = Buffer.from(binaryData.buffer, binaryData.byteOffset + 64); @@ -3297,20 +3315,17 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec naclFast.util = naclUtil; +const wsOPEN = 1; + function diffieHellman({ connector, afterFirstStep = () => {} }) { - const { - clientPrivateKey, - clientPublicKey, - clientPublicKeyHex, - protocol, - tag, - endpoint, - verbose - } = connector; + const { clientPrivateKey, clientPublicKey, clientPublicKeyHex, protocol, tag, endpoint, verbose } = + connector; return new Promise((success, reject) => { - connector.remoteObject('Auth') + connector + .remoteObject('Auth') .call('exchangePubkeys', { pubkey: clientPublicKeyHex }) + //.call('exchangePubkeys', { pubkey: clientPublicKeyHex, clientWsId: connector.connection.websocket.__id }) .then(remotePubkeyHex => { const sharedSecret = naclFast.box.before(hexToBuffer(remotePubkeyHex), clientPrivateKey); @@ -3323,33 +3338,50 @@ function diffieHellman({ connector, afterFirstStep = () => {} }) { ); } - connector.remoteObject('Auth') - .call('finalizeHandshake', { protocol }) - .then(res => { - // finalizeHandshake rpc endpoint on server can cleanly retorn {error} as a result - // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) - if (res && res.error) { - console.log(res.error); - // this connection will keep hangling and no reconnect tries will be made - // since we keep websocket open just that nothing is happening - - // when we enable the protocol on the endpoint we have to restart the process - // frontend connector will get disconnected at this point, websocket will close - // and from then on it tries reconnecting again so when ws first connects - // and protocol is present , it will be a success - - // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging - } else { - success(); - - const _tag = tag ? ` (${tag})` : ''; - logger.cyan( - connector.log, - `${endpoint}${_tag} ✓ Connection [ ${protocol || '"no-name"'} ] ready` - ); - } - }) - .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + // if connection has closed at this point we don't try to send into closed + // connection, it would still work but error would be logged + if (connector.connection.websocket.readyState == wsOPEN) { + connector + .remoteObject('Auth') + .call('finalizeHandshake', { protocol }) + .then(res => { + // finalizeHandshake rpc endpoint on server can cleanly return {error} as a result + // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) + if (res && res.error) { + console.log(res.error); + // this connection will keep hangling and no reconnect tries will be made + // since we keep websocket open just that nothing is happening + + // when we enable the protocol on the endpoint we have to restart the process + // frontend connector will get disconnected at this point, websocket will close + // and from then on it tries reconnecting again so when ws first connects + // and protocol is present , it will be a success + + // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging + } else { + success(); + + const _tag = tag ? ` (${tag})` : ''; + logger.cyan( + connector.log, + `✓✓✓ ${endpoint}${_tag} ✓ Connection #${connector.connection.websocket.__id} [ ${ + protocol || '"no-name"' + } ] ready` + ); + } + }) + .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + } else { + const _tag = tag ? ` (${tag})` : ''; + logger.yellow( + connector.log, + `${endpoint}${_tag} ✖ Connection [ ${ + protocol || '"no-name"' + } ] closed just before finalizeHandshake step` + ); + // don't reject here -- because it will show some wring log message in connector + // on:ready error "will not try to reconnect" .. which is not the case here + } }) .catch(reject); }); @@ -4843,7 +4875,7 @@ const DECOMMISSION_INACTIVITY = 60000; // 1min //const DECOMMISSION_INACTIVITY = 120000; // 2min //const DECOMMISSION_INACTIVITY = 10000; // 2min -const wsOPEN = 1; +const wsOPEN$1 = 1; class Connector extends Eev { constructor({ @@ -4999,7 +5031,7 @@ class Connector extends Eev { this.successfulConnectsCount += 1; if (this.verbose) { - logger.green(this.log, `✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`); + logger.white(this.log, `✓ Connector ${this.endpoint} connected (${this.successfulConnectsCount} total reconnects)`); } const websocketId = this.connection.websocket.__id; @@ -5031,7 +5063,7 @@ class Connector extends Eev { // but sometimes we also get an open websocket after rpc timeout (not sure but this code handles it anyway, should be no problem, only better for all cases) if ( this.connection.websocket.__id == websocketId && - this.connection.websocket.readyState == wsOPEN + this.connection.websocket.readyState == wsOPEN$1 ) { //⚠️ we only show if it seems still relevant, special case // previously we had this first log output above this if statement @@ -5215,7 +5247,7 @@ function determineEndpoint({ endpoint, host, port }) { const browser$1 = typeof window !== 'undefined'; const wsCONNECTING = 0; -const wsOPEN$1 = 1; +const wsOPEN$2 = 1; //const wsCLOSING = 2; //const wsCLOSED = 3; @@ -5229,6 +5261,22 @@ const CONN_IDLE_TICKS = 3; // how long to wait for a new websocket to connect... after this we cancel it const WAIT_FOR_NEW_CONN_TICKS = 5; // 5000 ms ( = (5) * CONN_CHECK_INTERVAL ) +function addListener(name, callback, ws) { + if (browser$1) { + ws.addEventListener(name, callback); + } else { + ws.on(name, callback); + } +} + +function removeListener(name, callback, ws) { + if (browser$1) { + ws.removeEventListener(name, callback); + } else { + ws.off(name, callback); + } +} + //todo: remove 'dummy' argument once legacyLib with old MCS is history function establishAndMaintainConnection( { @@ -5268,7 +5316,8 @@ function establishAndMaintainConnection( connector.connection = { terminate() { this.websocket._removeAllCallbacks(); - this.websocket.close(); + this.websocket.__closed = true; + this.websocket.close(); // might take some time to actually close, we can get stray messages through that websocket //connector.connectStatus(undefined); connector.connectStatus(false); reconnect(); @@ -5302,14 +5351,14 @@ function checkConnection({ connector, reconnect, log }) { // decommissioned logger.yellow( log, - `${connector.endpoint} Connection decommisioned, closing websocket ${conn.websocket.__id}, will not retry again ` + `${connector.endpoint} Connection decommisioned, closing websocket #${conn.websocket.__id}, will not retry again ` ); decommission(connector); } else { // idle connection connector.emit('inactive_connection'); - logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection`); + logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection #${conn.websocket.__id}`); } conn.terminate(); @@ -5351,6 +5400,8 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb return; } + const wsId = Math.round(10 ** 5 * Math.random()).toString(); + //logger.write(log, `${endpoint} CONN_TICK`); //logger.write(log, `${endpoint} wsReadyState ${conn.currentlyTryingWS?.readyState}`); @@ -5366,9 +5417,10 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); } else if (verbose || browser$1) { - logger.write(log, `${endpoint} Created new websocket`); + logger.write(log, `${endpoint} Created new websocket #${wsId}`); } // so in case when device is online but websocket server is not running we usually @@ -5379,7 +5431,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb // (see above)... and we try with a new websocket every 4800ms again instead on every tick (800ms) const ws = new WebSocket(endpoint); - ws.__id = Math.random(); + ws.__id = wsId; conn.currentlyTryingWS = ws; conn.currentlyTryingWS._waitForConnectCounter = 0; @@ -5399,7 +5451,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } if (verbose || browser$1) { - logger.write(log, `${endpoint} Websocket open`); + logger.write(log, `${endpoint} Websocket #${wsId} open`); } conn.currentlyTryingWS = null; @@ -5412,14 +5464,14 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb }; ws._removeAllCallbacks = () => { - ws.removeEventListener('open', openCallback); + // logger.red( + // log, + // `${connector.endpoint} removing 1 callback (open) on ws #${ws.__id} [ ${connector.protocol} ]` + // ); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('open', openCallback); - } else { - ws.on('open', openCallback); - } + addListener('open', openCallback, ws); } function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, verbose }) { @@ -5436,7 +5488,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; const closeCallback = () => { - logger.write(log, `${connector.endpoint} ✖ Connection closed`); + //❗❗❗❗ -- can get stray messages even here!! after close callback ws implementation lets a few (one) messages through!! + // this only happened on LAN ... + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+167ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 ✖ Connection #28485 [ dmt ] closed' + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+01ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 Created new websocket #17068' + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+338ms) ∞ 1.0.0.1 consecutiveUnresolvedTimeout after 2x unresolved promise + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+43ms) ∞ lanServerConn — "ws://192.168.0.10:7780 connection #28485 [ dmt ] received msg '��\x19X���9�߈�V^L�#�b��)\x02�\r��n\x06^?U�v�\x00�ͻ>����k~�A(^�\t�İP�=���X*���'" + // maybe not needed anymore after listeners issue was fixed ..... + ws.__closed = true; + + logger.blue(log, `${connector.endpoint} ✖ Connection #${ws.__id} [ ${connector.protocol} ] closed`); if (connector.decommissioned) { connector.connectStatus(false); @@ -5449,6 +5510,7 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v // flip side is that there is such small delay between when we stop some process and when red x appears... but it's quite ok! // we do however disable all commands immediately ... so: show red X when connect status is FALSE excusively and disable all gui actions when it's NOT TRUE (false or undefined) connector.connectStatus(undefined); + reconnect(); //setTimeout(reconnect, MAX_RECONNECT_DELAY_AFTER_WS_CLOSE * Math.random()); // turns out we don't really need to do these delays, works fine without }; @@ -5462,11 +5524,26 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v const msg = browser$1 ? _msg.data : _msg; + if (ws.__closed) { + // if (msg != 'pong') { + // logger.red( + // log, + // `${connector.endpoint} Already closed connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + // } + return; + } + if (msg == 'pong') { connector.emit('pong'); return; } + // logger.red( + // log, + // `${connector.endpoint} connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + let jsonData; try { @@ -5482,22 +5559,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; ws._removeAllCallbacks = () => { - ws.removeEventListener('error', errorCallback); - ws.removeEventListener('close', closeCallback); - ws.removeEventListener('message', messageCallback); - - ws.removeEventListener('open', openCallback); + // logger.red(log, `${connector.endpoint} removing 4 callbacks on ws #${ws.__id} [ ${connector.protocol} ]`); + removeListener('error', errorCallback, ws); + removeListener('close', closeCallback, ws); + removeListener('message', messageCallback, ws); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('error', errorCallback); - ws.addEventListener('close', closeCallback); - ws.addEventListener('message', messageCallback); - } else { - ws.on('error', errorCallback); - ws.on('close', closeCallback); - ws.on('message', messageCallback); - } + addListener('error', errorCallback, ws); + addListener('close', closeCallback, ws); + addListener('message', messageCallback, ws); } function decommission(connector) { @@ -5505,21 +5576,23 @@ function decommission(connector) { if (conn.currentlyTryingWS) { conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); conn.currentlyTryingWS = null; } - if (conn.ws) { - conn.ws._removeAllCallbacks(); - conn.ws.close(); - conn.ws = null; + if (conn.websocket) { + conn.websocket._removeAllCallbacks(); + conn.websocket.__closed = true; + conn.websocket.close(); + conn.websocket = null; } connector.connectStatus(false); } function socketConnected(conn) { - return conn.websocket && conn.websocket.readyState == wsOPEN$1; + return conn.websocket && conn.websocket.readyState == wsOPEN$2 && !conn.websocket.__closed; // when terminating connection, might be useful -- check } function connectionIdle(conn) { diff --git a/core/node/connectome/dist/node/index.js b/core/node/connectome/dist/node/index.js index 3d6ea8198..6cd3881d9 100644 --- a/core/node/connectome/dist/node/index.js +++ b/core/node/connectome/dist/node/index.js @@ -7,13 +7,14 @@ var https = require('https'); var http = require('http'); var net = require('net'); var tls = require('tls'); -var require$$0$1 = require('crypto'); -var require$$1 = require('url'); +var require$$0$2 = require('crypto'); +var require$$0$1 = require('stream'); +var require$$2 = require('url'); var zlib = require('zlib'); var fs = require('fs'); var path = require('path'); var os = require('os'); -var require$$0 = require('stream'); +var require$$0 = require('buffer'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } @@ -22,8 +23,9 @@ var https__default = /*#__PURE__*/_interopDefaultLegacy(https); var http__default = /*#__PURE__*/_interopDefaultLegacy(http); var net__default = /*#__PURE__*/_interopDefaultLegacy(net); var tls__default = /*#__PURE__*/_interopDefaultLegacy(tls); +var require$$0__default$2 = /*#__PURE__*/_interopDefaultLegacy(require$$0$2); var require$$0__default$1 = /*#__PURE__*/_interopDefaultLegacy(require$$0$1); -var require$$1__default = /*#__PURE__*/_interopDefaultLegacy(require$$1); +var require$$2__default = /*#__PURE__*/_interopDefaultLegacy(require$$2); var zlib__default = /*#__PURE__*/_interopDefaultLegacy(zlib); var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); var path__default = /*#__PURE__*/_interopDefaultLegacy(path); @@ -48,10 +50,12 @@ function commonjsRequire () { var constants = { BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], + EMPTY_BUFFER: Buffer.alloc(0), GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + kForOnEventAttribute: Symbol('kIsForOnEventAttribute'), + kListener: Symbol('kListener'), kStatusCode: Symbol('status-code'), kWebSocket: Symbol('websocket'), - EMPTY_BUFFER: Buffer.alloc(0), NOOP: () => {} }; @@ -264,6 +268,8 @@ var bufferUtil = createCommonjsModule(function (module) { const { EMPTY_BUFFER } = constants; +const FastBuffer = Buffer[Symbol.species]; + /** * Merges an array of buffers into a new buffer. * @@ -285,7 +291,9 @@ function concat(list, totalLength) { offset += buf.length; } - if (offset < totalLength) return target.slice(0, offset); + if (offset < totalLength) { + return new FastBuffer(target.buffer, target.byteOffset, offset); + } return target; } @@ -314,9 +322,7 @@ function _mask(source, mask, output, offset, length) { * @public */ function _unmask(buffer, mask) { - // Required until https://github.com/nodejs/node/issues/9006 is resolved. - const length = buffer.length; - for (let i = 0; i < length; i++) { + for (let i = 0; i < buffer.length; i++) { buffer[i] ^= mask[i & 3]; } } @@ -329,11 +335,11 @@ function _unmask(buffer, mask) { * @public */ function toArrayBuffer(buf) { - if (buf.byteLength === buf.buffer.byteLength) { + if (buf.length === buf.buffer.byteLength) { return buf.buffer; } - return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length); } /** @@ -352,9 +358,9 @@ function toBuffer(data) { let buf; if (data instanceof ArrayBuffer) { - buf = Buffer.from(data); + buf = new FastBuffer(data); } else if (ArrayBuffer.isView(data)) { - buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); + buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength); } else { buf = Buffer.from(data); toBuffer.readOnly = false; @@ -363,31 +369,31 @@ function toBuffer(data) { return buf; } -try { - const bufferUtil = bufferutil; - const bu = bufferUtil.BufferUtil || bufferUtil; +module.exports = { + concat, + mask: _mask, + toArrayBuffer, + toBuffer, + unmask: _unmask +}; + +/* istanbul ignore else */ +if (!process.env.WS_NO_BUFFER_UTIL) { + try { + const bufferUtil = bufferutil; - module.exports = { - concat, - mask(source, mask, output, offset, length) { + module.exports.mask = function (source, mask, output, offset, length) { if (length < 48) _mask(source, mask, output, offset, length); - else bu.mask(source, mask, output, offset, length); - }, - toArrayBuffer, - toBuffer, - unmask(buffer, mask) { + else bufferUtil.mask(source, mask, output, offset, length); + }; + + module.exports.unmask = function (buffer, mask) { if (buffer.length < 32) _unmask(buffer, mask); - else bu.unmask(buffer, mask); - } - }; -} catch (e) /* istanbul ignore next */ { - module.exports = { - concat, - mask: _mask, - toArrayBuffer, - toBuffer, - unmask: _unmask - }; + else bufferUtil.unmask(buffer, mask); + }; + } catch (e) { + // Continue regardless of the error. + } } }); @@ -445,8 +451,9 @@ class Limiter { var limiter = Limiter; -const { kStatusCode, NOOP } = constants; +const { kStatusCode } = constants; +const FastBuffer = Buffer[Symbol.species]; const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); const kPerMessageDeflate = Symbol('permessage-deflate'); const kTotalLength = Symbol('total-length'); @@ -471,22 +478,22 @@ class PerMessageDeflate { * Creates a PerMessageDeflate instance. * * @param {Object} [options] Configuration options - * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept - * disabling of server context takeover + * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support + * for, or request, a custom client window size * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ * acknowledge disabling of client context takeover + * @param {Number} [options.concurrencyLimit=10] The number of concurrent + * calls to zlib * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the * use of a custom server window size - * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support - * for, or request, a custom client window size + * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept + * disabling of server context takeover + * @param {Number} [options.threshold=1024] Size (in bytes) below which + * messages should not be compressed if context takeover is disabled * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on * deflate * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on * inflate - * @param {Number} [options.threshold=1024] Size (in bytes) below which - * messages should not be compressed - * @param {Number} [options.concurrencyLimit=10] The number of concurrent - * calls to zlib * @param {Boolean} [isServer=false] Create the instance in either server or * client mode * @param {Number} [maxPayload=0] The maximum allowed message length @@ -754,7 +761,7 @@ class PerMessageDeflate { /** * Compress data. Concurrency limited. * - * @param {Buffer} data Data to compress + * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @public @@ -836,7 +843,7 @@ class PerMessageDeflate { /** * Compress data. * - * @param {Buffer} data Data to compress + * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @private @@ -859,13 +866,6 @@ class PerMessageDeflate { this._deflate[kTotalLength] = 0; this._deflate[kBuffers] = []; - // - // An `'error'` event is emitted, only on Node.js < 10.0.0, if the - // `zlib.DeflateRaw` instance is closed while data is being processed. - // This can happen if `PerMessageDeflate#cleanup()` is called at the wrong - // time due to an abnormal WebSocket closure. - // - this._deflate.on('error', NOOP); this._deflate.on('data', deflateOnData); } @@ -885,7 +885,9 @@ class PerMessageDeflate { this._deflate[kTotalLength] ); - if (fin) data = data.slice(0, data.length - 4); + if (fin) { + data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4); + } // // Ensure that the callback will not be called again in @@ -936,6 +938,7 @@ function inflateOnData(chunk) { } this[kError] = new RangeError('Max payload size exceeded'); + this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; this[kError][kStatusCode] = 1009; this.removeListener('data', inflateOnData); this.reset(); @@ -1029,6 +1032,31 @@ try { var validation = createCommonjsModule(function (module) { +const { isUtf8 } = require$$0__default['default']; + +// +// Allowed token characters: +// +// '!', '#', '$', '%', '&', ''', '*', '+', '-', +// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' +// +// tokenChars[32] === 0 // ' ' +// tokenChars[33] === 1 // '!' +// tokenChars[34] === 0 // '"' +// ... +// +// prettier-ignore +const tokenChars = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 +]; + /** * Checks if a status code is allowed in a close frame. * @@ -1061,7 +1089,7 @@ function _isValidUTF8(buf) { let i = 0; while (i < len) { - if (buf[i] < 0x80) { + if ((buf[i] & 0x80) === 0) { // 0xxxxxxx i++; } else if ((buf[i] & 0xe0) === 0xc0) { @@ -1072,9 +1100,9 @@ function _isValidUTF8(buf) { (buf[i] & 0xfe) === 0xc0 // Overlong ) { return false; - } else { - i += 2; } + + i += 2; } else if ((buf[i] & 0xf0) === 0xe0) { // 1110xxxx 10xxxxxx 10xxxxxx if ( @@ -1085,9 +1113,9 @@ function _isValidUTF8(buf) { (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) ) { return false; - } else { - i += 3; } + + i += 3; } else if ((buf[i] & 0xf8) === 0xf0) { // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx if ( @@ -1100,9 +1128,9 @@ function _isValidUTF8(buf) { buf[i] > 0xf4 // > U+10FFFF ) { return false; - } else { - i += 4; } + + i += 4; } else { return false; } @@ -1111,29 +1139,30 @@ function _isValidUTF8(buf) { return true; } -try { - let isValidUTF8 = utf8Validate; - - /* istanbul ignore if */ - if (typeof isValidUTF8 === 'object') { - isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0 - } +module.exports = { + isValidStatusCode, + isValidUTF8: _isValidUTF8, + tokenChars +}; - module.exports = { - isValidStatusCode, - isValidUTF8(buf) { - return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); - } - }; -} catch (e) /* istanbul ignore next */ { - module.exports = { - isValidStatusCode, - isValidUTF8: _isValidUTF8 +if (isUtf8) { + module.exports.isValidUTF8 = function (buf) { + return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf); }; +} /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) { + try { + const isValidUTF8 = utf8Validate; + + module.exports.isValidUTF8 = function (buf) { + return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf); + }; + } catch (e) { + // Continue regardless of the error. + } } }); -const { Writable } = require$$0__default['default']; +const { Writable } = require$$0__default$1['default']; const { @@ -1145,6 +1174,7 @@ const { const { concat, toArrayBuffer, unmask: unmask$1 } = bufferUtil; const { isValidStatusCode, isValidUTF8: isValidUTF8$1 } = validation; +const FastBuffer$1 = Buffer[Symbol.species]; const GET_INFO = 0; const GET_PAYLOAD_LENGTH_16 = 1; const GET_PAYLOAD_LENGTH_64 = 2; @@ -1155,26 +1185,31 @@ const INFLATING = 5; /** * HyBi Receiver implementation. * - * @extends stream.Writable + * @extends Writable */ class Receiver extends Writable { /** * Creates a Receiver instance. * - * @param {String} [binaryType=nodebuffer] The type for binary data - * @param {Object} [extensions] An object containing the negotiated extensions - * @param {Boolean} [isServer=false] Specifies whether to operate in client or - * server mode - * @param {Number} [maxPayload=0] The maximum allowed message length + * @param {Object} [options] Options object + * @param {String} [options.binaryType=nodebuffer] The type for binary data + * @param {Object} [options.extensions] An object containing the negotiated + * extensions + * @param {Boolean} [options.isServer=false] Specifies whether to operate in + * client or server mode + * @param {Number} [options.maxPayload=0] The maximum allowed message length + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages */ - constructor(binaryType, extensions, isServer, maxPayload) { + constructor(options = {}) { super(); - this._binaryType = binaryType || BINARY_TYPES[0]; + this._binaryType = options.binaryType || BINARY_TYPES[0]; + this._extensions = options.extensions || {}; + this._isServer = !!options.isServer; + this._maxPayload = options.maxPayload | 0; + this._skipUTF8Validation = !!options.skipUTF8Validation; this[kWebSocket] = undefined; - this._extensions = extensions || {}; - this._isServer = !!isServer; - this._maxPayload = maxPayload | 0; this._bufferedBytes = 0; this._buffers = []; @@ -1225,8 +1260,13 @@ class Receiver extends Writable { if (n < this._buffers[0].length) { const buf = this._buffers[0]; - this._buffers[0] = buf.slice(n); - return buf.slice(0, n); + this._buffers[0] = new FastBuffer$1( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); + + return new FastBuffer$1(buf.buffer, buf.byteOffset, n); } const dst = Buffer.allocUnsafe(n); @@ -1239,7 +1279,11 @@ class Receiver extends Writable { dst.set(this._buffers.shift(), offset); } else { dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); - this._buffers[0] = buf.slice(n); + this._buffers[0] = new FastBuffer$1( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); } n -= buf.length; @@ -1301,14 +1345,26 @@ class Receiver extends Writable { if ((buf[0] & 0x30) !== 0x00) { this._loop = false; - return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002); + return error( + RangeError, + 'RSV2 and RSV3 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_2_3' + ); } const compressed = (buf[0] & 0x40) === 0x40; if (compressed && !this._extensions[permessageDeflate.extensionName]) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } this._fin = (buf[0] & 0x80) === 0x80; @@ -1318,45 +1374,85 @@ class Receiver extends Writable { if (this._opcode === 0x00) { if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } if (!this._fragmented) { this._loop = false; - return error(RangeError, 'invalid opcode 0', true, 1002); + return error( + RangeError, + 'invalid opcode 0', + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._opcode = this._fragmented; } else if (this._opcode === 0x01 || this._opcode === 0x02) { if (this._fragmented) { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._compressed = compressed; } else if (this._opcode > 0x07 && this._opcode < 0x0b) { if (!this._fin) { this._loop = false; - return error(RangeError, 'FIN must be set', true, 1002); + return error( + RangeError, + 'FIN must be set', + true, + 1002, + 'WS_ERR_EXPECTED_FIN' + ); } if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } - if (this._payloadLength > 0x7d) { + if ( + this._payloadLength > 0x7d || + (this._opcode === 0x08 && this._payloadLength === 1) + ) { this._loop = false; return error( RangeError, `invalid payload length ${this._payloadLength}`, true, - 1002 + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' ); } } else { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } if (!this._fin && !this._fragmented) this._fragmented = this._opcode; @@ -1365,11 +1461,23 @@ class Receiver extends Writable { if (this._isServer) { if (!this._masked) { this._loop = false; - return error(RangeError, 'MASK must be set', true, 1002); + return error( + RangeError, + 'MASK must be set', + true, + 1002, + 'WS_ERR_EXPECTED_MASK' + ); } } else if (this._masked) { this._loop = false; - return error(RangeError, 'MASK must be clear', true, 1002); + return error( + RangeError, + 'MASK must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_MASK' + ); } if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; @@ -1418,7 +1526,8 @@ class Receiver extends Writable { RangeError, 'Unsupported WebSocket frame: payload length > 2^53 - 1', false, - 1009 + 1009, + 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' ); } @@ -1437,7 +1546,13 @@ class Receiver extends Writable { this._totalPayloadLength += this._payloadLength; if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { this._loop = false; - return error(RangeError, 'Max payload size exceeded', false, 1009); + return error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); } } @@ -1477,7 +1592,13 @@ class Receiver extends Writable { } data = this.consume(this._payloadLength); - if (this._masked) unmask$1(data, this._mask); + + if ( + this._masked && + (this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0 + ) { + unmask$1(data, this._mask); + } } if (this._opcode > 0x07) return this.controlMessage(data); @@ -1490,7 +1611,7 @@ class Receiver extends Writable { if (data.length) { // - // This message is not compressed so its lenght is the sum of the payload + // This message is not compressed so its length is the sum of the payload // length of all fragments. // this._messageLength = this._totalPayloadLength; @@ -1517,7 +1638,13 @@ class Receiver extends Writable { this._messageLength += buf.length; if (this._messageLength > this._maxPayload && this._maxPayload > 0) { return cb( - error(RangeError, 'Max payload size exceeded', false, 1009) + error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ) ); } @@ -1558,16 +1685,22 @@ class Receiver extends Writable { data = fragments; } - this.emit('message', data); + this.emit('message', data, true); } else { const buf = concat(fragments, messageLength); - if (!isValidUTF8$1(buf)) { + if (!this._skipUTF8Validation && !isValidUTF8$1(buf)) { this._loop = false; - return error(Error, 'invalid UTF-8 sequence', true, 1007); + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } - this.emit('message', buf.toString()); + this.emit('message', buf, false); } } @@ -1586,24 +1719,38 @@ class Receiver extends Writable { this._loop = false; if (data.length === 0) { - this.emit('conclude', 1005, ''); + this.emit('conclude', 1005, EMPTY_BUFFER); this.end(); - } else if (data.length === 1) { - return error(RangeError, 'invalid payload length 1', true, 1002); } else { const code = data.readUInt16BE(0); if (!isValidStatusCode(code)) { - return error(RangeError, `invalid status code ${code}`, true, 1002); + return error( + RangeError, + `invalid status code ${code}`, + true, + 1002, + 'WS_ERR_INVALID_CLOSE_CODE' + ); } - const buf = data.slice(2); + const buf = new FastBuffer$1( + data.buffer, + data.byteOffset + 2, + data.length - 2 + ); - if (!isValidUTF8$1(buf)) { - return error(Error, 'invalid UTF-8 sequence', true, 1007); + if (!this._skipUTF8Validation && !isValidUTF8$1(buf)) { + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } - this.emit('conclude', code, buf.toString()); + this.emit('conclude', code, buf); this.end(); } } else if (this._opcode === 0x09) { @@ -1621,32 +1768,35 @@ var receiver = Receiver; /** * Builds an error object. * - * @param {(Error|RangeError)} ErrorCtor The error constructor + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor * @param {String} message The error message * @param {Boolean} prefix Specifies whether or not to add a default prefix to * `message` * @param {Number} statusCode The status code + * @param {String} errorCode The exposed error code * @return {(Error|RangeError)} The error * @private */ -function error(ErrorCtor, message, prefix, statusCode) { +function error(ErrorCtor, message, prefix, statusCode, errorCode) { const err = new ErrorCtor( prefix ? `Invalid WebSocket frame: ${message}` : message ); Error.captureStackTrace(err, error); + err.code = errorCode; err[kStatusCode$1] = statusCode; return err; } -const { randomFillSync } = require$$0__default$1['default']; +const { randomFillSync } = require$$0__default$2['default']; const { EMPTY_BUFFER: EMPTY_BUFFER$1 } = constants; const { isValidStatusCode: isValidStatusCode$1 } = validation; const { mask: applyMask, toBuffer } = bufferUtil; -const mask$1 = Buffer.alloc(4); +const kByteLength = Symbol('kByteLength'); +const maskBuffer = Buffer.alloc(4); /** * HyBi Sender implementation. @@ -1655,11 +1805,19 @@ class Sender { /** * Creates a Sender instance. * - * @param {net.Socket} socket The connection socket + * @param {(net.Socket|tls.Socket)} socket The connection socket * @param {Object} [extensions] An object containing the negotiated extensions + * @param {Function} [generateMask] The function used to generate the masking + * key */ - constructor(socket, extensions) { + constructor(socket, extensions, generateMask) { this._extensions = extensions || {}; + + if (generateMask) { + this._generateMask = generateMask; + this._maskBuffer = Buffer.alloc(4); + } + this._socket = socket; this._firstFragment = true; @@ -1673,34 +1831,71 @@ class Sender { /** * Frames a piece of data according to the HyBi WebSocket protocol. * - * @param {Buffer} data The data to frame + * @param {(Buffer|String)} data The data to frame * @param {Object} options Options object - * @param {Number} options.opcode The opcode - * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be - * modified * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit - * @return {Buffer[]} The framed data as a list of `Buffer` instances + * @return {(Buffer|String)[]} The framed data * @public */ static frame(data, options) { - const merge = options.mask && options.readOnly; - let offset = options.mask ? 6 : 2; - let payloadLength = data.length; + let mask; + let merge = false; + let offset = 2; + let skipMasking = false; + + if (options.mask) { + mask = options.maskBuffer || maskBuffer; + + if (options.generateMask) { + options.generateMask(mask); + } else { + randomFillSync(mask, 0, 4); + } + + skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; + offset = 6; + } - if (data.length >= 65536) { + let dataLength; + + if (typeof data === 'string') { + if ( + (!options.mask || skipMasking) && + options[kByteLength] !== undefined + ) { + dataLength = options[kByteLength]; + } else { + data = Buffer.from(data); + dataLength = data.length; + } + } else { + dataLength = data.length; + merge = options.mask && options.readOnly && !skipMasking; + } + + let payloadLength = dataLength; + + if (dataLength >= 65536) { offset += 8; payloadLength = 127; - } else if (data.length > 125) { + } else if (dataLength > 125) { offset += 2; payloadLength = 126; } - const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); + const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset); target[0] = options.fin ? options.opcode | 0x80 : options.opcode; if (options.rsv1) target[0] |= 0x40; @@ -1708,28 +1903,28 @@ class Sender { target[1] = payloadLength; if (payloadLength === 126) { - target.writeUInt16BE(data.length, 2); + target.writeUInt16BE(dataLength, 2); } else if (payloadLength === 127) { - target.writeUInt32BE(0, 2); - target.writeUInt32BE(data.length, 6); + target[2] = target[3] = 0; + target.writeUIntBE(dataLength, 4, 6); } if (!options.mask) return [target, data]; - randomFillSync(mask$1, 0, 4); - target[1] |= 0x80; - target[offset - 4] = mask$1[0]; - target[offset - 3] = mask$1[1]; - target[offset - 2] = mask$1[2]; - target[offset - 1] = mask$1[3]; + target[offset - 4] = mask[0]; + target[offset - 3] = mask[1]; + target[offset - 2] = mask[2]; + target[offset - 1] = mask[3]; + + if (skipMasking) return [target, data]; if (merge) { - applyMask(data, mask$1, target, offset, data.length); + applyMask(data, mask, target, offset, dataLength); return [target]; } - applyMask(data, mask$1, data, 0, data.length); + applyMask(data, mask, data, 0, dataLength); return [target, data]; } @@ -1737,7 +1932,7 @@ class Sender { * Sends a close message to the other peer. * * @param {Number} [code] The status code component of the body - * @param {String} [data] The message component of the body + * @param {(String|Buffer)} [data] The message component of the body * @param {Boolean} [mask=false] Specifies whether or not to mask the message * @param {Function} [cb] Callback * @public @@ -1749,7 +1944,7 @@ class Sender { buf = EMPTY_BUFFER$1; } else if (typeof code !== 'number' || !isValidStatusCode$1(code)) { throw new TypeError('First argument must be a valid error code number'); - } else if (data === undefined || data === '') { + } else if (data === undefined || !data.length) { buf = Buffer.allocUnsafe(2); buf.writeUInt16BE(code, 0); } else { @@ -1761,37 +1956,32 @@ class Sender { buf = Buffer.allocUnsafe(2 + length); buf.writeUInt16BE(code, 0); - buf.write(data, 2); + + if (typeof data === 'string') { + buf.write(data, 2); + } else { + buf.set(data, 2); + } } + const options = { + [kByteLength]: buf.length, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x08, + readOnly: false, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doClose, buf, mask, cb]); + this.enqueue([this.dispatch, buf, false, options, cb]); } else { - this.doClose(buf, mask, cb); + this.sendFrame(Sender.frame(buf, options), cb); } } - /** - * Frames and sends a close message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Function} [cb] Callback - * @private - */ - doClose(data, mask, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x08, - mask, - readOnly: false - }), - cb - ); - } - /** * Sends a ping message to the other peer. * @@ -1801,41 +1991,40 @@ class Sender { * @public */ ping(data, mask, cb) { - const buf = toBuffer(data); + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } - if (buf.length > 125) { + if (byteLength > 125) { throw new RangeError('The data size must not be greater than 125 bytes'); } + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x09, + readOnly, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]); + this.enqueue([this.dispatch, data, false, options, cb]); } else { - this.doPing(buf, mask, toBuffer.readOnly, cb); + this.sendFrame(Sender.frame(data, options), cb); } } - /** - * Frames and sends a ping message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified - * @param {Function} [cb] Callback - * @private - */ - doPing(data, mask, readOnly, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x09, - mask, - readOnly - }), - cb - ); - } - /** * Sends a pong message to the other peer. * @@ -1845,50 +2034,49 @@ class Sender { * @public */ pong(data, mask, cb) { - const buf = toBuffer(data); + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } - if (buf.length > 125) { + if (byteLength > 125) { throw new RangeError('The data size must not be greater than 125 bytes'); } + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x0a, + readOnly, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]); + this.enqueue([this.dispatch, data, false, options, cb]); } else { - this.doPong(buf, mask, toBuffer.readOnly, cb); + this.sendFrame(Sender.frame(data, options), cb); } } - /** - * Frames and sends a pong message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified - * @param {Function} [cb] Callback - * @private - */ - doPong(data, mask, readOnly, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x0a, - mask, - readOnly - }), - cb - ); - } - /** * Sends a data message to the other peer. * * @param {*} data The message to send * @param {Object} options Options object - * @param {Boolean} [options.compress=false] Specifies whether or not to - * compress `data` * @param {Boolean} [options.binary=false] Specifies whether `data` is binary * or text + * @param {Boolean} [options.compress=false] Specifies whether or not to + * compress `data` * @param {Boolean} [options.fin=false] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask=false] Specifies whether or not to mask @@ -1897,15 +2085,34 @@ class Sender { * @public */ send(data, options, cb) { - const buf = toBuffer(data); const perMessageDeflate = this._extensions[permessageDeflate.extensionName]; let opcode = options.binary ? 2 : 1; let rsv1 = options.compress; + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + if (this._firstFragment) { this._firstFragment = false; - if (rsv1 && perMessageDeflate) { - rsv1 = buf.length >= perMessageDeflate._threshold; + if ( + rsv1 && + perMessageDeflate && + perMessageDeflate.params[ + perMessageDeflate._isServer + ? 'server_no_context_takeover' + : 'client_no_context_takeover' + ] + ) { + rsv1 = byteLength >= perMessageDeflate._threshold; } this._compress = rsv1; } else { @@ -1917,26 +2124,32 @@ class Sender { if (perMessageDeflate) { const opts = { + [kByteLength]: byteLength, fin: options.fin, - rsv1, - opcode, + generateMask: this._generateMask, mask: options.mask, - readOnly: toBuffer.readOnly + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1 }; if (this._deflating) { - this.enqueue([this.dispatch, buf, this._compress, opts, cb]); + this.enqueue([this.dispatch, data, this._compress, opts, cb]); } else { - this.dispatch(buf, this._compress, opts, cb); + this.dispatch(data, this._compress, opts, cb); } } else { this.sendFrame( - Sender.frame(buf, { + Sender.frame(data, { + [kByteLength]: byteLength, fin: options.fin, - rsv1: false, - opcode, + generateMask: this._generateMask, mask: options.mask, - readOnly: toBuffer.readOnly + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1: false }), cb ); @@ -1944,19 +2157,23 @@ class Sender { } /** - * Dispatches a data message. + * Dispatches a message. * - * @param {Buffer} data The message to send + * @param {(Buffer|String)} data The message to send * @param {Boolean} [compress=false] Specifies whether or not to compress * `data` * @param {Object} options Options object - * @param {Number} options.opcode The opcode - * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be - * modified * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit * @param {Function} [cb] Callback @@ -1970,7 +2187,7 @@ class Sender { const perMessageDeflate = this._extensions[permessageDeflate.extensionName]; - this._bufferedBytes += data.length; + this._bufferedBytes += options[kByteLength]; this._deflating = true; perMessageDeflate.compress(data, options.fin, (_, buf) => { if (this._socket.destroyed) { @@ -1981,7 +2198,8 @@ class Sender { if (typeof cb === 'function') cb(err); for (let i = 0; i < this._queue.length; i++) { - const callback = this._queue[i][4]; + const params = this._queue[i]; + const callback = params[params.length - 1]; if (typeof callback === 'function') callback(err); } @@ -1989,7 +2207,7 @@ class Sender { return; } - this._bufferedBytes -= data.length; + this._bufferedBytes -= options[kByteLength]; this._deflating = false; options.readOnly = false; this.sendFrame(Sender.frame(buf, options), cb); @@ -2006,7 +2224,7 @@ class Sender { while (!this._deflating && this._queue.length) { const params = this._queue.shift(); - this._bufferedBytes -= params[1].length; + this._bufferedBytes -= params[3][kByteLength]; Reflect.apply(params[0], this, params.slice(1)); } } @@ -2018,7 +2236,7 @@ class Sender { * @private */ enqueue(params) { - this._bufferedBytes += params[1].length; + this._bufferedBytes += params[3][kByteLength]; this._queue.push(params); } @@ -2043,112 +2261,173 @@ class Sender { var sender = Sender; +const { kForOnEventAttribute, kListener } = constants; + +const kCode = Symbol('kCode'); +const kData = Symbol('kData'); +const kError$1 = Symbol('kError'); +const kMessage = Symbol('kMessage'); +const kReason = Symbol('kReason'); +const kTarget = Symbol('kTarget'); +const kType = Symbol('kType'); +const kWasClean = Symbol('kWasClean'); + /** * Class representing an event. - * - * @private */ class Event { /** * Create a new `Event`. * * @param {String} type The name of the event - * @param {Object} target A reference to the target to which the event was - * dispatched + * @throws {TypeError} If the `type` argument is not specified + */ + constructor(type) { + this[kTarget] = null; + this[kType] = type; + } + + /** + * @type {*} + */ + get target() { + return this[kTarget]; + } + + /** + * @type {String} */ - constructor(type, target) { - this.target = target; - this.type = type; + get type() { + return this[kType]; } } +Object.defineProperty(Event.prototype, 'target', { enumerable: true }); +Object.defineProperty(Event.prototype, 'type', { enumerable: true }); + /** - * Class representing a message event. + * Class representing a close event. * * @extends Event - * @private */ -class MessageEvent extends Event { +class CloseEvent extends Event { /** - * Create a new `MessageEvent`. + * Create a new `CloseEvent`. * - * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {Number} [options.code=0] The status code explaining why the + * connection was closed + * @param {String} [options.reason=''] A human-readable string explaining why + * the connection was closed + * @param {Boolean} [options.wasClean=false] Indicates whether or not the + * connection was cleanly closed */ - constructor(data, target) { - super('message', target); + constructor(type, options = {}) { + super(type); - this.data = data; + this[kCode] = options.code === undefined ? 0 : options.code; + this[kReason] = options.reason === undefined ? '' : options.reason; + this[kWasClean] = options.wasClean === undefined ? false : options.wasClean; } -} -/** - * Class representing a close event. - * - * @extends Event - * @private - */ -class CloseEvent extends Event { /** - * Create a new `CloseEvent`. - * - * @param {Number} code The status code explaining why the connection is being - * closed - * @param {String} reason A human-readable string explaining why the - * connection is closing - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @type {Number} */ - constructor(code, reason, target) { - super('close', target); + get code() { + return this[kCode]; + } - this.wasClean = target._closeFrameReceived && target._closeFrameSent; - this.reason = reason; - this.code = code; + /** + * @type {String} + */ + get reason() { + return this[kReason]; + } + + /** + * @type {Boolean} + */ + get wasClean() { + return this[kWasClean]; } } +Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true }); + /** - * Class representing an open event. + * Class representing an error event. * * @extends Event - * @private */ -class OpenEvent extends Event { +class ErrorEvent extends Event { /** - * Create a new `OpenEvent`. + * Create a new `ErrorEvent`. * - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.error=null] The error that generated this event + * @param {String} [options.message=''] The error message + */ + constructor(type, options = {}) { + super(type); + + this[kError$1] = options.error === undefined ? null : options.error; + this[kMessage] = options.message === undefined ? '' : options.message; + } + + /** + * @type {*} */ - constructor(target) { - super('open', target); + get error() { + return this[kError$1]; + } + + /** + * @type {String} + */ + get message() { + return this[kMessage]; } } +Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true }); +Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true }); + /** - * Class representing an error event. + * Class representing a message event. * * @extends Event - * @private */ -class ErrorEvent extends Event { +class MessageEvent extends Event { /** - * Create a new `ErrorEvent`. + * Create a new `MessageEvent`. * - * @param {Object} error The error that generated this event - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.data=null] The message content */ - constructor(error, target) { - super('error', target); + constructor(type, options = {}) { + super(type); + + this[kData] = options.data === undefined ? null : options.data; + } - this.message = error.message; - this.error = error; + /** + * @type {*} + */ + get data() { + return this[kData]; } } +Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true }); + /** * This provides methods for emulating the `EventTarget` interface. It's not * meant to be used directly. @@ -2160,49 +2439,75 @@ const EventTarget = { * Register an event listener. * * @param {String} type A string representing the event type to listen for - * @param {Function} listener The listener to add + * @param {(Function|Object)} handler The listener to add * @param {Object} [options] An options object specifies characteristics about * the event listener - * @param {Boolean} [options.once=false] A `Boolean`` indicating that the + * @param {Boolean} [options.once=false] A `Boolean` indicating that the * listener should be invoked at most once after being added. If `true`, * the listener would be automatically removed when invoked. * @public */ - addEventListener(type, listener, options) { - if (typeof listener !== 'function') return; - - function onMessage(data) { - listener.call(this, new MessageEvent(data, this)); - } - - function onClose(code, message) { - listener.call(this, new CloseEvent(code, message, this)); - } - - function onError(error) { - listener.call(this, new ErrorEvent(error, this)); - } - - function onOpen() { - listener.call(this, new OpenEvent(this)); + addEventListener(type, handler, options = {}) { + for (const listener of this.listeners(type)) { + if ( + !options[kForOnEventAttribute] && + listener[kListener] === handler && + !listener[kForOnEventAttribute] + ) { + return; + } } - const method = options && options.once ? 'once' : 'on'; + let wrapper; if (type === 'message') { - onMessage._listener = listener; - this[method](type, onMessage); + wrapper = function onMessage(data, isBinary) { + const event = new MessageEvent('message', { + data: isBinary ? data : data.toString() + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'close') { - onClose._listener = listener; - this[method](type, onClose); + wrapper = function onClose(code, message) { + const event = new CloseEvent('close', { + code, + reason: message.toString(), + wasClean: this._closeFrameReceived && this._closeFrameSent + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'error') { - onError._listener = listener; - this[method](type, onError); + wrapper = function onError(error) { + const event = new ErrorEvent('error', { + error, + message: error.message + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'open') { - onOpen._listener = listener; - this[method](type, onOpen); + wrapper = function onOpen() { + const event = new Event('open'); + + event[kTarget] = this; + callListener(handler, this, event); + }; + } else { + return; + } + + wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; + wrapper[kListener] = handler; + + if (options.once) { + this.once(type, wrapper); } else { - this[method](type, listener); + this.on(type, wrapper); } }, @@ -2210,44 +2515,44 @@ const EventTarget = { * Remove an event listener. * * @param {String} type A string representing the event type to remove - * @param {Function} listener The listener to remove + * @param {(Function|Object)} handler The listener to remove * @public */ - removeEventListener(type, listener) { - const listeners = this.listeners(type); - - for (let i = 0; i < listeners.length; i++) { - if (listeners[i] === listener || listeners[i]._listener === listener) { - this.removeListener(type, listeners[i]); + removeEventListener(type, handler) { + for (const listener of this.listeners(type)) { + if (listener[kListener] === handler && !listener[kForOnEventAttribute]) { + this.removeListener(type, listener); + break; } } } }; -var eventTarget = EventTarget; +var eventTarget = { + CloseEvent, + ErrorEvent, + Event, + EventTarget, + MessageEvent +}; -// -// Allowed token characters: -// -// '!', '#', '$', '%', '&', ''', '*', '+', '-', -// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' -// -// tokenChars[32] === 0 // ' ' -// tokenChars[33] === 1 // '!' -// tokenChars[34] === 0 // '"' -// ... -// -// prettier-ignore -const tokenChars = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 - 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 -]; +/** + * Call an event listener + * + * @param {(Function|Object)} listener The listener to call + * @param {*} thisArg The value to use as `this`` when calling the listener + * @param {Event} event The event to pass to the listener + * @private + */ +function callListener(listener, thisArg, event) { + if (typeof listener === 'object' && listener.handleEvent) { + listener.handleEvent.call(listener, event); + } else { + listener.call(thisArg, event); + } +} + +const { tokenChars } = validation; /** * Adds an offer to the map of extension offers or a parameter to the map of @@ -2273,9 +2578,6 @@ function push(dest, name, elem) { */ function parse(header) { const offers = Object.create(null); - - if (header === undefined || header === '') return offers; - let params = Object.create(null); let mustUnescape = false; let isEscaping = false; @@ -2283,16 +2585,20 @@ function parse(header) { let extensionName; let paramName; let start = -1; + let code = -1; let end = -1; let i = 0; for (; i < header.length; i++) { - const code = header.charCodeAt(i); + code = header.charCodeAt(i); if (extensionName === undefined) { if (end === -1 && tokenChars[code] === 1) { if (start === -1) start = i; - } else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) { + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { if (end === -1 && start !== -1) end = i; } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { if (start === -1) { @@ -2393,7 +2699,7 @@ function parse(header) { } } - if (start === -1 || inQuotes) { + if (start === -1 || inQuotes || code === 0x20 || code === 0x09) { throw new SyntaxError('Unexpected end of input'); } @@ -2448,8 +2754,8 @@ function format(extensions) { var extension = { format, parse }; -const { randomBytes, createHash } = require$$0__default$1['default']; -const { URL } = require$$1__default['default']; +const { randomBytes, createHash } = require$$0__default$2['default']; +const { URL } = require$$2__default['default']; @@ -2458,17 +2764,23 @@ const { BINARY_TYPES: BINARY_TYPES$1, EMPTY_BUFFER: EMPTY_BUFFER$2, GUID, + kForOnEventAttribute: kForOnEventAttribute$1, + kListener: kListener$1, kStatusCode: kStatusCode$2, kWebSocket: kWebSocket$1, - NOOP: NOOP$1 + NOOP } = constants; -const { addEventListener, removeEventListener } = eventTarget; +const { + EventTarget: { addEventListener, removeEventListener } +} = eventTarget; const { format: format$1, parse: parse$1 } = extension; const { toBuffer: toBuffer$1 } = bufferUtil; -const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; -const protocolVersions = [8, 13]; const closeTimeout = 30 * 1000; +const kAborted = Symbol('kAborted'); +const protocolVersions = [8, 13]; +const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; +const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/; /** * Class representing a WebSocket. @@ -2479,7 +2791,7 @@ class WebSocket extends EventEmitter__default['default'] { /** * Create a new `WebSocket`. * - * @param {(String|url.URL)} address The URL to which to connect + * @param {(String|URL)} address The URL to which to connect * @param {(String|String[])} [protocols] The subprotocols * @param {Object} [options] Connection options */ @@ -2490,9 +2802,10 @@ class WebSocket extends EventEmitter__default['default'] { this._closeCode = 1006; this._closeFrameReceived = false; this._closeFrameSent = false; - this._closeMessage = ''; + this._closeMessage = EMPTY_BUFFER$2; this._closeTimer = null; this._extensions = {}; + this._paused = false; this._protocol = ''; this._readyState = WebSocket.CONNECTING; this._receiver = null; @@ -2504,11 +2817,15 @@ class WebSocket extends EventEmitter__default['default'] { this._isServer = false; this._redirects = 0; - if (Array.isArray(protocols)) { - protocols = protocols.join(', '); - } else if (typeof protocols === 'object' && protocols !== null) { - options = protocols; - protocols = undefined; + if (protocols === undefined) { + protocols = []; + } else if (!Array.isArray(protocols)) { + if (typeof protocols === 'object' && protocols !== null) { + options = protocols; + protocols = []; + } else { + protocols = [protocols]; + } } initAsClient(this, address, protocols, options); @@ -2555,6 +2872,45 @@ class WebSocket extends EventEmitter__default['default'] { return Object.keys(this._extensions).join(); } + /** + * @type {Boolean} + */ + get isPaused() { + return this._paused; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onclose() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onerror() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onopen() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onmessage() { + return null; + } + /** * @type {String} */ @@ -2579,20 +2935,27 @@ class WebSocket extends EventEmitter__default['default'] { /** * Set up the socket and the internal resources. * - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream - * @param {Number} [maxPayload=0] The maximum allowed message size + * @param {Object} options Options object + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Number} [options.maxPayload=0] The maximum allowed message size + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ - setSocket(socket, head, maxPayload) { - const receiver$1 = new receiver( - this.binaryType, - this._extensions, - this._isServer, - maxPayload - ); + setSocket(socket, head, options) { + const receiver$1 = new receiver({ + binaryType: this.binaryType, + extensions: this._extensions, + isServer: this._isServer, + maxPayload: options.maxPayload, + skipUTF8Validation: options.skipUTF8Validation + }); - this._sender = new sender(socket, this._extensions); + this._sender = new sender(socket, this._extensions, options.generateMask); this._receiver = receiver$1; this._socket = socket; @@ -2657,18 +3020,26 @@ class WebSocket extends EventEmitter__default['default'] { * +---+ * * @param {Number} [code] Status code explaining why the connection is closing - * @param {String} [data] A string explaining why the connection is closing + * @param {(String|Buffer)} [data] The reason why the connection is + * closing * @public */ close(code, data) { if (this.readyState === WebSocket.CLOSED) return; if (this.readyState === WebSocket.CONNECTING) { const msg = 'WebSocket was closed before the connection was established'; - return abortHandshake(this, this._req, msg); + abortHandshake(this, this._req, msg); + return; } if (this.readyState === WebSocket.CLOSING) { - if (this._closeFrameSent && this._closeFrameReceived) this._socket.end(); + if ( + this._closeFrameSent && + (this._closeFrameReceived || this._receiver._writableState.errorEmitted) + ) { + this._socket.end(); + } + return; } @@ -2681,7 +3052,13 @@ class WebSocket extends EventEmitter__default['default'] { if (err) return; this._closeFrameSent = true; - if (this._closeFrameReceived) this._socket.end(); + + if ( + this._closeFrameReceived || + this._receiver._writableState.errorEmitted + ) { + this._socket.end(); + } }); // @@ -2693,6 +3070,23 @@ class WebSocket extends EventEmitter__default['default'] { ); } + /** + * Pause the socket. + * + * @public + */ + pause() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = true; + this._socket.pause(); + } + /** * Send a ping. * @@ -2757,15 +3151,32 @@ class WebSocket extends EventEmitter__default['default'] { this._sender.pong(data || EMPTY_BUFFER$2, mask, cb); } + /** + * Resume the socket. + * + * @public + */ + resume() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = false; + if (!this._receiver._writableState.needDrain) this._socket.resume(); + } + /** * Send a data message. * * @param {*} data The message to send * @param {Object} [options] Options object - * @param {Boolean} [options.compress] Specifies whether or not to compress - * `data` * @param {Boolean} [options.binary] Specifies whether `data` is binary or * text + * @param {Boolean} [options.compress] Specifies whether or not to compress + * `data` * @param {Boolean} [options.fin=true] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask] Specifies whether or not to mask `data` @@ -2813,7 +3224,8 @@ class WebSocket extends EventEmitter__default['default'] { if (this.readyState === WebSocket.CLOSED) return; if (this.readyState === WebSocket.CONNECTING) { const msg = 'WebSocket was closed before the connection was established'; - return abortHandshake(this, this._req, msg); + abortHandshake(this, this._req, msg); + return; } if (this._socket) { @@ -2823,17 +3235,83 @@ class WebSocket extends EventEmitter__default['default'] { } } -readyStates.forEach((readyState, i) => { - const descriptor = { enumerable: true, value: i }; +/** + * @constant {Number} CONNECTING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} CONNECTING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); - Object.defineProperty(WebSocket.prototype, readyState, descriptor); - Object.defineProperty(WebSocket, readyState, descriptor); +/** + * @constant {Number} OPEN + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') }); [ 'binaryType', 'bufferedAmount', 'extensions', + 'isPaused', 'protocol', 'readyState', 'url' @@ -2847,37 +3325,27 @@ readyStates.forEach((readyState, i) => { // ['open', 'error', 'close', 'message'].forEach((method) => { Object.defineProperty(WebSocket.prototype, `on${method}`, { - configurable: true, enumerable: true, - /** - * Return the listener of the event. - * - * @return {(Function|undefined)} The event listener or `undefined` - * @public - */ get() { - const listeners = this.listeners(method); - for (let i = 0; i < listeners.length; i++) { - if (listeners[i]._listener) return listeners[i]._listener; + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute$1]) return listener[kListener$1]; } - return undefined; + return null; }, - /** - * Add a listener for the event. - * - * @param {Function} listener The listener to add - * @public - */ - set(listener) { - const listeners = this.listeners(method); - for (let i = 0; i < listeners.length; i++) { - // - // Remove only the listeners added via `addEventListener`. - // - if (listeners[i]._listener) this.removeListener(method, listeners[i]); + set(handler) { + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute$1]) { + this.removeListener(method, listener); + break; + } } - this.addEventListener(method, listener); + + if (typeof handler !== 'function') return; + + this.addEventListener(method, handler, { + [kForOnEventAttribute$1]: true + }); } }); }); @@ -2891,29 +3359,34 @@ var websocket = WebSocket; * Initialize a WebSocket client. * * @param {WebSocket} websocket The client to initialize - * @param {(String|url.URL)} address The URL to which to connect - * @param {String} [protocols] The subprotocols + * @param {(String|URL)} address The URL to which to connect + * @param {Array} protocols The subprotocols * @param {Object} [options] Connection options - * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable - * permessage-deflate + * @param {Boolean} [options.followRedirects=false] Whether or not to follow + * redirects + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the * handshake request - * @param {Number} [options.protocolVersion=13] Value of the - * `Sec-WebSocket-Version` header - * @param {String} [options.origin] Value of the `Origin` or - * `Sec-WebSocket-Origin` header * @param {Number} [options.maxPayload=104857600] The maximum allowed message * size - * @param {Boolean} [options.followRedirects=false] Whether or not to follow - * redirects * @param {Number} [options.maxRedirects=10] The maximum number of redirects * allowed + * @param {String} [options.origin] Value of the `Origin` or + * `Sec-WebSocket-Origin` header + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable + * permessage-deflate + * @param {Number} [options.protocolVersion=13] Value of the + * `Sec-WebSocket-Version` header + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ function initAsClient(websocket, address, protocols, options) { const opts = { protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: true, followRedirects: false, maxRedirects: 10, @@ -2923,7 +3396,7 @@ function initAsClient(websocket, address, protocols, options) { hostname: undefined, protocol: undefined, timeout: undefined, - method: undefined, + method: 'GET', host: undefined, path: undefined, port: undefined @@ -2942,21 +3415,43 @@ function initAsClient(websocket, address, protocols, options) { parsedUrl = address; websocket._url = address.href; } else { - parsedUrl = new URL(address); + try { + parsedUrl = new URL(address); + } catch (e) { + throw new SyntaxError(`Invalid URL: ${address}`); + } + websocket._url = address; } - const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; + const isSecure = parsedUrl.protocol === 'wss:'; + const isIpcUrl = parsedUrl.protocol === 'ws+unix:'; + let invalidUrlMessage; + + if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { + invalidUrlMessage = + 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; + } else if (isIpcUrl && !parsedUrl.pathname) { + invalidUrlMessage = "The URL's pathname is empty"; + } else if (parsedUrl.hash) { + invalidUrlMessage = 'The URL contains a fragment identifier'; + } + + if (invalidUrlMessage) { + const err = new SyntaxError(invalidUrlMessage); - if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) { - throw new Error(`Invalid URL: ${websocket.url}`); + if (websocket._redirects === 0) { + throw err; + } else { + emitErrorAndClose(websocket, err); + return; + } } - const isSecure = - parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:'; const defaultPort = isSecure ? 443 : 80; const key = randomBytes(16).toString('base64'); - const get = isSecure ? https__default['default'].get : http__default['default'].get; + const request = isSecure ? https__default['default'].request : http__default['default'].request; + const protocolSet = new Set(); let perMessageDeflate; opts.createConnection = isSecure ? tlsConnect : netConnect; @@ -2966,11 +3461,11 @@ function initAsClient(websocket, address, protocols, options) { ? parsedUrl.hostname.slice(1, -1) : parsedUrl.hostname; opts.headers = { + ...opts.headers, 'Sec-WebSocket-Version': opts.protocolVersion, 'Sec-WebSocket-Key': key, Connection: 'Upgrade', - Upgrade: 'websocket', - ...opts.headers + Upgrade: 'websocket' }; opts.path = parsedUrl.pathname + parsedUrl.search; opts.timeout = opts.handshakeTimeout; @@ -2985,8 +3480,22 @@ function initAsClient(websocket, address, protocols, options) { [permessageDeflate.extensionName]: perMessageDeflate.offer() }); } - if (protocols) { - opts.headers['Sec-WebSocket-Protocol'] = protocols; + if (protocols.length) { + for (const protocol of protocols) { + if ( + typeof protocol !== 'string' || + !subprotocolRegex.test(protocol) || + protocolSet.has(protocol) + ) { + throw new SyntaxError( + 'An invalid or duplicated subprotocol was specified' + ); + } + + protocolSet.add(protocol); + } + + opts.headers['Sec-WebSocket-Protocol'] = protocols.join(','); } if (opts.origin) { if (opts.protocolVersion < 13) { @@ -2999,15 +3508,87 @@ function initAsClient(websocket, address, protocols, options) { opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; } - if (isUnixSocket) { + if (isIpcUrl) { const parts = opts.path.split(':'); - opts.socketPath = parts[0]; - opts.path = parts[1]; + opts.socketPath = parts[0]; + opts.path = parts[1]; + } + + let req; + + if (opts.followRedirects) { + if (websocket._redirects === 0) { + websocket._originalIpc = isIpcUrl; + websocket._originalSecure = isSecure; + websocket._originalHostOrSocketPath = isIpcUrl + ? opts.socketPath + : parsedUrl.host; + + const headers = options && options.headers; + + // + // Shallow copy the user provided options so that headers can be changed + // without mutating the original object. + // + options = { ...options, headers: {} }; + + if (headers) { + for (const [key, value] of Object.entries(headers)) { + options.headers[key.toLowerCase()] = value; + } + } + } else if (websocket.listenerCount('redirect') === 0) { + const isSameHost = isIpcUrl + ? websocket._originalIpc + ? opts.socketPath === websocket._originalHostOrSocketPath + : false + : websocket._originalIpc + ? false + : parsedUrl.host === websocket._originalHostOrSocketPath; + + if (!isSameHost || (websocket._originalSecure && !isSecure)) { + // + // Match curl 7.77.0 behavior and drop the following headers. These + // headers are also dropped when following a redirect to a subdomain. + // + delete opts.headers.authorization; + delete opts.headers.cookie; + + if (!isSameHost) delete opts.headers.host; + + opts.auth = undefined; + } + } + + // + // Match curl 7.77.0 behavior and make the first `Authorization` header win. + // If the `Authorization` header is set, then there is nothing to do as it + // will take precedence. + // + if (opts.auth && !options.headers.authorization) { + options.headers.authorization = + 'Basic ' + Buffer.from(opts.auth).toString('base64'); + } + + req = websocket._req = request(opts); + + if (websocket._redirects) { + // + // Unlike what is done for the `'upgrade'` event, no early exit is + // triggered here if the user calls `websocket.close()` or + // `websocket.terminate()` from a listener of the `'redirect'` event. This + // is because the user can also call `request.destroy()` with an error + // before calling `websocket.close()` or `websocket.terminate()` and this + // would result in an error being emitted on the `request` object with no + // `'error'` event listeners attached. + // + websocket.emit('redirect', websocket.url, req); + } + } else { + req = websocket._req = request(opts); } - let req = (websocket._req = get(opts)); - if (opts.timeout) { req.on('timeout', () => { abortHandshake(websocket, req, 'Opening handshake has timed out'); @@ -3015,12 +3596,10 @@ function initAsClient(websocket, address, protocols, options) { } req.on('error', (err) => { - if (req === null || req.aborted) return; + if (req === null || req[kAborted]) return; req = websocket._req = null; - websocket._readyState = WebSocket.CLOSING; - websocket.emit('error', err); - websocket.emitClose(); + emitErrorAndClose(websocket, err); }); req.on('response', (res) => { @@ -3040,7 +3619,15 @@ function initAsClient(websocket, address, protocols, options) { req.abort(); - const addr = new URL(location, address); + let addr; + + try { + addr = new URL(location, address); + } catch (e) { + const err = new SyntaxError(`Invalid URL: ${location}`); + emitErrorAndClose(websocket, err); + return; + } initAsClient(websocket, addr, protocols, options); } else if (!websocket.emit('unexpected-response', req, res)) { @@ -3056,13 +3643,18 @@ function initAsClient(websocket, address, protocols, options) { websocket.emit('upgrade', res); // - // The user may have closed the connection from a listener of the `upgrade` - // event. + // The user may have closed the connection from a listener of the + // `'upgrade'` event. // if (websocket.readyState !== WebSocket.CONNECTING) return; req = websocket._req = null; + if (res.headers.upgrade.toLowerCase() !== 'websocket') { + abortHandshake(websocket, socket, 'Invalid Upgrade header'); + return; + } + const digest = createHash('sha1') .update(key + GUID) .digest('base64'); @@ -3073,15 +3665,16 @@ function initAsClient(websocket, address, protocols, options) { } const serverProt = res.headers['sec-websocket-protocol']; - const protList = (protocols || '').split(/, */); let protError; - if (!protocols && serverProt) { - protError = 'Server sent a subprotocol but none was requested'; - } else if (protocols && !serverProt) { + if (serverProt !== undefined) { + if (!protocolSet.size) { + protError = 'Server sent a subprotocol but none was requested'; + } else if (!protocolSet.has(serverProt)) { + protError = 'Server sent an invalid subprotocol'; + } + } else if (protocolSet.size) { protError = 'Server sent no subprotocol'; - } else if (serverProt && !protList.includes(serverProt)) { - protError = 'Server sent an invalid subprotocol'; } if (protError) { @@ -3091,28 +3684,75 @@ function initAsClient(websocket, address, protocols, options) { if (serverProt) websocket._protocol = serverProt; - if (perMessageDeflate) { + const secWebSocketExtensions = res.headers['sec-websocket-extensions']; + + if (secWebSocketExtensions !== undefined) { + if (!perMessageDeflate) { + const message = + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested'; + abortHandshake(websocket, socket, message); + return; + } + + let extensions; + try { - const extensions = parse$1(res.headers['sec-websocket-extensions']); + extensions = parse$1(secWebSocketExtensions); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } - if (extensions[permessageDeflate.extensionName]) { - perMessageDeflate.accept(extensions[permessageDeflate.extensionName]); - websocket._extensions[ - permessageDeflate.extensionName - ] = perMessageDeflate; - } + const extensionNames = Object.keys(extensions); + + if ( + extensionNames.length !== 1 || + extensionNames[0] !== permessageDeflate.extensionName + ) { + const message = 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } + + try { + perMessageDeflate.accept(extensions[permessageDeflate.extensionName]); } catch (err) { - abortHandshake( - websocket, - socket, - 'Invalid Sec-WebSocket-Extensions header' - ); + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); return; } + + websocket._extensions[permessageDeflate.extensionName] = + perMessageDeflate; } - websocket.setSocket(socket, head, opts.maxPayload); + websocket.setSocket(socket, head, { + generateMask: opts.generateMask, + maxPayload: opts.maxPayload, + skipUTF8Validation: opts.skipUTF8Validation + }); }); + + if (opts.finishRequest) { + opts.finishRequest(req, websocket); + } else { + req.end(); + } +} + +/** + * Emit the `'error'` and `'close'` events. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {Error} The error to emit + * @private + */ +function emitErrorAndClose(websocket, err) { + websocket._readyState = WebSocket.CLOSING; + websocket.emit('error', err); + websocket.emitClose(); } /** @@ -3148,8 +3788,8 @@ function tlsConnect(options) { * Abort the handshake and emit an error. * * @param {WebSocket} websocket The WebSocket instance - * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the - * socket to destroy + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to + * abort or the socket to destroy * @param {String} message The error message * @private */ @@ -3160,6 +3800,7 @@ function abortHandshake(websocket, stream, message) { Error.captureStackTrace(err, abortHandshake); if (stream.setHeader) { + stream[kAborted] = true; stream.abort(); if (stream.socket && !stream.socket.destroyed) { @@ -3171,8 +3812,7 @@ function abortHandshake(websocket, stream, message) { stream.socket.destroy(); } - stream.once('abort', websocket.emitClose.bind(websocket)); - websocket.emit('error', err); + process.nextTick(emitErrorAndClose, websocket, err); } else { stream.destroy(err); stream.once('error', websocket.emit.bind(websocket, 'error')); @@ -3208,7 +3848,7 @@ function sendAfterClose(websocket, data, cb) { `WebSocket is not open: readyState ${websocket.readyState} ` + `(${readyStates[websocket.readyState]})` ); - cb(err); + process.nextTick(cb, err); } } @@ -3216,19 +3856,21 @@ function sendAfterClose(websocket, data, cb) { * The listener of the `Receiver` `'conclude'` event. * * @param {Number} code The status code - * @param {String} reason The reason for closing + * @param {Buffer} reason The reason for closing * @private */ function receiverOnConclude(code, reason) { const websocket = this[kWebSocket$1]; - websocket._socket.removeListener('data', socketOnData); - websocket._socket.resume(); - websocket._closeFrameReceived = true; websocket._closeMessage = reason; websocket._closeCode = code; + if (websocket._socket[kWebSocket$1] === undefined) return; + + websocket._socket.removeListener('data', socketOnData); + process.nextTick(resume, websocket._socket); + if (code === 1005) websocket.close(); else websocket.close(code, reason); } @@ -3239,7 +3881,9 @@ function receiverOnConclude(code, reason) { * @private */ function receiverOnDrain() { - this[kWebSocket$1]._socket.resume(); + const websocket = this[kWebSocket$1]; + + if (!websocket.isPaused) websocket._socket.resume(); } /** @@ -3251,12 +3895,19 @@ function receiverOnDrain() { function receiverOnError(err) { const websocket = this[kWebSocket$1]; - websocket._socket.removeListener('data', socketOnData); + if (websocket._socket[kWebSocket$1] !== undefined) { + websocket._socket.removeListener('data', socketOnData); + + // + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See + // https://github.com/websockets/ws/issues/1940. + // + process.nextTick(resume, websocket._socket); + + websocket.close(err[kStatusCode$2]); + } - websocket._readyState = WebSocket.CLOSING; - websocket._closeCode = err[kStatusCode$2]; websocket.emit('error', err); - websocket._socket.destroy(); } /** @@ -3271,11 +3922,12 @@ function receiverOnFinish() { /** * The listener of the `Receiver` `'message'` event. * - * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Boolean} isBinary Specifies whether the message is binary or not * @private */ -function receiverOnMessage(data) { - this[kWebSocket$1].emit('message', data); +function receiverOnMessage(data, isBinary) { + this[kWebSocket$1].emit('message', data, isBinary); } /** @@ -3287,7 +3939,7 @@ function receiverOnMessage(data) { function receiverOnPing(data) { const websocket = this[kWebSocket$1]; - websocket.pong(data, !websocket._isServer, NOOP$1); + websocket.pong(data, !websocket._isServer, NOOP); websocket.emit('ping', data); } @@ -3301,6 +3953,16 @@ function receiverOnPong(data) { this[kWebSocket$1].emit('pong', data); } +/** + * Resume a readable stream + * + * @param {Readable} stream The readable stream + * @private + */ +function resume(stream) { + stream.resume(); +} + /** * The listener of the `net.Socket` `'close'` event. * @@ -3310,10 +3972,13 @@ function socketOnClose() { const websocket = this[kWebSocket$1]; this.removeListener('close', socketOnClose); + this.removeListener('data', socketOnData); this.removeListener('end', socketOnEnd); websocket._readyState = WebSocket.CLOSING; + let chunk; + // // The close frame might not have been received or the `'end'` event emitted, // for example, if the socket was destroyed due to an error. Ensure that the @@ -3321,13 +3986,19 @@ function socketOnClose() { // it. If the readable side of the socket is in flowing mode then there is no // buffered data as everything has been already written and `readable.read()` // will return `null`. If instead, the socket is paused, any possible buffered - // data will be read as a single chunk and emitted synchronously in a single - // `'data'` event. + // data will be read as a single chunk. // - websocket._socket.read(); + if ( + !this._readableState.endEmitted && + !websocket._closeFrameReceived && + !websocket._receiver._writableState.errorEmitted && + (chunk = websocket._socket.read()) !== null + ) { + websocket._receiver.write(chunk); + } + websocket._receiver.end(); - this.removeListener('data', socketOnData); this[kWebSocket$1] = undefined; clearTimeout(websocket._closeTimer); @@ -3377,7 +4048,7 @@ function socketOnError() { const websocket = this[kWebSocket$1]; this.removeListener('error', socketOnError); - this.on('error', NOOP$1); + this.on('error', NOOP); if (websocket) { websocket._readyState = WebSocket.CLOSING; @@ -3385,12 +4056,12 @@ function socketOnError() { } } -const { Duplex } = require$$0__default['default']; +const { Duplex } = require$$0__default$1['default']; /** * Emits the `'close'` event on a stream. * - * @param {stream.Duplex} The stream. + * @param {Duplex} stream The stream. * @private */ function emitClose(stream) { @@ -3428,25 +4099,11 @@ function duplexOnError(err) { * * @param {WebSocket} ws The `WebSocket` to wrap * @param {Object} [options] The options for the `Duplex` constructor - * @return {stream.Duplex} The duplex stream + * @return {Duplex} The duplex stream * @public */ function createWebSocketStream(ws, options) { - let resumeOnReceiverDrain = true; - - function receiverOnDrain() { - if (resumeOnReceiverDrain) ws._socket.resume(); - } - - if (ws.readyState === ws.CONNECTING) { - ws.once('open', function open() { - ws._receiver.removeAllListeners('drain'); - ws._receiver.on('drain', receiverOnDrain); - }); - } else { - ws._receiver.removeAllListeners('drain'); - ws._receiver.on('drain', receiverOnDrain); - } + let terminateOnDestroy = true; const duplex = new Duplex({ ...options, @@ -3456,16 +4113,26 @@ function createWebSocketStream(ws, options) { writableObjectMode: false }); - ws.on('message', function message(msg) { - if (!duplex.push(msg)) { - resumeOnReceiverDrain = false; - ws._socket.pause(); - } + ws.on('message', function message(msg, isBinary) { + const data = + !isBinary && duplex._readableState.objectMode ? msg.toString() : msg; + + if (!duplex.push(data)) ws.pause(); }); ws.once('error', function error(err) { if (duplex.destroyed) return; + // Prevent `ws.terminate()` from being called by `duplex._destroy()`. + // + // - If the `'error'` event is emitted before the `'open'` event, then + // `ws.terminate()` is a noop as no socket is assigned. + // - Otherwise, the error is re-emitted by the listener of the `'error'` + // event of the `Receiver` object. The listener already closes the + // connection by calling `ws.close()`. This allows a close frame to be + // sent to the other peer. If `ws.terminate()` is called right after this, + // then the close frame might not be sent. + terminateOnDestroy = false; duplex.destroy(err); }); @@ -3493,7 +4160,8 @@ function createWebSocketStream(ws, options) { if (!called) callback(err); process.nextTick(emitClose, duplex); }); - ws.terminate(); + + if (terminateOnDestroy) ws.terminate(); }; duplex._final = function (callback) { @@ -3525,10 +4193,7 @@ function createWebSocketStream(ws, options) { }; duplex._read = function () { - if (ws.readyState === ws.OPEN && !resumeOnReceiverDrain) { - resumeOnReceiverDrain = true; - if (!ws._receiver._writableState.needDrain) ws._socket.resume(); - } + if (ws.isPaused) ws.resume(); }; duplex._write = function (chunk, encoding, callback) { @@ -3549,16 +4214,81 @@ function createWebSocketStream(ws, options) { var stream = createWebSocketStream; -const { createHash: createHash$1 } = require$$0__default$1['default']; -const { createServer, STATUS_CODES } = http__default['default']; +const { tokenChars: tokenChars$1 } = validation; + +/** + * Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names. + * + * @param {String} header The field value of the header + * @return {Set} The subprotocol names + * @public + */ +function parse$2(header) { + const protocols = new Set(); + let start = -1; + let end = -1; + let i = 0; + + for (i; i < header.length; i++) { + const code = header.charCodeAt(i); + + if (end === -1 && tokenChars$1[code] === 1) { + if (start === -1) start = i; + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x2c /* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + + const protocol = header.slice(start, end); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + + if (start === -1 || end !== -1) { + throw new SyntaxError('Unexpected end of input'); + } + + const protocol = header.slice(start, i); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + return protocols; +} + +var subprotocol = { parse: parse$2 }; + +const { createHash: createHash$1 } = require$$0__default$2['default']; + + -const { format: format$2, parse: parse$2 } = extension; const { GUID: GUID$1, kWebSocket: kWebSocket$2 } = constants; const keyRegex = /^[+/0-9A-Za-z]{22}==$/; +const RUNNING = 0; +const CLOSING = 1; +const CLOSED = 2; + /** * Class representing a WebSocket server. * @@ -3582,8 +4312,13 @@ class WebSocketServer extends EventEmitter__default['default'] { * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable * permessage-deflate * @param {Number} [options.port] The port where to bind the server - * @param {http.Server} [options.server] A pre-created HTTP/S server to use + * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S + * server to use + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @param {Function} [options.verifyClient] A hook to reject connections + * @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket` + * class to use. It must be the `WebSocket` class or class that extends it * @param {Function} [callback] A listener for the `listening` event */ constructor(options, callback) { @@ -3591,6 +4326,7 @@ class WebSocketServer extends EventEmitter__default['default'] { options = { maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: false, handleProtocols: null, clientTracking: true, @@ -3601,18 +4337,24 @@ class WebSocketServer extends EventEmitter__default['default'] { host: null, path: null, port: null, + WebSocket: websocket, ...options }; - if (options.port == null && !options.server && !options.noServer) { + if ( + (options.port == null && !options.server && !options.noServer) || + (options.port != null && (options.server || options.noServer)) || + (options.server && options.noServer) + ) { throw new TypeError( - 'One of the "port", "server", or "noServer" options must be specified' + 'One and only one of the "port", "server", or "noServer" options ' + + 'must be specified' ); } if (options.port != null) { - this._server = createServer((req, res) => { - const body = STATUS_CODES[426]; + this._server = http__default['default'].createServer((req, res) => { + const body = http__default['default'].STATUS_CODES[426]; res.writeHead(426, { 'Content-Length': body.length, @@ -3643,8 +4385,13 @@ class WebSocketServer extends EventEmitter__default['default'] { } if (options.perMessageDeflate === true) options.perMessageDeflate = {}; - if (options.clientTracking) this.clients = new Set(); + if (options.clientTracking) { + this.clients = new Set(); + this._shouldEmitClose = false; + } + this.options = options; + this._state = RUNNING; } /** @@ -3666,37 +4413,58 @@ class WebSocketServer extends EventEmitter__default['default'] { } /** - * Close the server. + * Stop the server from accepting new connections and emit the `'close'` event + * when all existing connections are closed. * - * @param {Function} [cb] Callback + * @param {Function} [cb] A one-time listener for the `'close'` event * @public */ close(cb) { - if (cb) this.once('close', cb); + if (this._state === CLOSED) { + if (cb) { + this.once('close', () => { + cb(new Error('The server is not running')); + }); + } - // - // Terminate all associated clients. - // - if (this.clients) { - for (const client of this.clients) client.terminate(); + process.nextTick(emitClose$1, this); + return; } - const server = this._server; + if (cb) this.once('close', cb); + + if (this._state === CLOSING) return; + this._state = CLOSING; + + if (this.options.noServer || this.options.server) { + if (this._server) { + this._removeListeners(); + this._removeListeners = this._server = null; + } + + if (this.clients) { + if (!this.clients.size) { + process.nextTick(emitClose$1, this); + } else { + this._shouldEmitClose = true; + } + } else { + process.nextTick(emitClose$1, this); + } + } else { + const server = this._server; - if (server) { this._removeListeners(); this._removeListeners = this._server = null; // - // Close the http server if it was internally created. + // The HTTP/S server was created internally. Close it, and rely on its + // `'close'` event. // - if (this.options.port != null) { - server.close(() => this.emit('close')); - return; - } + server.close(() => { + emitClose$1(this); + }); } - - process.nextTick(emitClose$1, this); } /** @@ -3721,7 +4489,8 @@ class WebSocketServer extends EventEmitter__default['default'] { * Handle a HTTP Upgrade request. * * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @public @@ -3729,25 +4498,58 @@ class WebSocketServer extends EventEmitter__default['default'] { handleUpgrade(req, socket, head, cb) { socket.on('error', socketOnError$1); - const key = - req.headers['sec-websocket-key'] !== undefined - ? req.headers['sec-websocket-key'].trim() - : false; + const key = req.headers['sec-websocket-key']; const version = +req.headers['sec-websocket-version']; + + if (req.method !== 'GET') { + const message = 'Invalid HTTP method'; + abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); + return; + } + + if (req.headers.upgrade.toLowerCase() !== 'websocket') { + const message = 'Invalid Upgrade header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (!key || !keyRegex.test(key)) { + const message = 'Missing or invalid Sec-WebSocket-Key header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (version !== 8 && version !== 13) { + const message = 'Missing or invalid Sec-WebSocket-Version header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (!this.shouldHandle(req)) { + abortHandshake$1(socket, 400); + return; + } + + const secWebSocketProtocol = req.headers['sec-websocket-protocol']; + let protocols = new Set(); + + if (secWebSocketProtocol !== undefined) { + try { + protocols = subprotocol.parse(secWebSocketProtocol); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Protocol header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + } + + const secWebSocketExtensions = req.headers['sec-websocket-extensions']; const extensions = {}; if ( - req.method !== 'GET' || - req.headers.upgrade.toLowerCase() !== 'websocket' || - !key || - !keyRegex.test(key) || - (version !== 8 && version !== 13) || - !this.shouldHandle(req) + this.options.perMessageDeflate && + secWebSocketExtensions !== undefined ) { - return abortHandshake$1(socket, 400); - } - - if (this.options.perMessageDeflate) { const perMessageDeflate = new permessageDeflate( this.options.perMessageDeflate, true, @@ -3755,14 +4557,17 @@ class WebSocketServer extends EventEmitter__default['default'] { ); try { - const offers = parse$2(req.headers['sec-websocket-extensions']); + const offers = extension.parse(secWebSocketExtensions); if (offers[permessageDeflate.extensionName]) { perMessageDeflate.accept(offers[permessageDeflate.extensionName]); extensions[permessageDeflate.extensionName] = perMessageDeflate; } } catch (err) { - return abortHandshake$1(socket, 400); + const message = + 'Invalid or unacceptable Sec-WebSocket-Extensions header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; } } @@ -3783,7 +4588,15 @@ class WebSocketServer extends EventEmitter__default['default'] { return abortHandshake$1(socket, code || 401, message, headers); } - this.completeUpgrade(key, extensions, req, socket, head, cb); + this.completeUpgrade( + extensions, + key, + protocols, + req, + socket, + head, + cb + ); }); return; } @@ -3791,22 +4604,24 @@ class WebSocketServer extends EventEmitter__default['default'] { if (!this.options.verifyClient(info)) return abortHandshake$1(socket, 401); } - this.completeUpgrade(key, extensions, req, socket, head, cb); + this.completeUpgrade(extensions, key, protocols, req, socket, head, cb); } /** * Upgrade the connection to WebSocket. * - * @param {String} key The value of the `Sec-WebSocket-Key` header * @param {Object} extensions The accepted extensions + * @param {String} key The value of the `Sec-WebSocket-Key` header + * @param {Set} protocols The subprotocols * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @throws {Error} If called more than once with the same socket * @private */ - completeUpgrade(key, extensions, req, socket, head, cb) { + completeUpgrade(extensions, key, protocols, req, socket, head, cb) { // // Destroy the socket if the client has already sent a FIN packet. // @@ -3819,6 +4634,8 @@ class WebSocketServer extends EventEmitter__default['default'] { ); } + if (this._state > RUNNING) return abortHandshake$1(socket, 503); + const digest = createHash$1('sha1') .update(key + GUID$1) .digest('base64'); @@ -3830,20 +4647,15 @@ class WebSocketServer extends EventEmitter__default['default'] { `Sec-WebSocket-Accept: ${digest}` ]; - const ws = new websocket(null); - let protocol = req.headers['sec-websocket-protocol']; - - if (protocol) { - protocol = protocol.trim().split(/ *, */); + const ws = new this.options.WebSocket(null); + if (protocols.size) { // // Optionally call external protocol selection handler. // - if (this.options.handleProtocols) { - protocol = this.options.handleProtocols(protocol, req); - } else { - protocol = protocol[0]; - } + const protocol = this.options.handleProtocols + ? this.options.handleProtocols(protocols, req) + : protocols.values().next().value; if (protocol) { headers.push(`Sec-WebSocket-Protocol: ${protocol}`); @@ -3853,7 +4665,7 @@ class WebSocketServer extends EventEmitter__default['default'] { if (extensions[permessageDeflate.extensionName]) { const params = extensions[permessageDeflate.extensionName].params; - const value = format$2({ + const value = extension.format({ [permessageDeflate.extensionName]: [params] }); headers.push(`Sec-WebSocket-Extensions: ${value}`); @@ -3868,11 +4680,20 @@ class WebSocketServer extends EventEmitter__default['default'] { socket.write(headers.concat('\r\n').join('\r\n')); socket.removeListener('error', socketOnError$1); - ws.setSocket(socket, head, this.options.maxPayload); + ws.setSocket(socket, head, { + maxPayload: this.options.maxPayload, + skipUTF8Validation: this.options.skipUTF8Validation + }); if (this.clients) { this.clients.add(ws); - ws.on('close', () => this.clients.delete(ws)); + ws.on('close', () => { + this.clients.delete(ws); + + if (this._shouldEmitClose && !this.clients.size) { + process.nextTick(emitClose$1, this); + } + }); } cb(ws, req); @@ -3908,11 +4729,12 @@ function addListeners(server, map) { * @private */ function emitClose$1(server) { + server._state = CLOSED; server.emit('close'); } /** - * Handle premature socket errors. + * Handle socket errors. * * @private */ @@ -3923,34 +4745,61 @@ function socketOnError$1() { /** * Close the connection when preconditions are not fulfilled. * - * @param {net.Socket} socket The socket of the upgrade request + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} [message] The HTTP response body * @param {Object} [headers] Additional HTTP response headers * @private */ function abortHandshake$1(socket, code, message, headers) { - if (socket.writable) { - message = message || STATUS_CODES[code]; - headers = { - Connection: 'close', - 'Content-Type': 'text/html', - 'Content-Length': Buffer.byteLength(message), - ...headers - }; + // + // The socket is writable unless the user destroyed or ended it before calling + // `server.handleUpgrade()` or in the `verifyClient` function, which is a user + // error. Handling this does not make much sense as the worst that can happen + // is that some of the data written by the user might be discarded due to the + // call to `socket.end()` below, which triggers an `'error'` event that in + // turn causes the socket to be destroyed. + // + message = message || http__default['default'].STATUS_CODES[code]; + headers = { + Connection: 'close', + 'Content-Type': 'text/html', + 'Content-Length': Buffer.byteLength(message), + ...headers + }; - socket.write( - `HTTP/1.1 ${code} ${STATUS_CODES[code]}\r\n` + - Object.keys(headers) - .map((h) => `${h}: ${headers[h]}`) - .join('\r\n') + - '\r\n\r\n' + - message - ); - } + socket.once('finish', socket.destroy); + + socket.end( + `HTTP/1.1 ${code} ${http__default['default'].STATUS_CODES[code]}\r\n` + + Object.keys(headers) + .map((h) => `${h}: ${headers[h]}`) + .join('\r\n') + + '\r\n\r\n' + + message + ); +} + +/** + * Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least + * one listener for it, otherwise call `abortHandshake()`. + * + * @param {WebSocketServer} server The WebSocket server + * @param {http.IncomingMessage} req The request object + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} message The HTTP response body + * @private + */ +function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) { + if (server.listenerCount('wsClientError')) { + const err = new Error(message); + Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); - socket.removeListener('error', socketOnError$1); - socket.destroy(); + server.emit('wsClientError', err, socket, req); + } else { + abortHandshake$1(socket, code, message); + } } websocket.createWebSocketStream = stream; @@ -3958,6 +4807,9 @@ websocket.Server = websocketServer; websocket.Receiver = receiver; websocket.Sender = sender; +websocket.WebSocket = websocket; +websocket.WebSocketServer = websocket.Server; + var ws = websocket; var naclFast = createCommonjsModule(function (module) { @@ -6339,7 +7191,7 @@ nacl.setPRNG = function(fn) { }); } else if (typeof commonjsRequire !== 'undefined') { // Node.js. - crypto = require$$0__default$1['default']; + crypto = require$$0__default$2['default']; if (crypto && crypto.randomBytes) { nacl.setPRNG(function(x, n) { var i, v = crypto.randomBytes(n); @@ -7080,7 +7932,7 @@ function send({ data, connector }) { log, `Connector ${connector.endpoint} → Sending message #${connector.sentCount} ↴` ); - logger.gray(log, data); + logger.cyan(log, data); } connector.connection.websocket.send(data); @@ -7146,11 +7998,25 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec // 💡 encryptedJson data!! if (connector.verbose == 'extra') { logger.magenta(log, `Connector ${connector.endpoint} received bytes ↴`); - logger.gray(log, encryptedData); - logger.magenta( + logger.cyan(log, encryptedData); + logger.green(log, JSON.stringify(encryptedData)); + logger.gray( log, `Connector ${connector.endpoint} decrypting with shared secret ${connector.sharedSecret}...` ); + //logger.cyan(log, JSON.stringify(connector.sharedSecret)); + } + + if (!connector.sharedSecret) { + // we had this problem before -- zurich wifi -- when terminating inactive websocket + // it didn't actually close in time .. we set connector to disconnected and deleted sharedSecret + // but then a stray message json rpc return from hadshake arrived after that and couldn't be decrypted + // because it shouldn't have arrived in the first place after websocket was supposedly closed + // solution: __closed flag on all websockets.. it is set to true at the same time as calling close() + // and then any messages still coming over the wire on such closed websockets are dropped + // we hope websocket is eventually closed though (?) + // see messageCallback in establishAndMaintainConnection, this was fixed there + logger.red(log, `Connector ${connector.endpoint} missing sharedSecret - should not happen...`); } const _decryptedMessage = naclFast.secretbox.open(encryptedData, nonce, connector.sharedSecret); @@ -7162,7 +8028,7 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec const decodedMessage = naclFast.util.encodeUTF8(decryptedMessage); if (connector.verbose) { - logger.write(log, `Received message: ${decodedMessage}`); + logger.yellow(log, `Connector ${connector.endpoint} received message: ${decodedMessage}`); } try { @@ -7208,6 +8074,10 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec throw e; } } else { + if (connector.verbose) { + logger.yellow(log, `Connector ${connector.endpoint} received binary data`); + } + //const binaryData = decryptedMessage; // const sessionId = Buffer.from(binaryData.buffer, binaryData.byteOffset, 64).toString(); // const binaryPayload = Buffer.from(binaryData.buffer, binaryData.byteOffset + 64); @@ -7219,20 +8089,17 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec naclFast.util = naclUtil; +const wsOPEN = 1; + function diffieHellman({ connector, afterFirstStep = () => {} }) { - const { - clientPrivateKey, - clientPublicKey, - clientPublicKeyHex, - protocol, - tag, - endpoint, - verbose - } = connector; + const { clientPrivateKey, clientPublicKey, clientPublicKeyHex, protocol, tag, endpoint, verbose } = + connector; return new Promise((success, reject) => { - connector.remoteObject('Auth') + connector + .remoteObject('Auth') .call('exchangePubkeys', { pubkey: clientPublicKeyHex }) + //.call('exchangePubkeys', { pubkey: clientPublicKeyHex, clientWsId: connector.connection.websocket.__id }) .then(remotePubkeyHex => { const sharedSecret = naclFast.box.before(hexToBuffer(remotePubkeyHex), clientPrivateKey); @@ -7245,33 +8112,50 @@ function diffieHellman({ connector, afterFirstStep = () => {} }) { ); } - connector.remoteObject('Auth') - .call('finalizeHandshake', { protocol }) - .then(res => { - // finalizeHandshake rpc endpoint on server can cleanly retorn {error} as a result - // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) - if (res && res.error) { - console.log(res.error); - // this connection will keep hangling and no reconnect tries will be made - // since we keep websocket open just that nothing is happening - - // when we enable the protocol on the endpoint we have to restart the process - // frontend connector will get disconnected at this point, websocket will close - // and from then on it tries reconnecting again so when ws first connects - // and protocol is present , it will be a success - - // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging - } else { - success(); - - const _tag = tag ? ` (${tag})` : ''; - logger.cyan( - connector.log, - `${endpoint}${_tag} ✓ Connection [ ${protocol || '"no-name"'} ] ready` - ); - } - }) - .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + // if connection has closed at this point we don't try to send into closed + // connection, it would still work but error would be logged + if (connector.connection.websocket.readyState == wsOPEN) { + connector + .remoteObject('Auth') + .call('finalizeHandshake', { protocol }) + .then(res => { + // finalizeHandshake rpc endpoint on server can cleanly return {error} as a result + // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) + if (res && res.error) { + console.log(res.error); + // this connection will keep hangling and no reconnect tries will be made + // since we keep websocket open just that nothing is happening + + // when we enable the protocol on the endpoint we have to restart the process + // frontend connector will get disconnected at this point, websocket will close + // and from then on it tries reconnecting again so when ws first connects + // and protocol is present , it will be a success + + // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging + } else { + success(); + + const _tag = tag ? ` (${tag})` : ''; + logger.cyan( + connector.log, + `✓✓✓ ${endpoint}${_tag} ✓ Connection #${connector.connection.websocket.__id} [ ${ + protocol || '"no-name"' + } ] ready` + ); + } + }) + .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + } else { + const _tag = tag ? ` (${tag})` : ''; + logger.yellow( + connector.log, + `${endpoint}${_tag} ✖ Connection [ ${ + protocol || '"no-name"' + } ] closed just before finalizeHandshake step` + ); + // don't reject here -- because it will show some wring log message in connector + // on:ready error "will not try to reconnect" .. which is not the case here + } }) .catch(reject); }); @@ -8765,7 +9649,7 @@ const DECOMMISSION_INACTIVITY = 60000; // 1min //const DECOMMISSION_INACTIVITY = 120000; // 2min //const DECOMMISSION_INACTIVITY = 10000; // 2min -const wsOPEN = 1; +const wsOPEN$1 = 1; class Connector extends Eev { constructor({ @@ -8921,7 +9805,7 @@ class Connector extends Eev { this.successfulConnectsCount += 1; if (this.verbose) { - logger.green(this.log, `✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`); + logger.white(this.log, `✓ Connector ${this.endpoint} connected (${this.successfulConnectsCount} total reconnects)`); } const websocketId = this.connection.websocket.__id; @@ -8953,7 +9837,7 @@ class Connector extends Eev { // but sometimes we also get an open websocket after rpc timeout (not sure but this code handles it anyway, should be no problem, only better for all cases) if ( this.connection.websocket.__id == websocketId && - this.connection.websocket.readyState == wsOPEN + this.connection.websocket.readyState == wsOPEN$1 ) { //⚠️ we only show if it seems still relevant, special case // previously we had this first log output above this if statement @@ -9137,7 +10021,7 @@ function determineEndpoint({ endpoint, host, port }) { const browser$1 = typeof window !== 'undefined'; const wsCONNECTING = 0; -const wsOPEN$1 = 1; +const wsOPEN$2 = 1; //const wsCLOSING = 2; //const wsCLOSED = 3; @@ -9151,6 +10035,22 @@ const CONN_IDLE_TICKS = 3; // how long to wait for a new websocket to connect... after this we cancel it const WAIT_FOR_NEW_CONN_TICKS = 5; // 5000 ms ( = (5) * CONN_CHECK_INTERVAL ) +function addListener(name, callback, ws) { + if (browser$1) { + ws.addEventListener(name, callback); + } else { + ws.on(name, callback); + } +} + +function removeListener(name, callback, ws) { + if (browser$1) { + ws.removeEventListener(name, callback); + } else { + ws.off(name, callback); + } +} + //todo: remove 'dummy' argument once legacyLib with old MCS is history function establishAndMaintainConnection( { @@ -9190,7 +10090,8 @@ function establishAndMaintainConnection( connector.connection = { terminate() { this.websocket._removeAllCallbacks(); - this.websocket.close(); + this.websocket.__closed = true; + this.websocket.close(); // might take some time to actually close, we can get stray messages through that websocket //connector.connectStatus(undefined); connector.connectStatus(false); reconnect(); @@ -9224,14 +10125,14 @@ function checkConnection({ connector, reconnect, log }) { // decommissioned logger.yellow( log, - `${connector.endpoint} Connection decommisioned, closing websocket ${conn.websocket.__id}, will not retry again ` + `${connector.endpoint} Connection decommisioned, closing websocket #${conn.websocket.__id}, will not retry again ` ); decommission(connector); } else { // idle connection connector.emit('inactive_connection'); - logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection`); + logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection #${conn.websocket.__id}`); } conn.terminate(); @@ -9273,6 +10174,8 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb return; } + const wsId = Math.round(10 ** 5 * Math.random()).toString(); + //logger.write(log, `${endpoint} CONN_TICK`); //logger.write(log, `${endpoint} wsReadyState ${conn.currentlyTryingWS?.readyState}`); @@ -9288,9 +10191,10 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); } else if (verbose || browser$1) { - logger.write(log, `${endpoint} Created new websocket`); + logger.write(log, `${endpoint} Created new websocket #${wsId}`); } // so in case when device is online but websocket server is not running we usually @@ -9301,7 +10205,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb // (see above)... and we try with a new websocket every 4800ms again instead on every tick (800ms) const ws = new WebSocket(endpoint); - ws.__id = Math.random(); + ws.__id = wsId; conn.currentlyTryingWS = ws; conn.currentlyTryingWS._waitForConnectCounter = 0; @@ -9321,7 +10225,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } if (verbose || browser$1) { - logger.write(log, `${endpoint} Websocket open`); + logger.write(log, `${endpoint} Websocket #${wsId} open`); } conn.currentlyTryingWS = null; @@ -9334,14 +10238,14 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb }; ws._removeAllCallbacks = () => { - ws.removeEventListener('open', openCallback); + // logger.red( + // log, + // `${connector.endpoint} removing 1 callback (open) on ws #${ws.__id} [ ${connector.protocol} ]` + // ); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('open', openCallback); - } else { - ws.on('open', openCallback); - } + addListener('open', openCallback, ws); } function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, verbose }) { @@ -9358,7 +10262,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; const closeCallback = () => { - logger.write(log, `${connector.endpoint} ✖ Connection closed`); + //❗❗❗❗ -- can get stray messages even here!! after close callback ws implementation lets a few (one) messages through!! + // this only happened on LAN ... + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+167ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 ✖ Connection #28485 [ dmt ] closed' + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+01ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 Created new websocket #17068' + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+338ms) ∞ 1.0.0.1 consecutiveUnresolvedTimeout after 2x unresolved promise + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+43ms) ∞ lanServerConn — "ws://192.168.0.10:7780 connection #28485 [ dmt ] received msg '��\x19X���9�߈�V^L�#�b��)\x02�\r��n\x06^?U�v�\x00�ͻ>����k~�A(^�\t�İP�=���X*���'" + // maybe not needed anymore after listeners issue was fixed ..... + ws.__closed = true; + + logger.blue(log, `${connector.endpoint} ✖ Connection #${ws.__id} [ ${connector.protocol} ] closed`); if (connector.decommissioned) { connector.connectStatus(false); @@ -9371,6 +10284,7 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v // flip side is that there is such small delay between when we stop some process and when red x appears... but it's quite ok! // we do however disable all commands immediately ... so: show red X when connect status is FALSE excusively and disable all gui actions when it's NOT TRUE (false or undefined) connector.connectStatus(undefined); + reconnect(); //setTimeout(reconnect, MAX_RECONNECT_DELAY_AFTER_WS_CLOSE * Math.random()); // turns out we don't really need to do these delays, works fine without }; @@ -9384,11 +10298,26 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v const msg = browser$1 ? _msg.data : _msg; + if (ws.__closed) { + // if (msg != 'pong') { + // logger.red( + // log, + // `${connector.endpoint} Already closed connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + // } + return; + } + if (msg == 'pong') { connector.emit('pong'); return; } + // logger.red( + // log, + // `${connector.endpoint} connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + let jsonData; try { @@ -9404,22 +10333,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; ws._removeAllCallbacks = () => { - ws.removeEventListener('error', errorCallback); - ws.removeEventListener('close', closeCallback); - ws.removeEventListener('message', messageCallback); - - ws.removeEventListener('open', openCallback); + // logger.red(log, `${connector.endpoint} removing 4 callbacks on ws #${ws.__id} [ ${connector.protocol} ]`); + removeListener('error', errorCallback, ws); + removeListener('close', closeCallback, ws); + removeListener('message', messageCallback, ws); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('error', errorCallback); - ws.addEventListener('close', closeCallback); - ws.addEventListener('message', messageCallback); - } else { - ws.on('error', errorCallback); - ws.on('close', closeCallback); - ws.on('message', messageCallback); - } + addListener('error', errorCallback, ws); + addListener('close', closeCallback, ws); + addListener('message', messageCallback, ws); } function decommission(connector) { @@ -9427,21 +10350,23 @@ function decommission(connector) { if (conn.currentlyTryingWS) { conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); conn.currentlyTryingWS = null; } - if (conn.ws) { - conn.ws._removeAllCallbacks(); - conn.ws.close(); - conn.ws = null; + if (conn.websocket) { + conn.websocket._removeAllCallbacks(); + conn.websocket.__closed = true; + conn.websocket.close(); + conn.websocket = null; } connector.connectStatus(false); } function socketConnected(conn) { - return conn.websocket && conn.websocket.readyState == wsOPEN$1; + return conn.websocket && conn.websocket.readyState == wsOPEN$2 && !conn.websocket.__closed; // when terminating connection, might be useful -- check } function connectionIdle(conn) { diff --git a/core/node/connectome/dist/node/index.mjs b/core/node/connectome/dist/node/index.mjs index eb3ab69cf..17e328413 100644 --- a/core/node/connectome/dist/node/index.mjs +++ b/core/node/connectome/dist/node/index.mjs @@ -3,13 +3,14 @@ import https from 'https'; import http from 'http'; import net from 'net'; import tls from 'tls'; -import require$$0$1 from 'crypto'; -import require$$1 from 'url'; +import require$$0$2 from 'crypto'; +import require$$0$1 from 'stream'; +import require$$2 from 'url'; import zlib from 'zlib'; import fs from 'fs'; import path from 'path'; import os from 'os'; -import require$$0 from 'stream'; +import require$$0 from 'buffer'; var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; @@ -29,10 +30,12 @@ function commonjsRequire () { var constants = { BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], + EMPTY_BUFFER: Buffer.alloc(0), GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + kForOnEventAttribute: Symbol('kIsForOnEventAttribute'), + kListener: Symbol('kListener'), kStatusCode: Symbol('status-code'), kWebSocket: Symbol('websocket'), - EMPTY_BUFFER: Buffer.alloc(0), NOOP: () => {} }; @@ -245,6 +248,8 @@ var bufferUtil = createCommonjsModule(function (module) { const { EMPTY_BUFFER } = constants; +const FastBuffer = Buffer[Symbol.species]; + /** * Merges an array of buffers into a new buffer. * @@ -266,7 +271,9 @@ function concat(list, totalLength) { offset += buf.length; } - if (offset < totalLength) return target.slice(0, offset); + if (offset < totalLength) { + return new FastBuffer(target.buffer, target.byteOffset, offset); + } return target; } @@ -295,9 +302,7 @@ function _mask(source, mask, output, offset, length) { * @public */ function _unmask(buffer, mask) { - // Required until https://github.com/nodejs/node/issues/9006 is resolved. - const length = buffer.length; - for (let i = 0; i < length; i++) { + for (let i = 0; i < buffer.length; i++) { buffer[i] ^= mask[i & 3]; } } @@ -310,11 +315,11 @@ function _unmask(buffer, mask) { * @public */ function toArrayBuffer(buf) { - if (buf.byteLength === buf.buffer.byteLength) { + if (buf.length === buf.buffer.byteLength) { return buf.buffer; } - return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length); } /** @@ -333,9 +338,9 @@ function toBuffer(data) { let buf; if (data instanceof ArrayBuffer) { - buf = Buffer.from(data); + buf = new FastBuffer(data); } else if (ArrayBuffer.isView(data)) { - buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); + buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength); } else { buf = Buffer.from(data); toBuffer.readOnly = false; @@ -344,31 +349,31 @@ function toBuffer(data) { return buf; } -try { - const bufferUtil = bufferutil; - const bu = bufferUtil.BufferUtil || bufferUtil; +module.exports = { + concat, + mask: _mask, + toArrayBuffer, + toBuffer, + unmask: _unmask +}; + +/* istanbul ignore else */ +if (!process.env.WS_NO_BUFFER_UTIL) { + try { + const bufferUtil = bufferutil; - module.exports = { - concat, - mask(source, mask, output, offset, length) { + module.exports.mask = function (source, mask, output, offset, length) { if (length < 48) _mask(source, mask, output, offset, length); - else bu.mask(source, mask, output, offset, length); - }, - toArrayBuffer, - toBuffer, - unmask(buffer, mask) { + else bufferUtil.mask(source, mask, output, offset, length); + }; + + module.exports.unmask = function (buffer, mask) { if (buffer.length < 32) _unmask(buffer, mask); - else bu.unmask(buffer, mask); - } - }; -} catch (e) /* istanbul ignore next */ { - module.exports = { - concat, - mask: _mask, - toArrayBuffer, - toBuffer, - unmask: _unmask - }; + else bufferUtil.unmask(buffer, mask); + }; + } catch (e) { + // Continue regardless of the error. + } } }); @@ -426,8 +431,9 @@ class Limiter { var limiter = Limiter; -const { kStatusCode, NOOP } = constants; +const { kStatusCode } = constants; +const FastBuffer = Buffer[Symbol.species]; const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); const kPerMessageDeflate = Symbol('permessage-deflate'); const kTotalLength = Symbol('total-length'); @@ -452,22 +458,22 @@ class PerMessageDeflate { * Creates a PerMessageDeflate instance. * * @param {Object} [options] Configuration options - * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept - * disabling of server context takeover + * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support + * for, or request, a custom client window size * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ * acknowledge disabling of client context takeover + * @param {Number} [options.concurrencyLimit=10] The number of concurrent + * calls to zlib * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the * use of a custom server window size - * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support - * for, or request, a custom client window size + * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept + * disabling of server context takeover + * @param {Number} [options.threshold=1024] Size (in bytes) below which + * messages should not be compressed if context takeover is disabled * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on * deflate * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on * inflate - * @param {Number} [options.threshold=1024] Size (in bytes) below which - * messages should not be compressed - * @param {Number} [options.concurrencyLimit=10] The number of concurrent - * calls to zlib * @param {Boolean} [isServer=false] Create the instance in either server or * client mode * @param {Number} [maxPayload=0] The maximum allowed message length @@ -735,7 +741,7 @@ class PerMessageDeflate { /** * Compress data. Concurrency limited. * - * @param {Buffer} data Data to compress + * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @public @@ -817,7 +823,7 @@ class PerMessageDeflate { /** * Compress data. * - * @param {Buffer} data Data to compress + * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @private @@ -840,13 +846,6 @@ class PerMessageDeflate { this._deflate[kTotalLength] = 0; this._deflate[kBuffers] = []; - // - // An `'error'` event is emitted, only on Node.js < 10.0.0, if the - // `zlib.DeflateRaw` instance is closed while data is being processed. - // This can happen if `PerMessageDeflate#cleanup()` is called at the wrong - // time due to an abnormal WebSocket closure. - // - this._deflate.on('error', NOOP); this._deflate.on('data', deflateOnData); } @@ -866,7 +865,9 @@ class PerMessageDeflate { this._deflate[kTotalLength] ); - if (fin) data = data.slice(0, data.length - 4); + if (fin) { + data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4); + } // // Ensure that the callback will not be called again in @@ -917,6 +918,7 @@ function inflateOnData(chunk) { } this[kError] = new RangeError('Max payload size exceeded'); + this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; this[kError][kStatusCode] = 1009; this.removeListener('data', inflateOnData); this.reset(); @@ -1010,6 +1012,31 @@ try { var validation = createCommonjsModule(function (module) { +const { isUtf8 } = require$$0; + +// +// Allowed token characters: +// +// '!', '#', '$', '%', '&', ''', '*', '+', '-', +// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' +// +// tokenChars[32] === 0 // ' ' +// tokenChars[33] === 1 // '!' +// tokenChars[34] === 0 // '"' +// ... +// +// prettier-ignore +const tokenChars = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 +]; + /** * Checks if a status code is allowed in a close frame. * @@ -1042,7 +1069,7 @@ function _isValidUTF8(buf) { let i = 0; while (i < len) { - if (buf[i] < 0x80) { + if ((buf[i] & 0x80) === 0) { // 0xxxxxxx i++; } else if ((buf[i] & 0xe0) === 0xc0) { @@ -1053,9 +1080,9 @@ function _isValidUTF8(buf) { (buf[i] & 0xfe) === 0xc0 // Overlong ) { return false; - } else { - i += 2; } + + i += 2; } else if ((buf[i] & 0xf0) === 0xe0) { // 1110xxxx 10xxxxxx 10xxxxxx if ( @@ -1066,9 +1093,9 @@ function _isValidUTF8(buf) { (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) ) { return false; - } else { - i += 3; } + + i += 3; } else if ((buf[i] & 0xf8) === 0xf0) { // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx if ( @@ -1081,9 +1108,9 @@ function _isValidUTF8(buf) { buf[i] > 0xf4 // > U+10FFFF ) { return false; - } else { - i += 4; } + + i += 4; } else { return false; } @@ -1092,29 +1119,30 @@ function _isValidUTF8(buf) { return true; } -try { - let isValidUTF8 = utf8Validate; - - /* istanbul ignore if */ - if (typeof isValidUTF8 === 'object') { - isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0 - } +module.exports = { + isValidStatusCode, + isValidUTF8: _isValidUTF8, + tokenChars +}; - module.exports = { - isValidStatusCode, - isValidUTF8(buf) { - return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); - } - }; -} catch (e) /* istanbul ignore next */ { - module.exports = { - isValidStatusCode, - isValidUTF8: _isValidUTF8 +if (isUtf8) { + module.exports.isValidUTF8 = function (buf) { + return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf); }; +} /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) { + try { + const isValidUTF8 = utf8Validate; + + module.exports.isValidUTF8 = function (buf) { + return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf); + }; + } catch (e) { + // Continue regardless of the error. + } } }); -const { Writable } = require$$0; +const { Writable } = require$$0$1; const { @@ -1126,6 +1154,7 @@ const { const { concat, toArrayBuffer, unmask: unmask$1 } = bufferUtil; const { isValidStatusCode, isValidUTF8: isValidUTF8$1 } = validation; +const FastBuffer$1 = Buffer[Symbol.species]; const GET_INFO = 0; const GET_PAYLOAD_LENGTH_16 = 1; const GET_PAYLOAD_LENGTH_64 = 2; @@ -1136,26 +1165,31 @@ const INFLATING = 5; /** * HyBi Receiver implementation. * - * @extends stream.Writable + * @extends Writable */ class Receiver extends Writable { /** * Creates a Receiver instance. * - * @param {String} [binaryType=nodebuffer] The type for binary data - * @param {Object} [extensions] An object containing the negotiated extensions - * @param {Boolean} [isServer=false] Specifies whether to operate in client or - * server mode - * @param {Number} [maxPayload=0] The maximum allowed message length + * @param {Object} [options] Options object + * @param {String} [options.binaryType=nodebuffer] The type for binary data + * @param {Object} [options.extensions] An object containing the negotiated + * extensions + * @param {Boolean} [options.isServer=false] Specifies whether to operate in + * client or server mode + * @param {Number} [options.maxPayload=0] The maximum allowed message length + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages */ - constructor(binaryType, extensions, isServer, maxPayload) { + constructor(options = {}) { super(); - this._binaryType = binaryType || BINARY_TYPES[0]; + this._binaryType = options.binaryType || BINARY_TYPES[0]; + this._extensions = options.extensions || {}; + this._isServer = !!options.isServer; + this._maxPayload = options.maxPayload | 0; + this._skipUTF8Validation = !!options.skipUTF8Validation; this[kWebSocket] = undefined; - this._extensions = extensions || {}; - this._isServer = !!isServer; - this._maxPayload = maxPayload | 0; this._bufferedBytes = 0; this._buffers = []; @@ -1206,8 +1240,13 @@ class Receiver extends Writable { if (n < this._buffers[0].length) { const buf = this._buffers[0]; - this._buffers[0] = buf.slice(n); - return buf.slice(0, n); + this._buffers[0] = new FastBuffer$1( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); + + return new FastBuffer$1(buf.buffer, buf.byteOffset, n); } const dst = Buffer.allocUnsafe(n); @@ -1220,7 +1259,11 @@ class Receiver extends Writable { dst.set(this._buffers.shift(), offset); } else { dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); - this._buffers[0] = buf.slice(n); + this._buffers[0] = new FastBuffer$1( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); } n -= buf.length; @@ -1282,14 +1325,26 @@ class Receiver extends Writable { if ((buf[0] & 0x30) !== 0x00) { this._loop = false; - return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002); + return error( + RangeError, + 'RSV2 and RSV3 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_2_3' + ); } const compressed = (buf[0] & 0x40) === 0x40; if (compressed && !this._extensions[permessageDeflate.extensionName]) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } this._fin = (buf[0] & 0x80) === 0x80; @@ -1299,45 +1354,85 @@ class Receiver extends Writable { if (this._opcode === 0x00) { if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } if (!this._fragmented) { this._loop = false; - return error(RangeError, 'invalid opcode 0', true, 1002); + return error( + RangeError, + 'invalid opcode 0', + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._opcode = this._fragmented; } else if (this._opcode === 0x01 || this._opcode === 0x02) { if (this._fragmented) { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._compressed = compressed; } else if (this._opcode > 0x07 && this._opcode < 0x0b) { if (!this._fin) { this._loop = false; - return error(RangeError, 'FIN must be set', true, 1002); + return error( + RangeError, + 'FIN must be set', + true, + 1002, + 'WS_ERR_EXPECTED_FIN' + ); } if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } - if (this._payloadLength > 0x7d) { + if ( + this._payloadLength > 0x7d || + (this._opcode === 0x08 && this._payloadLength === 1) + ) { this._loop = false; return error( RangeError, `invalid payload length ${this._payloadLength}`, true, - 1002 + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' ); } } else { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } if (!this._fin && !this._fragmented) this._fragmented = this._opcode; @@ -1346,11 +1441,23 @@ class Receiver extends Writable { if (this._isServer) { if (!this._masked) { this._loop = false; - return error(RangeError, 'MASK must be set', true, 1002); + return error( + RangeError, + 'MASK must be set', + true, + 1002, + 'WS_ERR_EXPECTED_MASK' + ); } } else if (this._masked) { this._loop = false; - return error(RangeError, 'MASK must be clear', true, 1002); + return error( + RangeError, + 'MASK must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_MASK' + ); } if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; @@ -1399,7 +1506,8 @@ class Receiver extends Writable { RangeError, 'Unsupported WebSocket frame: payload length > 2^53 - 1', false, - 1009 + 1009, + 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' ); } @@ -1418,7 +1526,13 @@ class Receiver extends Writable { this._totalPayloadLength += this._payloadLength; if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { this._loop = false; - return error(RangeError, 'Max payload size exceeded', false, 1009); + return error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); } } @@ -1458,7 +1572,13 @@ class Receiver extends Writable { } data = this.consume(this._payloadLength); - if (this._masked) unmask$1(data, this._mask); + + if ( + this._masked && + (this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0 + ) { + unmask$1(data, this._mask); + } } if (this._opcode > 0x07) return this.controlMessage(data); @@ -1471,7 +1591,7 @@ class Receiver extends Writable { if (data.length) { // - // This message is not compressed so its lenght is the sum of the payload + // This message is not compressed so its length is the sum of the payload // length of all fragments. // this._messageLength = this._totalPayloadLength; @@ -1498,7 +1618,13 @@ class Receiver extends Writable { this._messageLength += buf.length; if (this._messageLength > this._maxPayload && this._maxPayload > 0) { return cb( - error(RangeError, 'Max payload size exceeded', false, 1009) + error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ) ); } @@ -1539,16 +1665,22 @@ class Receiver extends Writable { data = fragments; } - this.emit('message', data); + this.emit('message', data, true); } else { const buf = concat(fragments, messageLength); - if (!isValidUTF8$1(buf)) { + if (!this._skipUTF8Validation && !isValidUTF8$1(buf)) { this._loop = false; - return error(Error, 'invalid UTF-8 sequence', true, 1007); + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } - this.emit('message', buf.toString()); + this.emit('message', buf, false); } } @@ -1567,24 +1699,38 @@ class Receiver extends Writable { this._loop = false; if (data.length === 0) { - this.emit('conclude', 1005, ''); + this.emit('conclude', 1005, EMPTY_BUFFER); this.end(); - } else if (data.length === 1) { - return error(RangeError, 'invalid payload length 1', true, 1002); } else { const code = data.readUInt16BE(0); if (!isValidStatusCode(code)) { - return error(RangeError, `invalid status code ${code}`, true, 1002); + return error( + RangeError, + `invalid status code ${code}`, + true, + 1002, + 'WS_ERR_INVALID_CLOSE_CODE' + ); } - const buf = data.slice(2); + const buf = new FastBuffer$1( + data.buffer, + data.byteOffset + 2, + data.length - 2 + ); - if (!isValidUTF8$1(buf)) { - return error(Error, 'invalid UTF-8 sequence', true, 1007); + if (!this._skipUTF8Validation && !isValidUTF8$1(buf)) { + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } - this.emit('conclude', code, buf.toString()); + this.emit('conclude', code, buf); this.end(); } } else if (this._opcode === 0x09) { @@ -1602,32 +1748,35 @@ var receiver = Receiver; /** * Builds an error object. * - * @param {(Error|RangeError)} ErrorCtor The error constructor + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor * @param {String} message The error message * @param {Boolean} prefix Specifies whether or not to add a default prefix to * `message` * @param {Number} statusCode The status code + * @param {String} errorCode The exposed error code * @return {(Error|RangeError)} The error * @private */ -function error(ErrorCtor, message, prefix, statusCode) { +function error(ErrorCtor, message, prefix, statusCode, errorCode) { const err = new ErrorCtor( prefix ? `Invalid WebSocket frame: ${message}` : message ); Error.captureStackTrace(err, error); + err.code = errorCode; err[kStatusCode$1] = statusCode; return err; } -const { randomFillSync } = require$$0$1; +const { randomFillSync } = require$$0$2; const { EMPTY_BUFFER: EMPTY_BUFFER$1 } = constants; const { isValidStatusCode: isValidStatusCode$1 } = validation; const { mask: applyMask, toBuffer } = bufferUtil; -const mask$1 = Buffer.alloc(4); +const kByteLength = Symbol('kByteLength'); +const maskBuffer = Buffer.alloc(4); /** * HyBi Sender implementation. @@ -1636,11 +1785,19 @@ class Sender { /** * Creates a Sender instance. * - * @param {net.Socket} socket The connection socket + * @param {(net.Socket|tls.Socket)} socket The connection socket * @param {Object} [extensions] An object containing the negotiated extensions + * @param {Function} [generateMask] The function used to generate the masking + * key */ - constructor(socket, extensions) { + constructor(socket, extensions, generateMask) { this._extensions = extensions || {}; + + if (generateMask) { + this._generateMask = generateMask; + this._maskBuffer = Buffer.alloc(4); + } + this._socket = socket; this._firstFragment = true; @@ -1654,34 +1811,71 @@ class Sender { /** * Frames a piece of data according to the HyBi WebSocket protocol. * - * @param {Buffer} data The data to frame + * @param {(Buffer|String)} data The data to frame * @param {Object} options Options object - * @param {Number} options.opcode The opcode - * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be - * modified * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit - * @return {Buffer[]} The framed data as a list of `Buffer` instances + * @return {(Buffer|String)[]} The framed data * @public */ static frame(data, options) { - const merge = options.mask && options.readOnly; - let offset = options.mask ? 6 : 2; - let payloadLength = data.length; + let mask; + let merge = false; + let offset = 2; + let skipMasking = false; + + if (options.mask) { + mask = options.maskBuffer || maskBuffer; + + if (options.generateMask) { + options.generateMask(mask); + } else { + randomFillSync(mask, 0, 4); + } + + skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; + offset = 6; + } - if (data.length >= 65536) { + let dataLength; + + if (typeof data === 'string') { + if ( + (!options.mask || skipMasking) && + options[kByteLength] !== undefined + ) { + dataLength = options[kByteLength]; + } else { + data = Buffer.from(data); + dataLength = data.length; + } + } else { + dataLength = data.length; + merge = options.mask && options.readOnly && !skipMasking; + } + + let payloadLength = dataLength; + + if (dataLength >= 65536) { offset += 8; payloadLength = 127; - } else if (data.length > 125) { + } else if (dataLength > 125) { offset += 2; payloadLength = 126; } - const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); + const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset); target[0] = options.fin ? options.opcode | 0x80 : options.opcode; if (options.rsv1) target[0] |= 0x40; @@ -1689,28 +1883,28 @@ class Sender { target[1] = payloadLength; if (payloadLength === 126) { - target.writeUInt16BE(data.length, 2); + target.writeUInt16BE(dataLength, 2); } else if (payloadLength === 127) { - target.writeUInt32BE(0, 2); - target.writeUInt32BE(data.length, 6); + target[2] = target[3] = 0; + target.writeUIntBE(dataLength, 4, 6); } if (!options.mask) return [target, data]; - randomFillSync(mask$1, 0, 4); - target[1] |= 0x80; - target[offset - 4] = mask$1[0]; - target[offset - 3] = mask$1[1]; - target[offset - 2] = mask$1[2]; - target[offset - 1] = mask$1[3]; + target[offset - 4] = mask[0]; + target[offset - 3] = mask[1]; + target[offset - 2] = mask[2]; + target[offset - 1] = mask[3]; + + if (skipMasking) return [target, data]; if (merge) { - applyMask(data, mask$1, target, offset, data.length); + applyMask(data, mask, target, offset, dataLength); return [target]; } - applyMask(data, mask$1, data, 0, data.length); + applyMask(data, mask, data, 0, dataLength); return [target, data]; } @@ -1718,7 +1912,7 @@ class Sender { * Sends a close message to the other peer. * * @param {Number} [code] The status code component of the body - * @param {String} [data] The message component of the body + * @param {(String|Buffer)} [data] The message component of the body * @param {Boolean} [mask=false] Specifies whether or not to mask the message * @param {Function} [cb] Callback * @public @@ -1730,7 +1924,7 @@ class Sender { buf = EMPTY_BUFFER$1; } else if (typeof code !== 'number' || !isValidStatusCode$1(code)) { throw new TypeError('First argument must be a valid error code number'); - } else if (data === undefined || data === '') { + } else if (data === undefined || !data.length) { buf = Buffer.allocUnsafe(2); buf.writeUInt16BE(code, 0); } else { @@ -1742,37 +1936,32 @@ class Sender { buf = Buffer.allocUnsafe(2 + length); buf.writeUInt16BE(code, 0); - buf.write(data, 2); + + if (typeof data === 'string') { + buf.write(data, 2); + } else { + buf.set(data, 2); + } } + const options = { + [kByteLength]: buf.length, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x08, + readOnly: false, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doClose, buf, mask, cb]); + this.enqueue([this.dispatch, buf, false, options, cb]); } else { - this.doClose(buf, mask, cb); + this.sendFrame(Sender.frame(buf, options), cb); } } - /** - * Frames and sends a close message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Function} [cb] Callback - * @private - */ - doClose(data, mask, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x08, - mask, - readOnly: false - }), - cb - ); - } - /** * Sends a ping message to the other peer. * @@ -1782,41 +1971,40 @@ class Sender { * @public */ ping(data, mask, cb) { - const buf = toBuffer(data); + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } - if (buf.length > 125) { + if (byteLength > 125) { throw new RangeError('The data size must not be greater than 125 bytes'); } + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x09, + readOnly, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]); + this.enqueue([this.dispatch, data, false, options, cb]); } else { - this.doPing(buf, mask, toBuffer.readOnly, cb); + this.sendFrame(Sender.frame(data, options), cb); } } - /** - * Frames and sends a ping message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified - * @param {Function} [cb] Callback - * @private - */ - doPing(data, mask, readOnly, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x09, - mask, - readOnly - }), - cb - ); - } - /** * Sends a pong message to the other peer. * @@ -1826,50 +2014,49 @@ class Sender { * @public */ pong(data, mask, cb) { - const buf = toBuffer(data); + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } - if (buf.length > 125) { + if (byteLength > 125) { throw new RangeError('The data size must not be greater than 125 bytes'); } + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x0a, + readOnly, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]); + this.enqueue([this.dispatch, data, false, options, cb]); } else { - this.doPong(buf, mask, toBuffer.readOnly, cb); + this.sendFrame(Sender.frame(data, options), cb); } } - /** - * Frames and sends a pong message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified - * @param {Function} [cb] Callback - * @private - */ - doPong(data, mask, readOnly, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x0a, - mask, - readOnly - }), - cb - ); - } - /** * Sends a data message to the other peer. * * @param {*} data The message to send * @param {Object} options Options object - * @param {Boolean} [options.compress=false] Specifies whether or not to - * compress `data` * @param {Boolean} [options.binary=false] Specifies whether `data` is binary * or text + * @param {Boolean} [options.compress=false] Specifies whether or not to + * compress `data` * @param {Boolean} [options.fin=false] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask=false] Specifies whether or not to mask @@ -1878,15 +2065,34 @@ class Sender { * @public */ send(data, options, cb) { - const buf = toBuffer(data); const perMessageDeflate = this._extensions[permessageDeflate.extensionName]; let opcode = options.binary ? 2 : 1; let rsv1 = options.compress; + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + if (this._firstFragment) { this._firstFragment = false; - if (rsv1 && perMessageDeflate) { - rsv1 = buf.length >= perMessageDeflate._threshold; + if ( + rsv1 && + perMessageDeflate && + perMessageDeflate.params[ + perMessageDeflate._isServer + ? 'server_no_context_takeover' + : 'client_no_context_takeover' + ] + ) { + rsv1 = byteLength >= perMessageDeflate._threshold; } this._compress = rsv1; } else { @@ -1898,26 +2104,32 @@ class Sender { if (perMessageDeflate) { const opts = { + [kByteLength]: byteLength, fin: options.fin, - rsv1, - opcode, + generateMask: this._generateMask, mask: options.mask, - readOnly: toBuffer.readOnly + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1 }; if (this._deflating) { - this.enqueue([this.dispatch, buf, this._compress, opts, cb]); + this.enqueue([this.dispatch, data, this._compress, opts, cb]); } else { - this.dispatch(buf, this._compress, opts, cb); + this.dispatch(data, this._compress, opts, cb); } } else { this.sendFrame( - Sender.frame(buf, { + Sender.frame(data, { + [kByteLength]: byteLength, fin: options.fin, - rsv1: false, - opcode, + generateMask: this._generateMask, mask: options.mask, - readOnly: toBuffer.readOnly + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1: false }), cb ); @@ -1925,19 +2137,23 @@ class Sender { } /** - * Dispatches a data message. + * Dispatches a message. * - * @param {Buffer} data The message to send + * @param {(Buffer|String)} data The message to send * @param {Boolean} [compress=false] Specifies whether or not to compress * `data` * @param {Object} options Options object - * @param {Number} options.opcode The opcode - * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be - * modified * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit * @param {Function} [cb] Callback @@ -1951,7 +2167,7 @@ class Sender { const perMessageDeflate = this._extensions[permessageDeflate.extensionName]; - this._bufferedBytes += data.length; + this._bufferedBytes += options[kByteLength]; this._deflating = true; perMessageDeflate.compress(data, options.fin, (_, buf) => { if (this._socket.destroyed) { @@ -1962,7 +2178,8 @@ class Sender { if (typeof cb === 'function') cb(err); for (let i = 0; i < this._queue.length; i++) { - const callback = this._queue[i][4]; + const params = this._queue[i]; + const callback = params[params.length - 1]; if (typeof callback === 'function') callback(err); } @@ -1970,7 +2187,7 @@ class Sender { return; } - this._bufferedBytes -= data.length; + this._bufferedBytes -= options[kByteLength]; this._deflating = false; options.readOnly = false; this.sendFrame(Sender.frame(buf, options), cb); @@ -1987,7 +2204,7 @@ class Sender { while (!this._deflating && this._queue.length) { const params = this._queue.shift(); - this._bufferedBytes -= params[1].length; + this._bufferedBytes -= params[3][kByteLength]; Reflect.apply(params[0], this, params.slice(1)); } } @@ -1999,7 +2216,7 @@ class Sender { * @private */ enqueue(params) { - this._bufferedBytes += params[1].length; + this._bufferedBytes += params[3][kByteLength]; this._queue.push(params); } @@ -2024,112 +2241,173 @@ class Sender { var sender = Sender; +const { kForOnEventAttribute, kListener } = constants; + +const kCode = Symbol('kCode'); +const kData = Symbol('kData'); +const kError$1 = Symbol('kError'); +const kMessage = Symbol('kMessage'); +const kReason = Symbol('kReason'); +const kTarget = Symbol('kTarget'); +const kType = Symbol('kType'); +const kWasClean = Symbol('kWasClean'); + /** * Class representing an event. - * - * @private */ class Event { /** * Create a new `Event`. * * @param {String} type The name of the event - * @param {Object} target A reference to the target to which the event was - * dispatched + * @throws {TypeError} If the `type` argument is not specified + */ + constructor(type) { + this[kTarget] = null; + this[kType] = type; + } + + /** + * @type {*} + */ + get target() { + return this[kTarget]; + } + + /** + * @type {String} */ - constructor(type, target) { - this.target = target; - this.type = type; + get type() { + return this[kType]; } } +Object.defineProperty(Event.prototype, 'target', { enumerable: true }); +Object.defineProperty(Event.prototype, 'type', { enumerable: true }); + /** - * Class representing a message event. + * Class representing a close event. * * @extends Event - * @private */ -class MessageEvent extends Event { +class CloseEvent extends Event { /** - * Create a new `MessageEvent`. + * Create a new `CloseEvent`. * - * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {Number} [options.code=0] The status code explaining why the + * connection was closed + * @param {String} [options.reason=''] A human-readable string explaining why + * the connection was closed + * @param {Boolean} [options.wasClean=false] Indicates whether or not the + * connection was cleanly closed */ - constructor(data, target) { - super('message', target); + constructor(type, options = {}) { + super(type); - this.data = data; + this[kCode] = options.code === undefined ? 0 : options.code; + this[kReason] = options.reason === undefined ? '' : options.reason; + this[kWasClean] = options.wasClean === undefined ? false : options.wasClean; } -} -/** - * Class representing a close event. - * - * @extends Event - * @private - */ -class CloseEvent extends Event { /** - * Create a new `CloseEvent`. - * - * @param {Number} code The status code explaining why the connection is being - * closed - * @param {String} reason A human-readable string explaining why the - * connection is closing - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @type {Number} */ - constructor(code, reason, target) { - super('close', target); + get code() { + return this[kCode]; + } - this.wasClean = target._closeFrameReceived && target._closeFrameSent; - this.reason = reason; - this.code = code; + /** + * @type {String} + */ + get reason() { + return this[kReason]; + } + + /** + * @type {Boolean} + */ + get wasClean() { + return this[kWasClean]; } } +Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true }); + /** - * Class representing an open event. + * Class representing an error event. * * @extends Event - * @private */ -class OpenEvent extends Event { +class ErrorEvent extends Event { /** - * Create a new `OpenEvent`. + * Create a new `ErrorEvent`. * - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.error=null] The error that generated this event + * @param {String} [options.message=''] The error message + */ + constructor(type, options = {}) { + super(type); + + this[kError$1] = options.error === undefined ? null : options.error; + this[kMessage] = options.message === undefined ? '' : options.message; + } + + /** + * @type {*} */ - constructor(target) { - super('open', target); + get error() { + return this[kError$1]; + } + + /** + * @type {String} + */ + get message() { + return this[kMessage]; } } +Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true }); +Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true }); + /** - * Class representing an error event. + * Class representing a message event. * * @extends Event - * @private */ -class ErrorEvent extends Event { +class MessageEvent extends Event { /** - * Create a new `ErrorEvent`. + * Create a new `MessageEvent`. * - * @param {Object} error The error that generated this event - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.data=null] The message content */ - constructor(error, target) { - super('error', target); + constructor(type, options = {}) { + super(type); + + this[kData] = options.data === undefined ? null : options.data; + } - this.message = error.message; - this.error = error; + /** + * @type {*} + */ + get data() { + return this[kData]; } } +Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true }); + /** * This provides methods for emulating the `EventTarget` interface. It's not * meant to be used directly. @@ -2141,49 +2419,75 @@ const EventTarget = { * Register an event listener. * * @param {String} type A string representing the event type to listen for - * @param {Function} listener The listener to add + * @param {(Function|Object)} handler The listener to add * @param {Object} [options] An options object specifies characteristics about * the event listener - * @param {Boolean} [options.once=false] A `Boolean`` indicating that the + * @param {Boolean} [options.once=false] A `Boolean` indicating that the * listener should be invoked at most once after being added. If `true`, * the listener would be automatically removed when invoked. * @public */ - addEventListener(type, listener, options) { - if (typeof listener !== 'function') return; - - function onMessage(data) { - listener.call(this, new MessageEvent(data, this)); - } - - function onClose(code, message) { - listener.call(this, new CloseEvent(code, message, this)); - } - - function onError(error) { - listener.call(this, new ErrorEvent(error, this)); - } - - function onOpen() { - listener.call(this, new OpenEvent(this)); + addEventListener(type, handler, options = {}) { + for (const listener of this.listeners(type)) { + if ( + !options[kForOnEventAttribute] && + listener[kListener] === handler && + !listener[kForOnEventAttribute] + ) { + return; + } } - const method = options && options.once ? 'once' : 'on'; + let wrapper; if (type === 'message') { - onMessage._listener = listener; - this[method](type, onMessage); + wrapper = function onMessage(data, isBinary) { + const event = new MessageEvent('message', { + data: isBinary ? data : data.toString() + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'close') { - onClose._listener = listener; - this[method](type, onClose); + wrapper = function onClose(code, message) { + const event = new CloseEvent('close', { + code, + reason: message.toString(), + wasClean: this._closeFrameReceived && this._closeFrameSent + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'error') { - onError._listener = listener; - this[method](type, onError); + wrapper = function onError(error) { + const event = new ErrorEvent('error', { + error, + message: error.message + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'open') { - onOpen._listener = listener; - this[method](type, onOpen); + wrapper = function onOpen() { + const event = new Event('open'); + + event[kTarget] = this; + callListener(handler, this, event); + }; + } else { + return; + } + + wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; + wrapper[kListener] = handler; + + if (options.once) { + this.once(type, wrapper); } else { - this[method](type, listener); + this.on(type, wrapper); } }, @@ -2191,44 +2495,44 @@ const EventTarget = { * Remove an event listener. * * @param {String} type A string representing the event type to remove - * @param {Function} listener The listener to remove + * @param {(Function|Object)} handler The listener to remove * @public */ - removeEventListener(type, listener) { - const listeners = this.listeners(type); - - for (let i = 0; i < listeners.length; i++) { - if (listeners[i] === listener || listeners[i]._listener === listener) { - this.removeListener(type, listeners[i]); + removeEventListener(type, handler) { + for (const listener of this.listeners(type)) { + if (listener[kListener] === handler && !listener[kForOnEventAttribute]) { + this.removeListener(type, listener); + break; } } } }; -var eventTarget = EventTarget; +var eventTarget = { + CloseEvent, + ErrorEvent, + Event, + EventTarget, + MessageEvent +}; -// -// Allowed token characters: -// -// '!', '#', '$', '%', '&', ''', '*', '+', '-', -// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' -// -// tokenChars[32] === 0 // ' ' -// tokenChars[33] === 1 // '!' -// tokenChars[34] === 0 // '"' -// ... -// -// prettier-ignore -const tokenChars = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 - 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 -]; +/** + * Call an event listener + * + * @param {(Function|Object)} listener The listener to call + * @param {*} thisArg The value to use as `this`` when calling the listener + * @param {Event} event The event to pass to the listener + * @private + */ +function callListener(listener, thisArg, event) { + if (typeof listener === 'object' && listener.handleEvent) { + listener.handleEvent.call(listener, event); + } else { + listener.call(thisArg, event); + } +} + +const { tokenChars } = validation; /** * Adds an offer to the map of extension offers or a parameter to the map of @@ -2254,9 +2558,6 @@ function push(dest, name, elem) { */ function parse(header) { const offers = Object.create(null); - - if (header === undefined || header === '') return offers; - let params = Object.create(null); let mustUnescape = false; let isEscaping = false; @@ -2264,16 +2565,20 @@ function parse(header) { let extensionName; let paramName; let start = -1; + let code = -1; let end = -1; let i = 0; for (; i < header.length; i++) { - const code = header.charCodeAt(i); + code = header.charCodeAt(i); if (extensionName === undefined) { if (end === -1 && tokenChars[code] === 1) { if (start === -1) start = i; - } else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) { + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { if (end === -1 && start !== -1) end = i; } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { if (start === -1) { @@ -2374,7 +2679,7 @@ function parse(header) { } } - if (start === -1 || inQuotes) { + if (start === -1 || inQuotes || code === 0x20 || code === 0x09) { throw new SyntaxError('Unexpected end of input'); } @@ -2429,8 +2734,8 @@ function format(extensions) { var extension = { format, parse }; -const { randomBytes, createHash } = require$$0$1; -const { URL } = require$$1; +const { randomBytes, createHash } = require$$0$2; +const { URL } = require$$2; @@ -2439,17 +2744,23 @@ const { BINARY_TYPES: BINARY_TYPES$1, EMPTY_BUFFER: EMPTY_BUFFER$2, GUID, + kForOnEventAttribute: kForOnEventAttribute$1, + kListener: kListener$1, kStatusCode: kStatusCode$2, kWebSocket: kWebSocket$1, - NOOP: NOOP$1 + NOOP } = constants; -const { addEventListener, removeEventListener } = eventTarget; +const { + EventTarget: { addEventListener, removeEventListener } +} = eventTarget; const { format: format$1, parse: parse$1 } = extension; const { toBuffer: toBuffer$1 } = bufferUtil; -const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; -const protocolVersions = [8, 13]; const closeTimeout = 30 * 1000; +const kAborted = Symbol('kAborted'); +const protocolVersions = [8, 13]; +const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; +const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/; /** * Class representing a WebSocket. @@ -2460,7 +2771,7 @@ class WebSocket extends EventEmitter { /** * Create a new `WebSocket`. * - * @param {(String|url.URL)} address The URL to which to connect + * @param {(String|URL)} address The URL to which to connect * @param {(String|String[])} [protocols] The subprotocols * @param {Object} [options] Connection options */ @@ -2471,9 +2782,10 @@ class WebSocket extends EventEmitter { this._closeCode = 1006; this._closeFrameReceived = false; this._closeFrameSent = false; - this._closeMessage = ''; + this._closeMessage = EMPTY_BUFFER$2; this._closeTimer = null; this._extensions = {}; + this._paused = false; this._protocol = ''; this._readyState = WebSocket.CONNECTING; this._receiver = null; @@ -2485,11 +2797,15 @@ class WebSocket extends EventEmitter { this._isServer = false; this._redirects = 0; - if (Array.isArray(protocols)) { - protocols = protocols.join(', '); - } else if (typeof protocols === 'object' && protocols !== null) { - options = protocols; - protocols = undefined; + if (protocols === undefined) { + protocols = []; + } else if (!Array.isArray(protocols)) { + if (typeof protocols === 'object' && protocols !== null) { + options = protocols; + protocols = []; + } else { + protocols = [protocols]; + } } initAsClient(this, address, protocols, options); @@ -2536,6 +2852,45 @@ class WebSocket extends EventEmitter { return Object.keys(this._extensions).join(); } + /** + * @type {Boolean} + */ + get isPaused() { + return this._paused; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onclose() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onerror() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onopen() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onmessage() { + return null; + } + /** * @type {String} */ @@ -2560,20 +2915,27 @@ class WebSocket extends EventEmitter { /** * Set up the socket and the internal resources. * - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream - * @param {Number} [maxPayload=0] The maximum allowed message size + * @param {Object} options Options object + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Number} [options.maxPayload=0] The maximum allowed message size + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ - setSocket(socket, head, maxPayload) { - const receiver$1 = new receiver( - this.binaryType, - this._extensions, - this._isServer, - maxPayload - ); + setSocket(socket, head, options) { + const receiver$1 = new receiver({ + binaryType: this.binaryType, + extensions: this._extensions, + isServer: this._isServer, + maxPayload: options.maxPayload, + skipUTF8Validation: options.skipUTF8Validation + }); - this._sender = new sender(socket, this._extensions); + this._sender = new sender(socket, this._extensions, options.generateMask); this._receiver = receiver$1; this._socket = socket; @@ -2638,18 +3000,26 @@ class WebSocket extends EventEmitter { * +---+ * * @param {Number} [code] Status code explaining why the connection is closing - * @param {String} [data] A string explaining why the connection is closing + * @param {(String|Buffer)} [data] The reason why the connection is + * closing * @public */ close(code, data) { if (this.readyState === WebSocket.CLOSED) return; if (this.readyState === WebSocket.CONNECTING) { const msg = 'WebSocket was closed before the connection was established'; - return abortHandshake(this, this._req, msg); + abortHandshake(this, this._req, msg); + return; } if (this.readyState === WebSocket.CLOSING) { - if (this._closeFrameSent && this._closeFrameReceived) this._socket.end(); + if ( + this._closeFrameSent && + (this._closeFrameReceived || this._receiver._writableState.errorEmitted) + ) { + this._socket.end(); + } + return; } @@ -2662,7 +3032,13 @@ class WebSocket extends EventEmitter { if (err) return; this._closeFrameSent = true; - if (this._closeFrameReceived) this._socket.end(); + + if ( + this._closeFrameReceived || + this._receiver._writableState.errorEmitted + ) { + this._socket.end(); + } }); // @@ -2674,6 +3050,23 @@ class WebSocket extends EventEmitter { ); } + /** + * Pause the socket. + * + * @public + */ + pause() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = true; + this._socket.pause(); + } + /** * Send a ping. * @@ -2738,15 +3131,32 @@ class WebSocket extends EventEmitter { this._sender.pong(data || EMPTY_BUFFER$2, mask, cb); } + /** + * Resume the socket. + * + * @public + */ + resume() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = false; + if (!this._receiver._writableState.needDrain) this._socket.resume(); + } + /** * Send a data message. * * @param {*} data The message to send * @param {Object} [options] Options object - * @param {Boolean} [options.compress] Specifies whether or not to compress - * `data` * @param {Boolean} [options.binary] Specifies whether `data` is binary or * text + * @param {Boolean} [options.compress] Specifies whether or not to compress + * `data` * @param {Boolean} [options.fin=true] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask] Specifies whether or not to mask `data` @@ -2794,7 +3204,8 @@ class WebSocket extends EventEmitter { if (this.readyState === WebSocket.CLOSED) return; if (this.readyState === WebSocket.CONNECTING) { const msg = 'WebSocket was closed before the connection was established'; - return abortHandshake(this, this._req, msg); + abortHandshake(this, this._req, msg); + return; } if (this._socket) { @@ -2804,17 +3215,83 @@ class WebSocket extends EventEmitter { } } -readyStates.forEach((readyState, i) => { - const descriptor = { enumerable: true, value: i }; +/** + * @constant {Number} CONNECTING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} CONNECTING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); - Object.defineProperty(WebSocket.prototype, readyState, descriptor); - Object.defineProperty(WebSocket, readyState, descriptor); +/** + * @constant {Number} OPEN + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') }); [ 'binaryType', 'bufferedAmount', 'extensions', + 'isPaused', 'protocol', 'readyState', 'url' @@ -2828,37 +3305,27 @@ readyStates.forEach((readyState, i) => { // ['open', 'error', 'close', 'message'].forEach((method) => { Object.defineProperty(WebSocket.prototype, `on${method}`, { - configurable: true, enumerable: true, - /** - * Return the listener of the event. - * - * @return {(Function|undefined)} The event listener or `undefined` - * @public - */ get() { - const listeners = this.listeners(method); - for (let i = 0; i < listeners.length; i++) { - if (listeners[i]._listener) return listeners[i]._listener; + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute$1]) return listener[kListener$1]; } - return undefined; + return null; }, - /** - * Add a listener for the event. - * - * @param {Function} listener The listener to add - * @public - */ - set(listener) { - const listeners = this.listeners(method); - for (let i = 0; i < listeners.length; i++) { - // - // Remove only the listeners added via `addEventListener`. - // - if (listeners[i]._listener) this.removeListener(method, listeners[i]); + set(handler) { + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute$1]) { + this.removeListener(method, listener); + break; + } } - this.addEventListener(method, listener); + + if (typeof handler !== 'function') return; + + this.addEventListener(method, handler, { + [kForOnEventAttribute$1]: true + }); } }); }); @@ -2872,29 +3339,34 @@ var websocket = WebSocket; * Initialize a WebSocket client. * * @param {WebSocket} websocket The client to initialize - * @param {(String|url.URL)} address The URL to which to connect - * @param {String} [protocols] The subprotocols + * @param {(String|URL)} address The URL to which to connect + * @param {Array} protocols The subprotocols * @param {Object} [options] Connection options - * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable - * permessage-deflate + * @param {Boolean} [options.followRedirects=false] Whether or not to follow + * redirects + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the * handshake request - * @param {Number} [options.protocolVersion=13] Value of the - * `Sec-WebSocket-Version` header - * @param {String} [options.origin] Value of the `Origin` or - * `Sec-WebSocket-Origin` header * @param {Number} [options.maxPayload=104857600] The maximum allowed message * size - * @param {Boolean} [options.followRedirects=false] Whether or not to follow - * redirects * @param {Number} [options.maxRedirects=10] The maximum number of redirects * allowed + * @param {String} [options.origin] Value of the `Origin` or + * `Sec-WebSocket-Origin` header + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable + * permessage-deflate + * @param {Number} [options.protocolVersion=13] Value of the + * `Sec-WebSocket-Version` header + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ function initAsClient(websocket, address, protocols, options) { const opts = { protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: true, followRedirects: false, maxRedirects: 10, @@ -2904,7 +3376,7 @@ function initAsClient(websocket, address, protocols, options) { hostname: undefined, protocol: undefined, timeout: undefined, - method: undefined, + method: 'GET', host: undefined, path: undefined, port: undefined @@ -2923,21 +3395,43 @@ function initAsClient(websocket, address, protocols, options) { parsedUrl = address; websocket._url = address.href; } else { - parsedUrl = new URL(address); + try { + parsedUrl = new URL(address); + } catch (e) { + throw new SyntaxError(`Invalid URL: ${address}`); + } + websocket._url = address; } - const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; + const isSecure = parsedUrl.protocol === 'wss:'; + const isIpcUrl = parsedUrl.protocol === 'ws+unix:'; + let invalidUrlMessage; + + if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { + invalidUrlMessage = + 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; + } else if (isIpcUrl && !parsedUrl.pathname) { + invalidUrlMessage = "The URL's pathname is empty"; + } else if (parsedUrl.hash) { + invalidUrlMessage = 'The URL contains a fragment identifier'; + } + + if (invalidUrlMessage) { + const err = new SyntaxError(invalidUrlMessage); - if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) { - throw new Error(`Invalid URL: ${websocket.url}`); + if (websocket._redirects === 0) { + throw err; + } else { + emitErrorAndClose(websocket, err); + return; + } } - const isSecure = - parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:'; const defaultPort = isSecure ? 443 : 80; const key = randomBytes(16).toString('base64'); - const get = isSecure ? https.get : http.get; + const request = isSecure ? https.request : http.request; + const protocolSet = new Set(); let perMessageDeflate; opts.createConnection = isSecure ? tlsConnect : netConnect; @@ -2947,11 +3441,11 @@ function initAsClient(websocket, address, protocols, options) { ? parsedUrl.hostname.slice(1, -1) : parsedUrl.hostname; opts.headers = { + ...opts.headers, 'Sec-WebSocket-Version': opts.protocolVersion, 'Sec-WebSocket-Key': key, Connection: 'Upgrade', - Upgrade: 'websocket', - ...opts.headers + Upgrade: 'websocket' }; opts.path = parsedUrl.pathname + parsedUrl.search; opts.timeout = opts.handshakeTimeout; @@ -2966,8 +3460,22 @@ function initAsClient(websocket, address, protocols, options) { [permessageDeflate.extensionName]: perMessageDeflate.offer() }); } - if (protocols) { - opts.headers['Sec-WebSocket-Protocol'] = protocols; + if (protocols.length) { + for (const protocol of protocols) { + if ( + typeof protocol !== 'string' || + !subprotocolRegex.test(protocol) || + protocolSet.has(protocol) + ) { + throw new SyntaxError( + 'An invalid or duplicated subprotocol was specified' + ); + } + + protocolSet.add(protocol); + } + + opts.headers['Sec-WebSocket-Protocol'] = protocols.join(','); } if (opts.origin) { if (opts.protocolVersion < 13) { @@ -2980,15 +3488,87 @@ function initAsClient(websocket, address, protocols, options) { opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; } - if (isUnixSocket) { + if (isIpcUrl) { const parts = opts.path.split(':'); - opts.socketPath = parts[0]; - opts.path = parts[1]; + opts.socketPath = parts[0]; + opts.path = parts[1]; + } + + let req; + + if (opts.followRedirects) { + if (websocket._redirects === 0) { + websocket._originalIpc = isIpcUrl; + websocket._originalSecure = isSecure; + websocket._originalHostOrSocketPath = isIpcUrl + ? opts.socketPath + : parsedUrl.host; + + const headers = options && options.headers; + + // + // Shallow copy the user provided options so that headers can be changed + // without mutating the original object. + // + options = { ...options, headers: {} }; + + if (headers) { + for (const [key, value] of Object.entries(headers)) { + options.headers[key.toLowerCase()] = value; + } + } + } else if (websocket.listenerCount('redirect') === 0) { + const isSameHost = isIpcUrl + ? websocket._originalIpc + ? opts.socketPath === websocket._originalHostOrSocketPath + : false + : websocket._originalIpc + ? false + : parsedUrl.host === websocket._originalHostOrSocketPath; + + if (!isSameHost || (websocket._originalSecure && !isSecure)) { + // + // Match curl 7.77.0 behavior and drop the following headers. These + // headers are also dropped when following a redirect to a subdomain. + // + delete opts.headers.authorization; + delete opts.headers.cookie; + + if (!isSameHost) delete opts.headers.host; + + opts.auth = undefined; + } + } + + // + // Match curl 7.77.0 behavior and make the first `Authorization` header win. + // If the `Authorization` header is set, then there is nothing to do as it + // will take precedence. + // + if (opts.auth && !options.headers.authorization) { + options.headers.authorization = + 'Basic ' + Buffer.from(opts.auth).toString('base64'); + } + + req = websocket._req = request(opts); + + if (websocket._redirects) { + // + // Unlike what is done for the `'upgrade'` event, no early exit is + // triggered here if the user calls `websocket.close()` or + // `websocket.terminate()` from a listener of the `'redirect'` event. This + // is because the user can also call `request.destroy()` with an error + // before calling `websocket.close()` or `websocket.terminate()` and this + // would result in an error being emitted on the `request` object with no + // `'error'` event listeners attached. + // + websocket.emit('redirect', websocket.url, req); + } + } else { + req = websocket._req = request(opts); } - let req = (websocket._req = get(opts)); - if (opts.timeout) { req.on('timeout', () => { abortHandshake(websocket, req, 'Opening handshake has timed out'); @@ -2996,12 +3576,10 @@ function initAsClient(websocket, address, protocols, options) { } req.on('error', (err) => { - if (req === null || req.aborted) return; + if (req === null || req[kAborted]) return; req = websocket._req = null; - websocket._readyState = WebSocket.CLOSING; - websocket.emit('error', err); - websocket.emitClose(); + emitErrorAndClose(websocket, err); }); req.on('response', (res) => { @@ -3021,7 +3599,15 @@ function initAsClient(websocket, address, protocols, options) { req.abort(); - const addr = new URL(location, address); + let addr; + + try { + addr = new URL(location, address); + } catch (e) { + const err = new SyntaxError(`Invalid URL: ${location}`); + emitErrorAndClose(websocket, err); + return; + } initAsClient(websocket, addr, protocols, options); } else if (!websocket.emit('unexpected-response', req, res)) { @@ -3037,13 +3623,18 @@ function initAsClient(websocket, address, protocols, options) { websocket.emit('upgrade', res); // - // The user may have closed the connection from a listener of the `upgrade` - // event. + // The user may have closed the connection from a listener of the + // `'upgrade'` event. // if (websocket.readyState !== WebSocket.CONNECTING) return; req = websocket._req = null; + if (res.headers.upgrade.toLowerCase() !== 'websocket') { + abortHandshake(websocket, socket, 'Invalid Upgrade header'); + return; + } + const digest = createHash('sha1') .update(key + GUID) .digest('base64'); @@ -3054,15 +3645,16 @@ function initAsClient(websocket, address, protocols, options) { } const serverProt = res.headers['sec-websocket-protocol']; - const protList = (protocols || '').split(/, */); let protError; - if (!protocols && serverProt) { - protError = 'Server sent a subprotocol but none was requested'; - } else if (protocols && !serverProt) { + if (serverProt !== undefined) { + if (!protocolSet.size) { + protError = 'Server sent a subprotocol but none was requested'; + } else if (!protocolSet.has(serverProt)) { + protError = 'Server sent an invalid subprotocol'; + } + } else if (protocolSet.size) { protError = 'Server sent no subprotocol'; - } else if (serverProt && !protList.includes(serverProt)) { - protError = 'Server sent an invalid subprotocol'; } if (protError) { @@ -3072,28 +3664,75 @@ function initAsClient(websocket, address, protocols, options) { if (serverProt) websocket._protocol = serverProt; - if (perMessageDeflate) { + const secWebSocketExtensions = res.headers['sec-websocket-extensions']; + + if (secWebSocketExtensions !== undefined) { + if (!perMessageDeflate) { + const message = + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested'; + abortHandshake(websocket, socket, message); + return; + } + + let extensions; + try { - const extensions = parse$1(res.headers['sec-websocket-extensions']); + extensions = parse$1(secWebSocketExtensions); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } - if (extensions[permessageDeflate.extensionName]) { - perMessageDeflate.accept(extensions[permessageDeflate.extensionName]); - websocket._extensions[ - permessageDeflate.extensionName - ] = perMessageDeflate; - } + const extensionNames = Object.keys(extensions); + + if ( + extensionNames.length !== 1 || + extensionNames[0] !== permessageDeflate.extensionName + ) { + const message = 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } + + try { + perMessageDeflate.accept(extensions[permessageDeflate.extensionName]); } catch (err) { - abortHandshake( - websocket, - socket, - 'Invalid Sec-WebSocket-Extensions header' - ); + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); return; } + + websocket._extensions[permessageDeflate.extensionName] = + perMessageDeflate; } - websocket.setSocket(socket, head, opts.maxPayload); + websocket.setSocket(socket, head, { + generateMask: opts.generateMask, + maxPayload: opts.maxPayload, + skipUTF8Validation: opts.skipUTF8Validation + }); }); + + if (opts.finishRequest) { + opts.finishRequest(req, websocket); + } else { + req.end(); + } +} + +/** + * Emit the `'error'` and `'close'` events. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {Error} The error to emit + * @private + */ +function emitErrorAndClose(websocket, err) { + websocket._readyState = WebSocket.CLOSING; + websocket.emit('error', err); + websocket.emitClose(); } /** @@ -3129,8 +3768,8 @@ function tlsConnect(options) { * Abort the handshake and emit an error. * * @param {WebSocket} websocket The WebSocket instance - * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the - * socket to destroy + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to + * abort or the socket to destroy * @param {String} message The error message * @private */ @@ -3141,6 +3780,7 @@ function abortHandshake(websocket, stream, message) { Error.captureStackTrace(err, abortHandshake); if (stream.setHeader) { + stream[kAborted] = true; stream.abort(); if (stream.socket && !stream.socket.destroyed) { @@ -3152,8 +3792,7 @@ function abortHandshake(websocket, stream, message) { stream.socket.destroy(); } - stream.once('abort', websocket.emitClose.bind(websocket)); - websocket.emit('error', err); + process.nextTick(emitErrorAndClose, websocket, err); } else { stream.destroy(err); stream.once('error', websocket.emit.bind(websocket, 'error')); @@ -3189,7 +3828,7 @@ function sendAfterClose(websocket, data, cb) { `WebSocket is not open: readyState ${websocket.readyState} ` + `(${readyStates[websocket.readyState]})` ); - cb(err); + process.nextTick(cb, err); } } @@ -3197,19 +3836,21 @@ function sendAfterClose(websocket, data, cb) { * The listener of the `Receiver` `'conclude'` event. * * @param {Number} code The status code - * @param {String} reason The reason for closing + * @param {Buffer} reason The reason for closing * @private */ function receiverOnConclude(code, reason) { const websocket = this[kWebSocket$1]; - websocket._socket.removeListener('data', socketOnData); - websocket._socket.resume(); - websocket._closeFrameReceived = true; websocket._closeMessage = reason; websocket._closeCode = code; + if (websocket._socket[kWebSocket$1] === undefined) return; + + websocket._socket.removeListener('data', socketOnData); + process.nextTick(resume, websocket._socket); + if (code === 1005) websocket.close(); else websocket.close(code, reason); } @@ -3220,7 +3861,9 @@ function receiverOnConclude(code, reason) { * @private */ function receiverOnDrain() { - this[kWebSocket$1]._socket.resume(); + const websocket = this[kWebSocket$1]; + + if (!websocket.isPaused) websocket._socket.resume(); } /** @@ -3232,12 +3875,19 @@ function receiverOnDrain() { function receiverOnError(err) { const websocket = this[kWebSocket$1]; - websocket._socket.removeListener('data', socketOnData); + if (websocket._socket[kWebSocket$1] !== undefined) { + websocket._socket.removeListener('data', socketOnData); + + // + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See + // https://github.com/websockets/ws/issues/1940. + // + process.nextTick(resume, websocket._socket); + + websocket.close(err[kStatusCode$2]); + } - websocket._readyState = WebSocket.CLOSING; - websocket._closeCode = err[kStatusCode$2]; websocket.emit('error', err); - websocket._socket.destroy(); } /** @@ -3252,11 +3902,12 @@ function receiverOnFinish() { /** * The listener of the `Receiver` `'message'` event. * - * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Boolean} isBinary Specifies whether the message is binary or not * @private */ -function receiverOnMessage(data) { - this[kWebSocket$1].emit('message', data); +function receiverOnMessage(data, isBinary) { + this[kWebSocket$1].emit('message', data, isBinary); } /** @@ -3268,7 +3919,7 @@ function receiverOnMessage(data) { function receiverOnPing(data) { const websocket = this[kWebSocket$1]; - websocket.pong(data, !websocket._isServer, NOOP$1); + websocket.pong(data, !websocket._isServer, NOOP); websocket.emit('ping', data); } @@ -3282,6 +3933,16 @@ function receiverOnPong(data) { this[kWebSocket$1].emit('pong', data); } +/** + * Resume a readable stream + * + * @param {Readable} stream The readable stream + * @private + */ +function resume(stream) { + stream.resume(); +} + /** * The listener of the `net.Socket` `'close'` event. * @@ -3291,10 +3952,13 @@ function socketOnClose() { const websocket = this[kWebSocket$1]; this.removeListener('close', socketOnClose); + this.removeListener('data', socketOnData); this.removeListener('end', socketOnEnd); websocket._readyState = WebSocket.CLOSING; + let chunk; + // // The close frame might not have been received or the `'end'` event emitted, // for example, if the socket was destroyed due to an error. Ensure that the @@ -3302,13 +3966,19 @@ function socketOnClose() { // it. If the readable side of the socket is in flowing mode then there is no // buffered data as everything has been already written and `readable.read()` // will return `null`. If instead, the socket is paused, any possible buffered - // data will be read as a single chunk and emitted synchronously in a single - // `'data'` event. + // data will be read as a single chunk. // - websocket._socket.read(); + if ( + !this._readableState.endEmitted && + !websocket._closeFrameReceived && + !websocket._receiver._writableState.errorEmitted && + (chunk = websocket._socket.read()) !== null + ) { + websocket._receiver.write(chunk); + } + websocket._receiver.end(); - this.removeListener('data', socketOnData); this[kWebSocket$1] = undefined; clearTimeout(websocket._closeTimer); @@ -3358,7 +4028,7 @@ function socketOnError() { const websocket = this[kWebSocket$1]; this.removeListener('error', socketOnError); - this.on('error', NOOP$1); + this.on('error', NOOP); if (websocket) { websocket._readyState = WebSocket.CLOSING; @@ -3366,12 +4036,12 @@ function socketOnError() { } } -const { Duplex } = require$$0; +const { Duplex } = require$$0$1; /** * Emits the `'close'` event on a stream. * - * @param {stream.Duplex} The stream. + * @param {Duplex} stream The stream. * @private */ function emitClose(stream) { @@ -3409,25 +4079,11 @@ function duplexOnError(err) { * * @param {WebSocket} ws The `WebSocket` to wrap * @param {Object} [options] The options for the `Duplex` constructor - * @return {stream.Duplex} The duplex stream + * @return {Duplex} The duplex stream * @public */ function createWebSocketStream(ws, options) { - let resumeOnReceiverDrain = true; - - function receiverOnDrain() { - if (resumeOnReceiverDrain) ws._socket.resume(); - } - - if (ws.readyState === ws.CONNECTING) { - ws.once('open', function open() { - ws._receiver.removeAllListeners('drain'); - ws._receiver.on('drain', receiverOnDrain); - }); - } else { - ws._receiver.removeAllListeners('drain'); - ws._receiver.on('drain', receiverOnDrain); - } + let terminateOnDestroy = true; const duplex = new Duplex({ ...options, @@ -3437,16 +4093,26 @@ function createWebSocketStream(ws, options) { writableObjectMode: false }); - ws.on('message', function message(msg) { - if (!duplex.push(msg)) { - resumeOnReceiverDrain = false; - ws._socket.pause(); - } + ws.on('message', function message(msg, isBinary) { + const data = + !isBinary && duplex._readableState.objectMode ? msg.toString() : msg; + + if (!duplex.push(data)) ws.pause(); }); ws.once('error', function error(err) { if (duplex.destroyed) return; + // Prevent `ws.terminate()` from being called by `duplex._destroy()`. + // + // - If the `'error'` event is emitted before the `'open'` event, then + // `ws.terminate()` is a noop as no socket is assigned. + // - Otherwise, the error is re-emitted by the listener of the `'error'` + // event of the `Receiver` object. The listener already closes the + // connection by calling `ws.close()`. This allows a close frame to be + // sent to the other peer. If `ws.terminate()` is called right after this, + // then the close frame might not be sent. + terminateOnDestroy = false; duplex.destroy(err); }); @@ -3474,7 +4140,8 @@ function createWebSocketStream(ws, options) { if (!called) callback(err); process.nextTick(emitClose, duplex); }); - ws.terminate(); + + if (terminateOnDestroy) ws.terminate(); }; duplex._final = function (callback) { @@ -3506,10 +4173,7 @@ function createWebSocketStream(ws, options) { }; duplex._read = function () { - if (ws.readyState === ws.OPEN && !resumeOnReceiverDrain) { - resumeOnReceiverDrain = true; - if (!ws._receiver._writableState.needDrain) ws._socket.resume(); - } + if (ws.isPaused) ws.resume(); }; duplex._write = function (chunk, encoding, callback) { @@ -3530,16 +4194,81 @@ function createWebSocketStream(ws, options) { var stream = createWebSocketStream; -const { createHash: createHash$1 } = require$$0$1; -const { createServer, STATUS_CODES } = http; +const { tokenChars: tokenChars$1 } = validation; + +/** + * Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names. + * + * @param {String} header The field value of the header + * @return {Set} The subprotocol names + * @public + */ +function parse$2(header) { + const protocols = new Set(); + let start = -1; + let end = -1; + let i = 0; + + for (i; i < header.length; i++) { + const code = header.charCodeAt(i); + + if (end === -1 && tokenChars$1[code] === 1) { + if (start === -1) start = i; + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x2c /* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + + const protocol = header.slice(start, end); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + + if (start === -1 || end !== -1) { + throw new SyntaxError('Unexpected end of input'); + } + + const protocol = header.slice(start, i); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + return protocols; +} + +var subprotocol = { parse: parse$2 }; + +const { createHash: createHash$1 } = require$$0$2; + + -const { format: format$2, parse: parse$2 } = extension; const { GUID: GUID$1, kWebSocket: kWebSocket$2 } = constants; const keyRegex = /^[+/0-9A-Za-z]{22}==$/; +const RUNNING = 0; +const CLOSING = 1; +const CLOSED = 2; + /** * Class representing a WebSocket server. * @@ -3563,8 +4292,13 @@ class WebSocketServer extends EventEmitter { * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable * permessage-deflate * @param {Number} [options.port] The port where to bind the server - * @param {http.Server} [options.server] A pre-created HTTP/S server to use + * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S + * server to use + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @param {Function} [options.verifyClient] A hook to reject connections + * @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket` + * class to use. It must be the `WebSocket` class or class that extends it * @param {Function} [callback] A listener for the `listening` event */ constructor(options, callback) { @@ -3572,6 +4306,7 @@ class WebSocketServer extends EventEmitter { options = { maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: false, handleProtocols: null, clientTracking: true, @@ -3582,18 +4317,24 @@ class WebSocketServer extends EventEmitter { host: null, path: null, port: null, + WebSocket: websocket, ...options }; - if (options.port == null && !options.server && !options.noServer) { + if ( + (options.port == null && !options.server && !options.noServer) || + (options.port != null && (options.server || options.noServer)) || + (options.server && options.noServer) + ) { throw new TypeError( - 'One of the "port", "server", or "noServer" options must be specified' + 'One and only one of the "port", "server", or "noServer" options ' + + 'must be specified' ); } if (options.port != null) { - this._server = createServer((req, res) => { - const body = STATUS_CODES[426]; + this._server = http.createServer((req, res) => { + const body = http.STATUS_CODES[426]; res.writeHead(426, { 'Content-Length': body.length, @@ -3624,8 +4365,13 @@ class WebSocketServer extends EventEmitter { } if (options.perMessageDeflate === true) options.perMessageDeflate = {}; - if (options.clientTracking) this.clients = new Set(); + if (options.clientTracking) { + this.clients = new Set(); + this._shouldEmitClose = false; + } + this.options = options; + this._state = RUNNING; } /** @@ -3647,37 +4393,58 @@ class WebSocketServer extends EventEmitter { } /** - * Close the server. + * Stop the server from accepting new connections and emit the `'close'` event + * when all existing connections are closed. * - * @param {Function} [cb] Callback + * @param {Function} [cb] A one-time listener for the `'close'` event * @public */ close(cb) { - if (cb) this.once('close', cb); + if (this._state === CLOSED) { + if (cb) { + this.once('close', () => { + cb(new Error('The server is not running')); + }); + } - // - // Terminate all associated clients. - // - if (this.clients) { - for (const client of this.clients) client.terminate(); + process.nextTick(emitClose$1, this); + return; } - const server = this._server; + if (cb) this.once('close', cb); + + if (this._state === CLOSING) return; + this._state = CLOSING; + + if (this.options.noServer || this.options.server) { + if (this._server) { + this._removeListeners(); + this._removeListeners = this._server = null; + } + + if (this.clients) { + if (!this.clients.size) { + process.nextTick(emitClose$1, this); + } else { + this._shouldEmitClose = true; + } + } else { + process.nextTick(emitClose$1, this); + } + } else { + const server = this._server; - if (server) { this._removeListeners(); this._removeListeners = this._server = null; // - // Close the http server if it was internally created. + // The HTTP/S server was created internally. Close it, and rely on its + // `'close'` event. // - if (this.options.port != null) { - server.close(() => this.emit('close')); - return; - } + server.close(() => { + emitClose$1(this); + }); } - - process.nextTick(emitClose$1, this); } /** @@ -3702,7 +4469,8 @@ class WebSocketServer extends EventEmitter { * Handle a HTTP Upgrade request. * * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @public @@ -3710,25 +4478,58 @@ class WebSocketServer extends EventEmitter { handleUpgrade(req, socket, head, cb) { socket.on('error', socketOnError$1); - const key = - req.headers['sec-websocket-key'] !== undefined - ? req.headers['sec-websocket-key'].trim() - : false; + const key = req.headers['sec-websocket-key']; const version = +req.headers['sec-websocket-version']; + + if (req.method !== 'GET') { + const message = 'Invalid HTTP method'; + abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); + return; + } + + if (req.headers.upgrade.toLowerCase() !== 'websocket') { + const message = 'Invalid Upgrade header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (!key || !keyRegex.test(key)) { + const message = 'Missing or invalid Sec-WebSocket-Key header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (version !== 8 && version !== 13) { + const message = 'Missing or invalid Sec-WebSocket-Version header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (!this.shouldHandle(req)) { + abortHandshake$1(socket, 400); + return; + } + + const secWebSocketProtocol = req.headers['sec-websocket-protocol']; + let protocols = new Set(); + + if (secWebSocketProtocol !== undefined) { + try { + protocols = subprotocol.parse(secWebSocketProtocol); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Protocol header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + } + + const secWebSocketExtensions = req.headers['sec-websocket-extensions']; const extensions = {}; if ( - req.method !== 'GET' || - req.headers.upgrade.toLowerCase() !== 'websocket' || - !key || - !keyRegex.test(key) || - (version !== 8 && version !== 13) || - !this.shouldHandle(req) + this.options.perMessageDeflate && + secWebSocketExtensions !== undefined ) { - return abortHandshake$1(socket, 400); - } - - if (this.options.perMessageDeflate) { const perMessageDeflate = new permessageDeflate( this.options.perMessageDeflate, true, @@ -3736,14 +4537,17 @@ class WebSocketServer extends EventEmitter { ); try { - const offers = parse$2(req.headers['sec-websocket-extensions']); + const offers = extension.parse(secWebSocketExtensions); if (offers[permessageDeflate.extensionName]) { perMessageDeflate.accept(offers[permessageDeflate.extensionName]); extensions[permessageDeflate.extensionName] = perMessageDeflate; } } catch (err) { - return abortHandshake$1(socket, 400); + const message = + 'Invalid or unacceptable Sec-WebSocket-Extensions header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; } } @@ -3764,7 +4568,15 @@ class WebSocketServer extends EventEmitter { return abortHandshake$1(socket, code || 401, message, headers); } - this.completeUpgrade(key, extensions, req, socket, head, cb); + this.completeUpgrade( + extensions, + key, + protocols, + req, + socket, + head, + cb + ); }); return; } @@ -3772,22 +4584,24 @@ class WebSocketServer extends EventEmitter { if (!this.options.verifyClient(info)) return abortHandshake$1(socket, 401); } - this.completeUpgrade(key, extensions, req, socket, head, cb); + this.completeUpgrade(extensions, key, protocols, req, socket, head, cb); } /** * Upgrade the connection to WebSocket. * - * @param {String} key The value of the `Sec-WebSocket-Key` header * @param {Object} extensions The accepted extensions + * @param {String} key The value of the `Sec-WebSocket-Key` header + * @param {Set} protocols The subprotocols * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @throws {Error} If called more than once with the same socket * @private */ - completeUpgrade(key, extensions, req, socket, head, cb) { + completeUpgrade(extensions, key, protocols, req, socket, head, cb) { // // Destroy the socket if the client has already sent a FIN packet. // @@ -3800,6 +4614,8 @@ class WebSocketServer extends EventEmitter { ); } + if (this._state > RUNNING) return abortHandshake$1(socket, 503); + const digest = createHash$1('sha1') .update(key + GUID$1) .digest('base64'); @@ -3811,20 +4627,15 @@ class WebSocketServer extends EventEmitter { `Sec-WebSocket-Accept: ${digest}` ]; - const ws = new websocket(null); - let protocol = req.headers['sec-websocket-protocol']; - - if (protocol) { - protocol = protocol.trim().split(/ *, */); + const ws = new this.options.WebSocket(null); + if (protocols.size) { // // Optionally call external protocol selection handler. // - if (this.options.handleProtocols) { - protocol = this.options.handleProtocols(protocol, req); - } else { - protocol = protocol[0]; - } + const protocol = this.options.handleProtocols + ? this.options.handleProtocols(protocols, req) + : protocols.values().next().value; if (protocol) { headers.push(`Sec-WebSocket-Protocol: ${protocol}`); @@ -3834,7 +4645,7 @@ class WebSocketServer extends EventEmitter { if (extensions[permessageDeflate.extensionName]) { const params = extensions[permessageDeflate.extensionName].params; - const value = format$2({ + const value = extension.format({ [permessageDeflate.extensionName]: [params] }); headers.push(`Sec-WebSocket-Extensions: ${value}`); @@ -3849,11 +4660,20 @@ class WebSocketServer extends EventEmitter { socket.write(headers.concat('\r\n').join('\r\n')); socket.removeListener('error', socketOnError$1); - ws.setSocket(socket, head, this.options.maxPayload); + ws.setSocket(socket, head, { + maxPayload: this.options.maxPayload, + skipUTF8Validation: this.options.skipUTF8Validation + }); if (this.clients) { this.clients.add(ws); - ws.on('close', () => this.clients.delete(ws)); + ws.on('close', () => { + this.clients.delete(ws); + + if (this._shouldEmitClose && !this.clients.size) { + process.nextTick(emitClose$1, this); + } + }); } cb(ws, req); @@ -3889,11 +4709,12 @@ function addListeners(server, map) { * @private */ function emitClose$1(server) { + server._state = CLOSED; server.emit('close'); } /** - * Handle premature socket errors. + * Handle socket errors. * * @private */ @@ -3904,34 +4725,61 @@ function socketOnError$1() { /** * Close the connection when preconditions are not fulfilled. * - * @param {net.Socket} socket The socket of the upgrade request + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} [message] The HTTP response body * @param {Object} [headers] Additional HTTP response headers * @private */ function abortHandshake$1(socket, code, message, headers) { - if (socket.writable) { - message = message || STATUS_CODES[code]; - headers = { - Connection: 'close', - 'Content-Type': 'text/html', - 'Content-Length': Buffer.byteLength(message), - ...headers - }; + // + // The socket is writable unless the user destroyed or ended it before calling + // `server.handleUpgrade()` or in the `verifyClient` function, which is a user + // error. Handling this does not make much sense as the worst that can happen + // is that some of the data written by the user might be discarded due to the + // call to `socket.end()` below, which triggers an `'error'` event that in + // turn causes the socket to be destroyed. + // + message = message || http.STATUS_CODES[code]; + headers = { + Connection: 'close', + 'Content-Type': 'text/html', + 'Content-Length': Buffer.byteLength(message), + ...headers + }; - socket.write( - `HTTP/1.1 ${code} ${STATUS_CODES[code]}\r\n` + - Object.keys(headers) - .map((h) => `${h}: ${headers[h]}`) - .join('\r\n') + - '\r\n\r\n' + - message - ); - } + socket.once('finish', socket.destroy); + + socket.end( + `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + + Object.keys(headers) + .map((h) => `${h}: ${headers[h]}`) + .join('\r\n') + + '\r\n\r\n' + + message + ); +} + +/** + * Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least + * one listener for it, otherwise call `abortHandshake()`. + * + * @param {WebSocketServer} server The WebSocket server + * @param {http.IncomingMessage} req The request object + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} message The HTTP response body + * @private + */ +function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) { + if (server.listenerCount('wsClientError')) { + const err = new Error(message); + Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); - socket.removeListener('error', socketOnError$1); - socket.destroy(); + server.emit('wsClientError', err, socket, req); + } else { + abortHandshake$1(socket, code, message); + } } websocket.createWebSocketStream = stream; @@ -3939,6 +4787,9 @@ websocket.Server = websocketServer; websocket.Receiver = receiver; websocket.Sender = sender; +websocket.WebSocket = websocket; +websocket.WebSocketServer = websocket.Server; + var ws = websocket; var naclFast = createCommonjsModule(function (module) { @@ -6320,7 +7171,7 @@ nacl.setPRNG = function(fn) { }); } else if (typeof commonjsRequire !== 'undefined') { // Node.js. - crypto = require$$0$1; + crypto = require$$0$2; if (crypto && crypto.randomBytes) { nacl.setPRNG(function(x, n) { var i, v = crypto.randomBytes(n); @@ -7061,7 +7912,7 @@ function send({ data, connector }) { log, `Connector ${connector.endpoint} → Sending message #${connector.sentCount} ↴` ); - logger.gray(log, data); + logger.cyan(log, data); } connector.connection.websocket.send(data); @@ -7127,11 +7978,25 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec // 💡 encryptedJson data!! if (connector.verbose == 'extra') { logger.magenta(log, `Connector ${connector.endpoint} received bytes ↴`); - logger.gray(log, encryptedData); - logger.magenta( + logger.cyan(log, encryptedData); + logger.green(log, JSON.stringify(encryptedData)); + logger.gray( log, `Connector ${connector.endpoint} decrypting with shared secret ${connector.sharedSecret}...` ); + //logger.cyan(log, JSON.stringify(connector.sharedSecret)); + } + + if (!connector.sharedSecret) { + // we had this problem before -- zurich wifi -- when terminating inactive websocket + // it didn't actually close in time .. we set connector to disconnected and deleted sharedSecret + // but then a stray message json rpc return from hadshake arrived after that and couldn't be decrypted + // because it shouldn't have arrived in the first place after websocket was supposedly closed + // solution: __closed flag on all websockets.. it is set to true at the same time as calling close() + // and then any messages still coming over the wire on such closed websockets are dropped + // we hope websocket is eventually closed though (?) + // see messageCallback in establishAndMaintainConnection, this was fixed there + logger.red(log, `Connector ${connector.endpoint} missing sharedSecret - should not happen...`); } const _decryptedMessage = naclFast.secretbox.open(encryptedData, nonce, connector.sharedSecret); @@ -7143,7 +8008,7 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec const decodedMessage = naclFast.util.encodeUTF8(decryptedMessage); if (connector.verbose) { - logger.write(log, `Received message: ${decodedMessage}`); + logger.yellow(log, `Connector ${connector.endpoint} received message: ${decodedMessage}`); } try { @@ -7189,6 +8054,10 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec throw e; } } else { + if (connector.verbose) { + logger.yellow(log, `Connector ${connector.endpoint} received binary data`); + } + //const binaryData = decryptedMessage; // const sessionId = Buffer.from(binaryData.buffer, binaryData.byteOffset, 64).toString(); // const binaryPayload = Buffer.from(binaryData.buffer, binaryData.byteOffset + 64); @@ -7200,20 +8069,17 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec naclFast.util = naclUtil; +const wsOPEN = 1; + function diffieHellman({ connector, afterFirstStep = () => {} }) { - const { - clientPrivateKey, - clientPublicKey, - clientPublicKeyHex, - protocol, - tag, - endpoint, - verbose - } = connector; + const { clientPrivateKey, clientPublicKey, clientPublicKeyHex, protocol, tag, endpoint, verbose } = + connector; return new Promise((success, reject) => { - connector.remoteObject('Auth') + connector + .remoteObject('Auth') .call('exchangePubkeys', { pubkey: clientPublicKeyHex }) + //.call('exchangePubkeys', { pubkey: clientPublicKeyHex, clientWsId: connector.connection.websocket.__id }) .then(remotePubkeyHex => { const sharedSecret = naclFast.box.before(hexToBuffer(remotePubkeyHex), clientPrivateKey); @@ -7226,33 +8092,50 @@ function diffieHellman({ connector, afterFirstStep = () => {} }) { ); } - connector.remoteObject('Auth') - .call('finalizeHandshake', { protocol }) - .then(res => { - // finalizeHandshake rpc endpoint on server can cleanly retorn {error} as a result - // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) - if (res && res.error) { - console.log(res.error); - // this connection will keep hangling and no reconnect tries will be made - // since we keep websocket open just that nothing is happening - - // when we enable the protocol on the endpoint we have to restart the process - // frontend connector will get disconnected at this point, websocket will close - // and from then on it tries reconnecting again so when ws first connects - // and protocol is present , it will be a success - - // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging - } else { - success(); - - const _tag = tag ? ` (${tag})` : ''; - logger.cyan( - connector.log, - `${endpoint}${_tag} ✓ Connection [ ${protocol || '"no-name"'} ] ready` - ); - } - }) - .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + // if connection has closed at this point we don't try to send into closed + // connection, it would still work but error would be logged + if (connector.connection.websocket.readyState == wsOPEN) { + connector + .remoteObject('Auth') + .call('finalizeHandshake', { protocol }) + .then(res => { + // finalizeHandshake rpc endpoint on server can cleanly return {error} as a result + // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) + if (res && res.error) { + console.log(res.error); + // this connection will keep hangling and no reconnect tries will be made + // since we keep websocket open just that nothing is happening + + // when we enable the protocol on the endpoint we have to restart the process + // frontend connector will get disconnected at this point, websocket will close + // and from then on it tries reconnecting again so when ws first connects + // and protocol is present , it will be a success + + // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging + } else { + success(); + + const _tag = tag ? ` (${tag})` : ''; + logger.cyan( + connector.log, + `✓✓✓ ${endpoint}${_tag} ✓ Connection #${connector.connection.websocket.__id} [ ${ + protocol || '"no-name"' + } ] ready` + ); + } + }) + .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + } else { + const _tag = tag ? ` (${tag})` : ''; + logger.yellow( + connector.log, + `${endpoint}${_tag} ✖ Connection [ ${ + protocol || '"no-name"' + } ] closed just before finalizeHandshake step` + ); + // don't reject here -- because it will show some wring log message in connector + // on:ready error "will not try to reconnect" .. which is not the case here + } }) .catch(reject); }); @@ -8746,7 +9629,7 @@ const DECOMMISSION_INACTIVITY = 60000; // 1min //const DECOMMISSION_INACTIVITY = 120000; // 2min //const DECOMMISSION_INACTIVITY = 10000; // 2min -const wsOPEN = 1; +const wsOPEN$1 = 1; class Connector extends Eev { constructor({ @@ -8902,7 +9785,7 @@ class Connector extends Eev { this.successfulConnectsCount += 1; if (this.verbose) { - logger.green(this.log, `✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`); + logger.white(this.log, `✓ Connector ${this.endpoint} connected (${this.successfulConnectsCount} total reconnects)`); } const websocketId = this.connection.websocket.__id; @@ -8934,7 +9817,7 @@ class Connector extends Eev { // but sometimes we also get an open websocket after rpc timeout (not sure but this code handles it anyway, should be no problem, only better for all cases) if ( this.connection.websocket.__id == websocketId && - this.connection.websocket.readyState == wsOPEN + this.connection.websocket.readyState == wsOPEN$1 ) { //⚠️ we only show if it seems still relevant, special case // previously we had this first log output above this if statement @@ -9118,7 +10001,7 @@ function determineEndpoint({ endpoint, host, port }) { const browser$1 = typeof window !== 'undefined'; const wsCONNECTING = 0; -const wsOPEN$1 = 1; +const wsOPEN$2 = 1; //const wsCLOSING = 2; //const wsCLOSED = 3; @@ -9132,6 +10015,22 @@ const CONN_IDLE_TICKS = 3; // how long to wait for a new websocket to connect... after this we cancel it const WAIT_FOR_NEW_CONN_TICKS = 5; // 5000 ms ( = (5) * CONN_CHECK_INTERVAL ) +function addListener(name, callback, ws) { + if (browser$1) { + ws.addEventListener(name, callback); + } else { + ws.on(name, callback); + } +} + +function removeListener(name, callback, ws) { + if (browser$1) { + ws.removeEventListener(name, callback); + } else { + ws.off(name, callback); + } +} + //todo: remove 'dummy' argument once legacyLib with old MCS is history function establishAndMaintainConnection( { @@ -9171,7 +10070,8 @@ function establishAndMaintainConnection( connector.connection = { terminate() { this.websocket._removeAllCallbacks(); - this.websocket.close(); + this.websocket.__closed = true; + this.websocket.close(); // might take some time to actually close, we can get stray messages through that websocket //connector.connectStatus(undefined); connector.connectStatus(false); reconnect(); @@ -9205,14 +10105,14 @@ function checkConnection({ connector, reconnect, log }) { // decommissioned logger.yellow( log, - `${connector.endpoint} Connection decommisioned, closing websocket ${conn.websocket.__id}, will not retry again ` + `${connector.endpoint} Connection decommisioned, closing websocket #${conn.websocket.__id}, will not retry again ` ); decommission(connector); } else { // idle connection connector.emit('inactive_connection'); - logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection`); + logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection #${conn.websocket.__id}`); } conn.terminate(); @@ -9254,6 +10154,8 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb return; } + const wsId = Math.round(10 ** 5 * Math.random()).toString(); + //logger.write(log, `${endpoint} CONN_TICK`); //logger.write(log, `${endpoint} wsReadyState ${conn.currentlyTryingWS?.readyState}`); @@ -9269,9 +10171,10 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); } else if (verbose || browser$1) { - logger.write(log, `${endpoint} Created new websocket`); + logger.write(log, `${endpoint} Created new websocket #${wsId}`); } // so in case when device is online but websocket server is not running we usually @@ -9282,7 +10185,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb // (see above)... and we try with a new websocket every 4800ms again instead on every tick (800ms) const ws = new WebSocket(endpoint); - ws.__id = Math.random(); + ws.__id = wsId; conn.currentlyTryingWS = ws; conn.currentlyTryingWS._waitForConnectCounter = 0; @@ -9302,7 +10205,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } if (verbose || browser$1) { - logger.write(log, `${endpoint} Websocket open`); + logger.write(log, `${endpoint} Websocket #${wsId} open`); } conn.currentlyTryingWS = null; @@ -9315,14 +10218,14 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb }; ws._removeAllCallbacks = () => { - ws.removeEventListener('open', openCallback); + // logger.red( + // log, + // `${connector.endpoint} removing 1 callback (open) on ws #${ws.__id} [ ${connector.protocol} ]` + // ); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('open', openCallback); - } else { - ws.on('open', openCallback); - } + addListener('open', openCallback, ws); } function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, verbose }) { @@ -9339,7 +10242,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; const closeCallback = () => { - logger.write(log, `${connector.endpoint} ✖ Connection closed`); + //❗❗❗❗ -- can get stray messages even here!! after close callback ws implementation lets a few (one) messages through!! + // this only happened on LAN ... + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+167ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 ✖ Connection #28485 [ dmt ] closed' + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+01ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 Created new websocket #17068' + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+338ms) ∞ 1.0.0.1 consecutiveUnresolvedTimeout after 2x unresolved promise + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+43ms) ∞ lanServerConn — "ws://192.168.0.10:7780 connection #28485 [ dmt ] received msg '��\x19X���9�߈�V^L�#�b��)\x02�\r��n\x06^?U�v�\x00�ͻ>����k~�A(^�\t�İP�=���X*���'" + // maybe not needed anymore after listeners issue was fixed ..... + ws.__closed = true; + + logger.blue(log, `${connector.endpoint} ✖ Connection #${ws.__id} [ ${connector.protocol} ] closed`); if (connector.decommissioned) { connector.connectStatus(false); @@ -9352,6 +10264,7 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v // flip side is that there is such small delay between when we stop some process and when red x appears... but it's quite ok! // we do however disable all commands immediately ... so: show red X when connect status is FALSE excusively and disable all gui actions when it's NOT TRUE (false or undefined) connector.connectStatus(undefined); + reconnect(); //setTimeout(reconnect, MAX_RECONNECT_DELAY_AFTER_WS_CLOSE * Math.random()); // turns out we don't really need to do these delays, works fine without }; @@ -9365,11 +10278,26 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v const msg = browser$1 ? _msg.data : _msg; + if (ws.__closed) { + // if (msg != 'pong') { + // logger.red( + // log, + // `${connector.endpoint} Already closed connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + // } + return; + } + if (msg == 'pong') { connector.emit('pong'); return; } + // logger.red( + // log, + // `${connector.endpoint} connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + let jsonData; try { @@ -9385,22 +10313,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; ws._removeAllCallbacks = () => { - ws.removeEventListener('error', errorCallback); - ws.removeEventListener('close', closeCallback); - ws.removeEventListener('message', messageCallback); - - ws.removeEventListener('open', openCallback); + // logger.red(log, `${connector.endpoint} removing 4 callbacks on ws #${ws.__id} [ ${connector.protocol} ]`); + removeListener('error', errorCallback, ws); + removeListener('close', closeCallback, ws); + removeListener('message', messageCallback, ws); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('error', errorCallback); - ws.addEventListener('close', closeCallback); - ws.addEventListener('message', messageCallback); - } else { - ws.on('error', errorCallback); - ws.on('close', closeCallback); - ws.on('message', messageCallback); - } + addListener('error', errorCallback, ws); + addListener('close', closeCallback, ws); + addListener('message', messageCallback, ws); } function decommission(connector) { @@ -9408,21 +10330,23 @@ function decommission(connector) { if (conn.currentlyTryingWS) { conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); conn.currentlyTryingWS = null; } - if (conn.ws) { - conn.ws._removeAllCallbacks(); - conn.ws.close(); - conn.ws = null; + if (conn.websocket) { + conn.websocket._removeAllCallbacks(); + conn.websocket.__closed = true; + conn.websocket.close(); + conn.websocket = null; } connector.connectStatus(false); } function socketConnected(conn) { - return conn.websocket && conn.websocket.readyState == wsOPEN$1; + return conn.websocket && conn.websocket.readyState == wsOPEN$2 && !conn.websocket.__closed; // when terminating connection, might be useful -- check } function connectionIdle(conn) { diff --git a/core/node/connectome/node_modules/.package-lock.json b/core/node/connectome/node_modules/.package-lock.json index 5d224f0dc..ddb021d29 100644 --- a/core/node/connectome/node_modules/.package-lock.json +++ b/core/node/connectome/node_modules/.package-lock.json @@ -1,7 +1,7 @@ { "name": "connectome", - "version": "0.2.7", - "lockfileVersion": 2, + "version": "0.2.10", + "lockfileVersion": 3, "requires": true, "packages": { "node_modules/@rollup/plugin-commonjs": { @@ -385,11 +385,23 @@ "dev": true }, "node_modules/ws": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", - "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } } } diff --git a/core/node/connectome/node_modules/bufferutil/build/Makefile b/core/node/connectome/node_modules/bufferutil/build/Makefile index b61cadfb6..071a7ba03 100644 --- a/core/node/connectome/node_modules/bufferutil/build/Makefile +++ b/core/node/connectome/node_modules/bufferutil/build/Makefile @@ -47,6 +47,7 @@ CXXFLAGS.target ?= $(CPPFLAGS) $(CXXFLAGS) LINK.target ?= $(LINK) LDFLAGS.target ?= $(LDFLAGS) AR.target ?= $(AR) +PLI.target ?= pli # C++ apps need to be linked with g++. LINK ?= $(CXX.target) @@ -60,6 +61,7 @@ CXXFLAGS.host ?= $(CPPFLAGS_host) $(CXXFLAGS_host) LINK.host ?= $(CXX.host) LDFLAGS.host ?= $(LDFLAGS_host) AR.host ?= ar +PLI.host ?= pli # Define a dir function that can handle spaces. # http://www.gnu.org/software/make/manual/make.html#Syntax-of-Functions @@ -161,6 +163,9 @@ quiet_cmd_copy = COPY $@ # send stderr to /dev/null to ignore messages when linking directories. cmd_copy = ln -f "$<" "$@" 2>/dev/null || (rm -rf "$@" && cp -af "$<" "$@") +quiet_cmd_symlink = SYMLINK $@ +cmd_symlink = ln -sf "$<" "$@" + quiet_cmd_alink = LIBTOOL-STATIC $@ cmd_alink = rm -f $@ && ./gyp-mac-tool filter-libtool libtool $(GYP_LIBTOOLFLAGS) -static -o $@ $(filter %.o,$^) @@ -326,8 +331,8 @@ ifeq ($(strip $(foreach prefix,$(NO_LOAD),\ endif quiet_cmd_regen_makefile = ACTION Regenerating $@ -cmd_regen_makefile = cd $(srcdir); /Users/david/n/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/david/Library/Caches/node-gyp/19.0.0" "-Dnode_gyp_dir=/Users/david/n/lib/node_modules/npm/node_modules/node-gyp" "-Dnode_lib_file=/Users/david/Library/Caches/node-gyp/19.0.0/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/david/.dmt/core/node/connectome/node_modules/bufferutil" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/david/.dmt/core/node/connectome/node_modules/bufferutil/build/config.gypi -I/Users/david/n/lib/node_modules/npm/node_modules/node-gyp/addon.gypi -I/Users/david/Library/Caches/node-gyp/19.0.0/include/node/common.gypi "--toplevel-dir=." binding.gyp -Makefile: $(srcdir)/binding.gyp $(srcdir)/../../../../../../n/lib/node_modules/npm/node_modules/node-gyp/addon.gypi $(srcdir)/../../../../../../Library/Caches/node-gyp/19.0.0/include/node/common.gypi $(srcdir)/build/config.gypi +cmd_regen_makefile = cd $(srcdir); /Users/david/n/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/david/Library/Caches/node-gyp/19.7.0" "-Dnode_gyp_dir=/Users/david/n/lib/node_modules/npm/node_modules/node-gyp" "-Dnode_lib_file=/Users/david/Library/Caches/node-gyp/19.7.0/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/david/.dmt/core/node/connectome/node_modules/bufferutil" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/david/.dmt/core/node/connectome/node_modules/bufferutil/build/config.gypi -I/Users/david/n/lib/node_modules/npm/node_modules/node-gyp/addon.gypi -I/Users/david/Library/Caches/node-gyp/19.7.0/include/node/common.gypi "--toplevel-dir=." binding.gyp +Makefile: $(srcdir)/../../../../../../n/lib/node_modules/npm/node_modules/node-gyp/addon.gypi $(srcdir)/binding.gyp $(srcdir)/../../../../../../Library/Caches/node-gyp/19.7.0/include/node/common.gypi $(srcdir)/build/config.gypi $(call do_cmd,regen_makefile) # "all" is a concatenation of the "all" targets from all the included diff --git a/core/node/connectome/node_modules/bufferutil/build/Release/.deps/Release/obj.target/bufferutil/src/bufferutil.o.d b/core/node/connectome/node_modules/bufferutil/build/Release/.deps/Release/obj.target/bufferutil/src/bufferutil.o.d index 4a301a1d2..5574d6632 100644 --- a/core/node/connectome/node_modules/bufferutil/build/Release/.deps/Release/obj.target/bufferutil/src/bufferutil.o.d +++ b/core/node/connectome/node_modules/bufferutil/build/Release/.deps/Release/obj.target/bufferutil/src/bufferutil.o.d @@ -1,11 +1,11 @@ -cmd_Release/obj.target/bufferutil/src/bufferutil.o := cc -o Release/obj.target/bufferutil/src/bufferutil.o ../src/bufferutil.c '-DNODE_GYP_MODULE_NAME=bufferutil' '-DUSING_UV_SHARED=1' '-DUSING_V8_SHARED=1' '-DV8_DEPRECATION_WARNINGS=1' '-DV8_DEPRECATION_WARNINGS' '-DV8_IMMINENT_DEPRECATION_WARNINGS' '-D_GLIBCXX_USE_CXX11_ABI=1' '-D_DARWIN_USE_64_BIT_INODE=1' '-D_LARGEFILE_SOURCE' '-D_FILE_OFFSET_BITS=64' '-DOPENSSL_NO_PINSHARED' '-DOPENSSL_THREADS' '-DBUILDING_NODE_EXTENSION' -I/Users/david/Library/Caches/node-gyp/19.0.0/include/node -I/Users/david/Library/Caches/node-gyp/19.0.0/src -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/openssl/config -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/openssl/openssl/include -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/uv/include -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/zlib -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/v8/include -O3 -gdwarf-2 -mmacosx-version-min=10.15 -arch arm64 -Wall -Wendif-labels -W -Wno-unused-parameter -fno-strict-aliasing -MMD -MF ./Release/.deps/Release/obj.target/bufferutil/src/bufferutil.o.d.raw -c +cmd_Release/obj.target/bufferutil/src/bufferutil.o := cc -o Release/obj.target/bufferutil/src/bufferutil.o ../src/bufferutil.c '-DNODE_GYP_MODULE_NAME=bufferutil' '-DUSING_UV_SHARED=1' '-DUSING_V8_SHARED=1' '-DV8_DEPRECATION_WARNINGS=1' '-DV8_DEPRECATION_WARNINGS' '-DV8_IMMINENT_DEPRECATION_WARNINGS' '-D_GLIBCXX_USE_CXX11_ABI=1' '-D_DARWIN_USE_64_BIT_INODE=1' '-D_LARGEFILE_SOURCE' '-D_FILE_OFFSET_BITS=64' '-DOPENSSL_NO_PINSHARED' '-DOPENSSL_THREADS' '-DBUILDING_NODE_EXTENSION' -I/Users/david/Library/Caches/node-gyp/19.7.0/include/node -I/Users/david/Library/Caches/node-gyp/19.7.0/src -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/openssl/config -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/openssl/openssl/include -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/uv/include -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/zlib -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/v8/include -O3 -gdwarf-2 -mmacosx-version-min=10.15 -arch arm64 -Wall -Wendif-labels -W -Wno-unused-parameter -fno-strict-aliasing -MMD -MF ./Release/.deps/Release/obj.target/bufferutil/src/bufferutil.o.d.raw -c Release/obj.target/bufferutil/src/bufferutil.o: ../src/bufferutil.c \ - /Users/david/Library/Caches/node-gyp/19.0.0/include/node/node_api.h \ - /Users/david/Library/Caches/node-gyp/19.0.0/include/node/js_native_api.h \ - /Users/david/Library/Caches/node-gyp/19.0.0/include/node/js_native_api_types.h \ - /Users/david/Library/Caches/node-gyp/19.0.0/include/node/node_api_types.h + /Users/david/Library/Caches/node-gyp/19.7.0/include/node/node_api.h \ + /Users/david/Library/Caches/node-gyp/19.7.0/include/node/js_native_api.h \ + /Users/david/Library/Caches/node-gyp/19.7.0/include/node/js_native_api_types.h \ + /Users/david/Library/Caches/node-gyp/19.7.0/include/node/node_api_types.h ../src/bufferutil.c: -/Users/david/Library/Caches/node-gyp/19.0.0/include/node/node_api.h: -/Users/david/Library/Caches/node-gyp/19.0.0/include/node/js_native_api.h: -/Users/david/Library/Caches/node-gyp/19.0.0/include/node/js_native_api_types.h: -/Users/david/Library/Caches/node-gyp/19.0.0/include/node/node_api_types.h: +/Users/david/Library/Caches/node-gyp/19.7.0/include/node/node_api.h: +/Users/david/Library/Caches/node-gyp/19.7.0/include/node/js_native_api.h: +/Users/david/Library/Caches/node-gyp/19.7.0/include/node/js_native_api_types.h: +/Users/david/Library/Caches/node-gyp/19.7.0/include/node/node_api_types.h: diff --git a/core/node/connectome/node_modules/bufferutil/build/Release/bufferutil.node b/core/node/connectome/node_modules/bufferutil/build/Release/bufferutil.node index 499277c2f9d6ef017ea30b049ad791981c586b2f..b1c1772ab22958d9243775dd59d43122a3d9d5a3 100755 GIT binary patch delta 52 zcmV-40L%Zpn*+R?1F(p~1mHS2WV4LI5DOq!S-`w2$F;Wf^B7S3n*$8HPo&GR77F|5 KDuic)eVj*@WEn>Q delta 52 zcmV-40L%Zpn*+R?1F(p~1jQ2BW3!CH5DOq#1FB{CSf9gxlCs4PETI_NAeCYVI z>g?W#%fg~gRvCr~sn%)DirGh3xzPbx%iJ25PdH}~om^)a5v+$@i%=KtG>nzNp`^<8 z7paM4PRW#t24BN?1%|g8ndT*Y&VpxETb}~FKJPr`fAEH7}f7SE|99-skHF}QBx6@_Dd9qvgfrXAEH6M#Ny|d^Ux&&H8YQ4ee z0cfj91E)y4v5^|myiU!pfXB!eYdVWH-A%gWG_&(C%U05$rayo_N?NYz!${dpx_*um z4}f-%-ld}-fcB6!o^n!Uwzq}t+xnfda6w{-?FRNLe1XjkY#PSWaXF!{F%gUEzCx3l z(F%KN5_B1qtMe2Zb-wp~K=(+8K&0s{0xp8c93Uhv~FKBift8 z)@n1cTIjV-%Z-IQHloF<>=uc>n914#omJUsZ9Eg(37u8hY3t8*0y9~$Dp$+JFN|7Vq?(9oci*@`1@$FssMe=TZd1!5#Rypw$CsV zXn&3jtIXn=`II>fx_9P#><$@=nH-b*T%hB*z!I(yh9~8YApbw+7 zTt#IlMuNgkkyl7uA2|2|be5Ei$6c`KZ3FAw{DgZ8Amqvdcp*99KIy=pF!{Ye7JV~- z5y|I`V0@!+eYW5ygzGy3&L0U2&zSiiX?}xT6bR2qf~RYiiP@h)Pl$#5Ap9QyUwRvTZHW zhbY?`MMcHL(WVcn$)6A?)r1A5DiRT)si?t%55$TUT+ts&BS`#VJ!kH>3l7XZbI$k8 z>wKHZ#E3dJqDD7WUp~}x|sCUzm$(X+H*Ri`I~*#;DgnvAA=fR^?!9pUx1ntwaWIR&+F2X zPOq=gXSJeEw^)_~DWM-OS??WK<3|q{&LS6^(6fk6w_8>t&cmq*^o5O<)dbv?Q-RWi zdOsJgo>y5b*oqwIdwgEU$|bGt89*=;%u~LG&w21bjr*>VEBqZTYer6_GM^*h1eA?b zrB*QhaS4?KgcUnMKQ;6i?7Y!_HT?!zXq(Th>}k?lc7s-)K<+it-~y&r{sj6rse6La zL(s}_QKx*8G=u%4q!B|GL1U!L4E-8sswZ7O#pDQRIu_CVx5fNx({(H_ND|b3dRQ+?Bde#VGg@Fw5$qR zs0V5z*{|qef5E{8tYRGOHx9N2#le1Yu+ccUeHLl((=rAgF$NAJ)fhNn44efu25vJ3 zmWRZIohJGxsPXfmUSnClR16(7G*c!XJ!(w&IV=`EV=QuS5Q~nR=vGi;(JPZWUFQXj z9Z&AleRZ+mUS9dRt2$Gc$bQF*ju$R^5VOoh$IV4Q0DXeV<2L-M^vdNtrwn}r)LiDI zxy)tIr#%w1AmD+!Wo1Ih2D`<0T3&G4HV(iMB6YbYx>^k)8j!xCbc7*Xm zVOQRAmKN zKI_W3E4%T>NWNS0`BzBZ4P-Ng2l5?3d%&+YMjf(%sU7%h!cE1QDcsZ#{B7Z^JU2Kb z+|&^KQf_eVI+dcO4AZI@5RK+h;!j>AI!@`Ro@-8*@@*!|>!s#~Qa<2h3wl*c`X&~H zjL(KlZo/dev/null || (rm -rf "$@" && cp -af "$<" "$@") +quiet_cmd_symlink = SYMLINK $@ +cmd_symlink = ln -sf "$<" "$@" + quiet_cmd_alink = LIBTOOL-STATIC $@ cmd_alink = rm -f $@ && ./gyp-mac-tool filter-libtool libtool $(GYP_LIBTOOLFLAGS) -static -o $@ $(filter %.o,$^) @@ -326,8 +331,8 @@ ifeq ($(strip $(foreach prefix,$(NO_LOAD),\ endif quiet_cmd_regen_makefile = ACTION Regenerating $@ -cmd_regen_makefile = cd $(srcdir); /Users/david/n/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/david/Library/Caches/node-gyp/19.0.0" "-Dnode_gyp_dir=/Users/david/n/lib/node_modules/npm/node_modules/node-gyp" "-Dnode_lib_file=/Users/david/Library/Caches/node-gyp/19.0.0/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/david/.dmt/core/node/connectome/node_modules/utf-8-validate" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/david/.dmt/core/node/connectome/node_modules/utf-8-validate/build/config.gypi -I/Users/david/n/lib/node_modules/npm/node_modules/node-gyp/addon.gypi -I/Users/david/Library/Caches/node-gyp/19.0.0/include/node/common.gypi "--toplevel-dir=." binding.gyp -Makefile: $(srcdir)/binding.gyp $(srcdir)/build/config.gypi $(srcdir)/../../../../../../n/lib/node_modules/npm/node_modules/node-gyp/addon.gypi $(srcdir)/../../../../../../Library/Caches/node-gyp/19.0.0/include/node/common.gypi +cmd_regen_makefile = cd $(srcdir); /Users/david/n/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/david/Library/Caches/node-gyp/19.7.0" "-Dnode_gyp_dir=/Users/david/n/lib/node_modules/npm/node_modules/node-gyp" "-Dnode_lib_file=/Users/david/Library/Caches/node-gyp/19.7.0/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/david/.dmt/core/node/connectome/node_modules/utf-8-validate" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/david/.dmt/core/node/connectome/node_modules/utf-8-validate/build/config.gypi -I/Users/david/n/lib/node_modules/npm/node_modules/node-gyp/addon.gypi -I/Users/david/Library/Caches/node-gyp/19.7.0/include/node/common.gypi "--toplevel-dir=." binding.gyp +Makefile: $(srcdir)/../../../../../../n/lib/node_modules/npm/node_modules/node-gyp/addon.gypi $(srcdir)/../../../../../../Library/Caches/node-gyp/19.7.0/include/node/common.gypi $(srcdir)/binding.gyp $(srcdir)/build/config.gypi $(call do_cmd,regen_makefile) # "all" is a concatenation of the "all" targets from all the included diff --git a/core/node/connectome/node_modules/utf-8-validate/build/Release/.deps/Release/obj.target/validation/src/validation.o.d b/core/node/connectome/node_modules/utf-8-validate/build/Release/.deps/Release/obj.target/validation/src/validation.o.d index 48633684e..c5439f6c1 100644 --- a/core/node/connectome/node_modules/utf-8-validate/build/Release/.deps/Release/obj.target/validation/src/validation.o.d +++ b/core/node/connectome/node_modules/utf-8-validate/build/Release/.deps/Release/obj.target/validation/src/validation.o.d @@ -1,11 +1,11 @@ -cmd_Release/obj.target/validation/src/validation.o := cc -o Release/obj.target/validation/src/validation.o ../src/validation.c '-DNODE_GYP_MODULE_NAME=validation' '-DUSING_UV_SHARED=1' '-DUSING_V8_SHARED=1' '-DV8_DEPRECATION_WARNINGS=1' '-DV8_DEPRECATION_WARNINGS' '-DV8_IMMINENT_DEPRECATION_WARNINGS' '-D_GLIBCXX_USE_CXX11_ABI=1' '-D_DARWIN_USE_64_BIT_INODE=1' '-D_LARGEFILE_SOURCE' '-D_FILE_OFFSET_BITS=64' '-DOPENSSL_NO_PINSHARED' '-DOPENSSL_THREADS' '-DBUILDING_NODE_EXTENSION' -I/Users/david/Library/Caches/node-gyp/19.0.0/include/node -I/Users/david/Library/Caches/node-gyp/19.0.0/src -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/openssl/config -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/openssl/openssl/include -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/uv/include -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/zlib -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/v8/include -O3 -gdwarf-2 -mmacosx-version-min=10.15 -arch arm64 -Wall -Wendif-labels -W -Wno-unused-parameter -fno-strict-aliasing -MMD -MF ./Release/.deps/Release/obj.target/validation/src/validation.o.d.raw -c +cmd_Release/obj.target/validation/src/validation.o := cc -o Release/obj.target/validation/src/validation.o ../src/validation.c '-DNODE_GYP_MODULE_NAME=validation' '-DUSING_UV_SHARED=1' '-DUSING_V8_SHARED=1' '-DV8_DEPRECATION_WARNINGS=1' '-DV8_DEPRECATION_WARNINGS' '-DV8_IMMINENT_DEPRECATION_WARNINGS' '-D_GLIBCXX_USE_CXX11_ABI=1' '-D_DARWIN_USE_64_BIT_INODE=1' '-D_LARGEFILE_SOURCE' '-D_FILE_OFFSET_BITS=64' '-DOPENSSL_NO_PINSHARED' '-DOPENSSL_THREADS' '-DBUILDING_NODE_EXTENSION' -I/Users/david/Library/Caches/node-gyp/19.7.0/include/node -I/Users/david/Library/Caches/node-gyp/19.7.0/src -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/openssl/config -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/openssl/openssl/include -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/uv/include -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/zlib -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/v8/include -O3 -gdwarf-2 -mmacosx-version-min=10.15 -arch arm64 -Wall -Wendif-labels -W -Wno-unused-parameter -fno-strict-aliasing -MMD -MF ./Release/.deps/Release/obj.target/validation/src/validation.o.d.raw -c Release/obj.target/validation/src/validation.o: ../src/validation.c \ - /Users/david/Library/Caches/node-gyp/19.0.0/include/node/node_api.h \ - /Users/david/Library/Caches/node-gyp/19.0.0/include/node/js_native_api.h \ - /Users/david/Library/Caches/node-gyp/19.0.0/include/node/js_native_api_types.h \ - /Users/david/Library/Caches/node-gyp/19.0.0/include/node/node_api_types.h + /Users/david/Library/Caches/node-gyp/19.7.0/include/node/node_api.h \ + /Users/david/Library/Caches/node-gyp/19.7.0/include/node/js_native_api.h \ + /Users/david/Library/Caches/node-gyp/19.7.0/include/node/js_native_api_types.h \ + /Users/david/Library/Caches/node-gyp/19.7.0/include/node/node_api_types.h ../src/validation.c: -/Users/david/Library/Caches/node-gyp/19.0.0/include/node/node_api.h: -/Users/david/Library/Caches/node-gyp/19.0.0/include/node/js_native_api.h: -/Users/david/Library/Caches/node-gyp/19.0.0/include/node/js_native_api_types.h: -/Users/david/Library/Caches/node-gyp/19.0.0/include/node/node_api_types.h: +/Users/david/Library/Caches/node-gyp/19.7.0/include/node/node_api.h: +/Users/david/Library/Caches/node-gyp/19.7.0/include/node/js_native_api.h: +/Users/david/Library/Caches/node-gyp/19.7.0/include/node/js_native_api_types.h: +/Users/david/Library/Caches/node-gyp/19.7.0/include/node/node_api_types.h: diff --git a/core/node/connectome/node_modules/utf-8-validate/build/Release/obj.target/validation/src/validation.o b/core/node/connectome/node_modules/utf-8-validate/build/Release/obj.target/validation/src/validation.o index 0792e31fff4131a25d19f7e406220b5d31615eb6..db831b67ea6d6eb819a8d2c609a1a409c3a4c4a0 100644 GIT binary patch delta 1133 zcmZ8fO-vI(7~R?FZXwXZ($a2=q&+k-q9VaVAx5gG7%o&34n(6(iA6M}C8!Y((wGo~ zl8AK-@jy^6#&{r*g#$M|Xn?a3;=z`iiJtIah(GatyHgKN^6i`V-ZwLErhU<~-t+i` zu@_YAF`zlPS3I9&1I2~sY+amWNmdp`Rx;MO<398__(j}PtBlqYR)viRS>~q$ZV65A zFXQtRf)>%|thh1Hqq+3xwNx&j%8cL4jOEhV)Zne5q0HEgR63i@-_BgIKKP5Q*N%W2 z_Fz2?`zk=MW?j)5B21q?k7^dFg$K(UZj#5e@mR zeWBls#YMQOlf|w6rYtWcoml!=iANh#gxTM8nj6hu-Hp$PxerYUL+O^l^(1pO}!;b zF=bQ=9vZ=e(t|B~sTc90*pu!-(32L-twJxN2Nl1!JL$m(v)}yQ_h#nJ?7PwSXlul9 zQkol6eovj%$qBMnpR7|`R%n8@G9~9&(mSKqnBeVQI9n8 zw3C!aL@Bmnc$iaer%%p`lCkw)IxJs`vhh)6chw$w8TOUa+i`F0SG7ent!doYIeFPv zNUtTlbW!!X50_(EZozG2HLV8HF-(koa6hzdlfN0F7xul#4%M~bAWn=S@5Y`wp61rD zsqmAI1d-cD2$vRq6N&f?iVwM1Ls>$O4m<~WM{Y(_^>3K*Aahf>pAM>iBF}>& ze0J|?d`J@u$QbBI&jc4S?a5(IqKRn5bRg}+g-IZV)rr5w|0qoBPZb3q% zRp#gIQRAVoe0=P_y0N}ZHLsl70F&6O>gH?5=GGZSB;4~pRHbE+n9nwc=)hG|2tgZL4c zmgF1rVuasaJ(!A2+N)4r9r9aRt_6vMh`dO)? YbXn55dZ)H diff --git a/core/node/connectome/node_modules/utf-8-validate/build/Release/validation.node b/core/node/connectome/node_modules/utf-8-validate/build/Release/validation.node index b0559a657eb3ac7aad555d934cc86eef812c295d..26ba5e771212424d2d2d05ced12441d35def7a0c 100755 GIT binary patch delta 52 zcmV-40L%aEk^}6L1F*os1mHS2WV6JB}TS$8Fa K?4KO!s`?GT!x}*V delta 52 zcmV-40L%aEk^}6L1F*os1jQ2BW3$A;hzB50k^tA@CJ{lv9tnRtTV9U*>#xs(F`kKf KP7sM+QazcSiWpM> diff --git a/core/node/connectome/node_modules/utf-8-validate/build/config.gypi b/core/node/connectome/node_modules/utf-8-validate/build/config.gypi index ca5f2eb6c..b3dc02373 100644 --- a/core/node/connectome/node_modules/utf-8-validate/build/config.gypi +++ b/core/node/connectome/node_modules/utf-8-validate/build/config.gypi @@ -10,6 +10,7 @@ "xcode_configuration_platform": "arm64" }, "variables": { + "arm_fpu": "neon", "asan": 0, "coverage": "false", "dcheck_always_on": 0, @@ -21,12 +22,12 @@ "error_on_warn": "false", "force_dynamic_crt": 0, "host_arch": "arm64", - "icu_data_in": "../../deps/icu-tmp/icudt71l.dat", + "icu_data_in": "../../deps/icu-tmp/icudt72l.dat", "icu_endianness": "l", "icu_gyp_path": "tools/icu/icu-generic.gyp", "icu_path": "deps/icu-small", "icu_small": "false", - "icu_ver_major": "71", + "icu_ver_major": "72", "is_debug": 0, "libdir": "lib", "llvm_version": "12.0", @@ -39,6 +40,7 @@ "node_byteorder": "little", "node_debug_lib": "false", "node_enable_d8": "false", + "node_enable_v8_vtunejit": "false", "node_fipsinstall": "false", "node_install_corepack": "true", "node_install_npm": "true", @@ -83,7 +85,6 @@ "lib/internal/assert.js", "lib/internal/assert/assertion_error.js", "lib/internal/assert/calltracker.js", - "lib/internal/assert/snapshot.js", "lib/internal/async_hooks.js", "lib/internal/blob.js", "lib/internal/blocklist.js", @@ -138,6 +139,7 @@ "lib/internal/error_serdes.js", "lib/internal/errors.js", "lib/internal/event_target.js", + "lib/internal/file.js", "lib/internal/fixed_queue.js", "lib/internal/freelist.js", "lib/internal/freeze_intrinsics.js", @@ -146,6 +148,7 @@ "lib/internal/fs/dir.js", "lib/internal/fs/promises.js", "lib/internal/fs/read_file_context.js", + "lib/internal/fs/recursive_watch.js", "lib/internal/fs/rimraf.js", "lib/internal/fs/streams.js", "lib/internal/fs/sync_write_stream.js", @@ -172,10 +175,11 @@ "lib/internal/main/prof_process.js", "lib/internal/main/repl.js", "lib/internal/main/run_main_module.js", + "lib/internal/main/single_executable_application.js", "lib/internal/main/test_runner.js", "lib/internal/main/watch_mode.js", "lib/internal/main/worker_thread.js", - "lib/internal/modules/cjs/helpers.js", + "lib/internal/mime.js", "lib/internal/modules/cjs/loader.js", "lib/internal/modules/esm/assert.js", "lib/internal/modules/esm/create_dynamic_module.js", @@ -191,6 +195,8 @@ "lib/internal/modules/esm/package_config.js", "lib/internal/modules/esm/resolve.js", "lib/internal/modules/esm/translators.js", + "lib/internal/modules/esm/utils.js", + "lib/internal/modules/helpers.js", "lib/internal/modules/package_json_reader.js", "lib/internal/modules/run_main.js", "lib/internal/net.js", @@ -260,11 +266,20 @@ "lib/internal/structured_clone.js", "lib/internal/test/binding.js", "lib/internal/test/transfer.js", + "lib/internal/test_runner/coverage.js", "lib/internal/test_runner/harness.js", + "lib/internal/test_runner/mock.js", + "lib/internal/test_runner/reporter/dot.js", + "lib/internal/test_runner/reporter/spec.js", + "lib/internal/test_runner/reporter/tap.js", "lib/internal/test_runner/runner.js", - "lib/internal/test_runner/tap_stream.js", + "lib/internal/test_runner/tap_checker.js", + "lib/internal/test_runner/tap_lexer.js", + "lib/internal/test_runner/tap_parser.js", "lib/internal/test_runner/test.js", + "lib/internal/test_runner/tests_stream.js", "lib/internal/test_runner/utils.js", + "lib/internal/test_runner/yaml_to_js.js", "lib/internal/timers.js", "lib/internal/tls/secure-context.js", "lib/internal/tls/secure-pair.js", @@ -285,6 +300,7 @@ "lib/internal/v8_prof_polyfill.js", "lib/internal/v8_prof_processor.js", "lib/internal/validators.js", + "lib/internal/vm.js", "lib/internal/vm/module.js", "lib/internal/wasm_web_api.js", "lib/internal/watch_mode/files_watcher.js", @@ -363,6 +379,7 @@ "openssl_quic": "true", "ossfuzz": "false", "shlib_suffix": "111.dylib", + "single_executable_application": "true", "target_arch": "arm64", "v8_enable_31bit_smis_on_64bit_arch": 0, "v8_enable_gdbjit": 0, @@ -383,7 +400,7 @@ "v8_use_siphash": 1, "want_separate_host_toolset": 0, "xcode_version": "12.0", - "nodedir": "/Users/david/Library/Caches/node-gyp/19.0.0", + "nodedir": "/Users/david/Library/Caches/node-gyp/19.7.0", "standalone_static_library": 1, "userconfig": "/Users/david/.npmrc", "cache": "/Users/david/.npm", @@ -391,7 +408,7 @@ "globalconfig": "/Users/david/n/etc/npmrc", "init_module": "/Users/david/.npm-init.js", "prefix": "/Users/david/n", - "user_agent": "npm/8.19.2 node/v19.0.0 darwin arm64 workspaces/false", + "user_agent": "npm/9.5.0 node/v19.7.0 darwin arm64 workspaces/false", "metrics_registry": "https://registry.npmjs.org/", "node_gyp": "/Users/david/n/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js", "global_prefix": "/Users/david/n" diff --git a/core/node/connectome/node_modules/utf-8-validate/build/validation.target.mk b/core/node/connectome/node_modules/utf-8-validate/build/validation.target.mk index a9e41a5fe..3aec1eebc 100644 --- a/core/node/connectome/node_modules/utf-8-validate/build/validation.target.mk +++ b/core/node/connectome/node_modules/utf-8-validate/build/validation.target.mk @@ -50,13 +50,13 @@ CFLAGS_OBJC_Debug := CFLAGS_OBJCC_Debug := INCS_Debug := \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/include/node \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/src \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/openssl/config \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/openssl/openssl/include \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/uv/include \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/zlib \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/v8/include + -I/Users/david/Library/Caches/node-gyp/19.7.0/include/node \ + -I/Users/david/Library/Caches/node-gyp/19.7.0/src \ + -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/openssl/config \ + -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/openssl/openssl/include \ + -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/uv/include \ + -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/zlib \ + -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/v8/include DEFS_Release := \ '-DNODE_GYP_MODULE_NAME=validation' \ @@ -103,13 +103,13 @@ CFLAGS_OBJC_Release := CFLAGS_OBJCC_Release := INCS_Release := \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/include/node \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/src \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/openssl/config \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/openssl/openssl/include \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/uv/include \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/zlib \ - -I/Users/david/Library/Caches/node-gyp/19.0.0/deps/v8/include + -I/Users/david/Library/Caches/node-gyp/19.7.0/include/node \ + -I/Users/david/Library/Caches/node-gyp/19.7.0/src \ + -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/openssl/config \ + -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/openssl/openssl/include \ + -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/uv/include \ + -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/zlib \ + -I/Users/david/Library/Caches/node-gyp/19.7.0/deps/v8/include OBJS := \ $(obj).target/$(TARGET)/src/validation.o diff --git a/core/node/connectome/node_modules/ws/LICENSE b/core/node/connectome/node_modules/ws/LICENSE index a145cd1df..1da5b96a1 100644 --- a/core/node/connectome/node_modules/ws/LICENSE +++ b/core/node/connectome/node_modules/ws/LICENSE @@ -1,21 +1,20 @@ -The MIT License (MIT) - Copyright (c) 2011 Einar Otto Stangvik +Copyright (c) 2013 Arnout Kazemier and contributors +Copyright (c) 2016 Luigi Pinca and contributors -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/core/node/connectome/node_modules/ws/README.md b/core/node/connectome/node_modules/ws/README.md index 9c6e5287c..a550ca1c7 100644 --- a/core/node/connectome/node_modules/ws/README.md +++ b/core/node/connectome/node_modules/ws/README.md @@ -1,9 +1,8 @@ # ws: a Node.js WebSocket library [![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws) -[![Build](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=build&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) -[![Windows x86 Build](https://img.shields.io/appveyor/ci/lpinca/ws/master.svg?logo=appveyor)](https://ci.appveyor.com/project/lpinca/ws) -[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg)](https://coveralls.io/github/websockets/ws) +[![CI](https://img.shields.io/github/actions/workflow/status/websockets/ws/ci.yml?branch=master&label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) +[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws) ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and server implementation. @@ -23,7 +22,7 @@ can use one of the many wrappers available on npm, like - [Protocol support](#protocol-support) - [Installing](#installing) - - [Opt-in for performance and spec compliance](#opt-in-for-performance-and-spec-compliance) + - [Opt-in for performance](#opt-in-for-performance) - [API docs](#api-docs) - [WebSocket compression](#websocket-compression) - [Usage examples](#usage-examples) @@ -34,7 +33,7 @@ can use one of the many wrappers available on npm, like - [Multiple servers sharing a single HTTP/S server](#multiple-servers-sharing-a-single-https-server) - [Client authentication](#client-authentication) - [Server broadcast](#server-broadcast) - - [echo.websocket.org demo](#echowebsocketorg-demo) + - [Round-trip time](#round-trip-time) - [Use the Node.js streams API](#use-the-nodejs-streams-api) - [Other examples](#other-examples) - [FAQ](#faq) @@ -59,9 +58,9 @@ npm install ws ### Opt-in for performance There are 2 optional modules that can be installed along side with the ws -module. These modules are binary addons which improve certain operations. -Prebuilt binaries are available for the most popular platforms so you don't -necessarily need to have a C++ compiler installed on your machine. +module. These modules are binary addons that improve the performance of certain +operations. Prebuilt binaries are available for the most popular platforms so +you don't necessarily need to have a C++ compiler installed on your machine. - `npm install --save-optional bufferutil`: Allows to efficiently perform operations such as masking and unmasking the data payload of the WebSocket @@ -69,6 +68,17 @@ necessarily need to have a C++ compiler installed on your machine. - `npm install --save-optional utf-8-validate`: Allows to efficiently check if a message contains valid UTF-8. +To not even try to require and use these modules, use the +[`WS_NO_BUFFER_UTIL`](./doc/ws.md#ws_no_buffer_util) and +[`WS_NO_UTF_8_VALIDATE`](./doc/ws.md#ws_no_utf_8_validate) environment +variables. These might be useful to enhance security in systems where a user can +put a package in the package search path of an application of another user, due +to how the Node.js resolver algorithm works. + +The `utf-8-validate` module is not needed and is not required, even if it is +already installed, regardless of the value of the `WS_NO_UTF_8_VALIDATE` +environment variable, if [`buffer.isUtf8()`][] is available. + ## API docs See [`/doc/ws.md`](./doc/ws.md) for Node.js-like documentation of ws classes and @@ -98,9 +108,9 @@ into the creation of [raw deflate/inflate streams][node-zlib-deflaterawdocs]. See [the docs][ws-server-options] for more options. ```js -const WebSocket = require('ws'); +import WebSocket, { WebSocketServer } from 'ws'; -const wss = new WebSocket.Server({ +const wss = new WebSocketServer({ port: 8080, perMessageDeflate: { zlibDeflateOptions: { @@ -119,7 +129,7 @@ const wss = new WebSocket.Server({ // Below options specified as default values. concurrencyLimit: 10, // Limits zlib concurrency for perf. threshold: 1024 // Size (in bytes) below which messages - // should not be compressed. + // should not be compressed if context takeover is disabled. } }); ``` @@ -129,7 +139,7 @@ server. To always disable the extension on the client set the `perMessageDeflate` option to `false`. ```js -const WebSocket = require('ws'); +import WebSocket from 'ws'; const ws = new WebSocket('ws://www.host.com/path', { perMessageDeflate: false @@ -141,26 +151,30 @@ const ws = new WebSocket('ws://www.host.com/path', { ### Sending and receiving text data ```js -const WebSocket = require('ws'); +import WebSocket from 'ws'; const ws = new WebSocket('ws://www.host.com/path'); +ws.on('error', console.error); + ws.on('open', function open() { ws.send('something'); }); -ws.on('message', function incoming(data) { - console.log(data); +ws.on('message', function message(data) { + console.log('received: %s', data); }); ``` ### Sending binary data ```js -const WebSocket = require('ws'); +import WebSocket from 'ws'; const ws = new WebSocket('ws://www.host.com/path'); +ws.on('error', console.error); + ws.on('open', function open() { const array = new Float32Array(5); @@ -175,13 +189,15 @@ ws.on('open', function open() { ### Simple server ```js -const WebSocket = require('ws'); +import { WebSocketServer } from 'ws'; -const wss = new WebSocket.Server({ port: 8080 }); +const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { - ws.on('message', function incoming(message) { - console.log('received: %s', message); + ws.on('error', console.error); + + ws.on('message', function message(data) { + console.log('received: %s', data); }); ws.send('something'); @@ -191,19 +207,21 @@ wss.on('connection', function connection(ws) { ### External HTTP/S server ```js -const fs = require('fs'); -const https = require('https'); -const WebSocket = require('ws'); +import { createServer } from 'https'; +import { readFileSync } from 'fs'; +import { WebSocketServer } from 'ws'; -const server = https.createServer({ - cert: fs.readFileSync('/path/to/cert.pem'), - key: fs.readFileSync('/path/to/key.pem') +const server = createServer({ + cert: readFileSync('/path/to/cert.pem'), + key: readFileSync('/path/to/key.pem') }); -const wss = new WebSocket.Server({ server }); +const wss = new WebSocketServer({ server }); wss.on('connection', function connection(ws) { - ws.on('message', function incoming(message) { - console.log('received: %s', message); + ws.on('error', console.error); + + ws.on('message', function message(data) { + console.log('received: %s', data); }); ws.send('something'); @@ -215,24 +233,28 @@ server.listen(8080); ### Multiple servers sharing a single HTTP/S server ```js -const http = require('http'); -const WebSocket = require('ws'); -const url = require('url'); +import { createServer } from 'http'; +import { parse } from 'url'; +import { WebSocketServer } from 'ws'; -const server = http.createServer(); -const wss1 = new WebSocket.Server({ noServer: true }); -const wss2 = new WebSocket.Server({ noServer: true }); +const server = createServer(); +const wss1 = new WebSocketServer({ noServer: true }); +const wss2 = new WebSocketServer({ noServer: true }); wss1.on('connection', function connection(ws) { + ws.on('error', console.error); + // ... }); wss2.on('connection', function connection(ws) { + ws.on('error', console.error); + // ... }); server.on('upgrade', function upgrade(request, socket, head) { - const pathname = url.parse(request.url).pathname; + const { pathname } = parse(request.url); if (pathname === '/foo') { wss1.handleUpgrade(request, socket, head, function done(ws) { @@ -253,27 +275,37 @@ server.listen(8080); ### Client authentication ```js -const http = require('http'); -const WebSocket = require('ws'); +import { createServer } from 'http'; +import { WebSocketServer } from 'ws'; -const server = http.createServer(); -const wss = new WebSocket.Server({ noServer: true }); +function onSocketError(err) { + console.error(err); +} + +const server = createServer(); +const wss = new WebSocketServer({ noServer: true }); wss.on('connection', function connection(ws, request, client) { - ws.on('message', function message(msg) { - console.log(`Received message ${msg} from user ${client}`); + ws.on('error', console.error); + + ws.on('message', function message(data) { + console.log(`Received message ${data} from user ${client}`); }); }); server.on('upgrade', function upgrade(request, socket, head) { + socket.on('error', onSocketError); + // This function is not defined on purpose. Implement it with your own logic. - authenticate(request, (err, client) => { + authenticate(request, function next(err, client) { if (err || !client) { socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); socket.destroy(); return; } + socket.removeListener('error', onSocketError); + wss.handleUpgrade(request, socket, head, function done(ws) { wss.emit('connection', ws, request, client); }); @@ -291,15 +323,17 @@ A client WebSocket broadcasting to all connected WebSocket clients, including itself. ```js -const WebSocket = require('ws'); +import WebSocket, { WebSocketServer } from 'ws'; -const wss = new WebSocket.Server({ port: 8080 }); +const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { - ws.on('message', function incoming(data) { + ws.on('error', console.error); + + ws.on('message', function message(data, isBinary) { wss.clients.forEach(function each(client) { if (client.readyState === WebSocket.OPEN) { - client.send(data); + client.send(data, { binary: isBinary }); } }); }); @@ -310,29 +344,31 @@ A client WebSocket broadcasting to every other connected WebSocket clients, excluding itself. ```js -const WebSocket = require('ws'); +import WebSocket, { WebSocketServer } from 'ws'; -const wss = new WebSocket.Server({ port: 8080 }); +const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { - ws.on('message', function incoming(data) { + ws.on('error', console.error); + + ws.on('message', function message(data, isBinary) { wss.clients.forEach(function each(client) { if (client !== ws && client.readyState === WebSocket.OPEN) { - client.send(data); + client.send(data, { binary: isBinary }); } }); }); }); ``` -### echo.websocket.org demo +### Round-trip time ```js -const WebSocket = require('ws'); +import WebSocket from 'ws'; -const ws = new WebSocket('wss://echo.websocket.org/', { - origin: 'https://websocket.org' -}); +const ws = new WebSocket('wss://websocket-echo.com/'); + +ws.on('error', console.error); ws.on('open', function open() { console.log('connected'); @@ -343,8 +379,8 @@ ws.on('close', function close() { console.log('disconnected'); }); -ws.on('message', function incoming(data) { - console.log(`Roundtrip time: ${Date.now() - data} ms`); +ws.on('message', function message(data) { + console.log(`Round-trip time: ${Date.now() - data} ms`); setTimeout(function timeout() { ws.send(Date.now()); @@ -355,13 +391,13 @@ ws.on('message', function incoming(data) { ### Use the Node.js streams API ```js -const WebSocket = require('ws'); +import WebSocket, { createWebSocketStream } from 'ws'; -const ws = new WebSocket('wss://echo.websocket.org/', { - origin: 'https://websocket.org' -}); +const ws = new WebSocket('wss://websocket-echo.com/'); + +const duplex = createWebSocketStream(ws, { encoding: 'utf8' }); -const duplex = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }); +duplex.on('error', console.error); duplex.pipe(process.stdout); process.stdin.pipe(duplex); @@ -381,12 +417,14 @@ Otherwise, see the test cases. The remote IP address can be obtained from the raw socket. ```js -const WebSocket = require('ws'); +import { WebSocketServer } from 'ws'; -const wss = new WebSocket.Server({ port: 8080 }); +const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws, req) { const ip = req.socket.remoteAddress; + + ws.on('error', console.error); }); ``` @@ -395,7 +433,9 @@ the `X-Forwarded-For` header. ```js wss.on('connection', function connection(ws, req) { - const ip = req.headers['x-forwarded-for'].split(/\s*,\s*/)[0]; + const ip = req.headers['x-forwarded-for'].split(',')[0].trim(); + + ws.on('error', console.error); }); ``` @@ -409,18 +449,17 @@ In these cases ping messages can be used as a means to verify that the remote endpoint is still responsive. ```js -const WebSocket = require('ws'); - -function noop() {} +import { WebSocketServer } from 'ws'; function heartbeat() { this.isAlive = true; } -const wss = new WebSocket.Server({ port: 8080 }); +const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function connection(ws) { ws.isAlive = true; + ws.on('error', console.error); ws.on('pong', heartbeat); }); @@ -429,7 +468,7 @@ const interval = setInterval(function ping() { if (ws.isAlive === false) return ws.terminate(); ws.isAlive = false; - ws.ping(noop); + ws.ping(); }); }, 30000); @@ -446,7 +485,7 @@ without knowing it. You might want to add a ping listener on your clients to prevent that. A simple implementation would be: ```js -const WebSocket = require('ws'); +import WebSocket from 'ws'; function heartbeat() { clearTimeout(this.pingTimeout); @@ -460,8 +499,9 @@ function heartbeat() { }, 30000 + 1000); } -const client = new WebSocket('wss://echo.websocket.org/'); +const client = new WebSocket('wss://websocket-echo.com/'); +client.on('error', console.error); client.on('open', heartbeat); client.on('ping', heartbeat); client.on('close', function clear() { @@ -482,6 +522,7 @@ We're using the GitHub [releases][changelog] for changelog entries. [MIT](LICENSE) +[`buffer.isutf8()`]: https://nodejs.org/api/buffer.html#bufferisutf8input [changelog]: https://github.com/websockets/ws/releases [client-report]: http://websockets.github.io/ws/autobahn/clients/ [https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent @@ -492,5 +533,4 @@ We're using the GitHub [releases][changelog] for changelog entries. [server-report]: http://websockets.github.io/ws/autobahn/servers/ [session-parse-example]: ./examples/express-session-parse [socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent -[ws-server-options]: - https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback +[ws-server-options]: ./doc/ws.md#new-websocketserveroptions-callback diff --git a/core/node/connectome/node_modules/ws/index.js b/core/node/connectome/node_modules/ws/index.js index 722c78676..41edb3b81 100644 --- a/core/node/connectome/node_modules/ws/index.js +++ b/core/node/connectome/node_modules/ws/index.js @@ -7,4 +7,7 @@ WebSocket.Server = require('./lib/websocket-server'); WebSocket.Receiver = require('./lib/receiver'); WebSocket.Sender = require('./lib/sender'); +WebSocket.WebSocket = WebSocket; +WebSocket.WebSocketServer = WebSocket.Server; + module.exports = WebSocket; diff --git a/core/node/connectome/node_modules/ws/lib/buffer-util.js b/core/node/connectome/node_modules/ws/lib/buffer-util.js index 6fd84c311..f7536e28e 100644 --- a/core/node/connectome/node_modules/ws/lib/buffer-util.js +++ b/core/node/connectome/node_modules/ws/lib/buffer-util.js @@ -2,6 +2,8 @@ const { EMPTY_BUFFER } = require('./constants'); +const FastBuffer = Buffer[Symbol.species]; + /** * Merges an array of buffers into a new buffer. * @@ -23,7 +25,9 @@ function concat(list, totalLength) { offset += buf.length; } - if (offset < totalLength) return target.slice(0, offset); + if (offset < totalLength) { + return new FastBuffer(target.buffer, target.byteOffset, offset); + } return target; } @@ -52,9 +56,7 @@ function _mask(source, mask, output, offset, length) { * @public */ function _unmask(buffer, mask) { - // Required until https://github.com/nodejs/node/issues/9006 is resolved. - const length = buffer.length; - for (let i = 0; i < length; i++) { + for (let i = 0; i < buffer.length; i++) { buffer[i] ^= mask[i & 3]; } } @@ -67,11 +69,11 @@ function _unmask(buffer, mask) { * @public */ function toArrayBuffer(buf) { - if (buf.byteLength === buf.buffer.byteLength) { + if (buf.length === buf.buffer.byteLength) { return buf.buffer; } - return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length); } /** @@ -90,9 +92,9 @@ function toBuffer(data) { let buf; if (data instanceof ArrayBuffer) { - buf = Buffer.from(data); + buf = new FastBuffer(data); } else if (ArrayBuffer.isView(data)) { - buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); + buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength); } else { buf = Buffer.from(data); toBuffer.readOnly = false; @@ -101,29 +103,29 @@ function toBuffer(data) { return buf; } -try { - const bufferUtil = require('bufferutil'); - const bu = bufferUtil.BufferUtil || bufferUtil; +module.exports = { + concat, + mask: _mask, + toArrayBuffer, + toBuffer, + unmask: _unmask +}; - module.exports = { - concat, - mask(source, mask, output, offset, length) { +/* istanbul ignore else */ +if (!process.env.WS_NO_BUFFER_UTIL) { + try { + const bufferUtil = require('bufferutil'); + + module.exports.mask = function (source, mask, output, offset, length) { if (length < 48) _mask(source, mask, output, offset, length); - else bu.mask(source, mask, output, offset, length); - }, - toArrayBuffer, - toBuffer, - unmask(buffer, mask) { + else bufferUtil.mask(source, mask, output, offset, length); + }; + + module.exports.unmask = function (buffer, mask) { if (buffer.length < 32) _unmask(buffer, mask); - else bu.unmask(buffer, mask); - } - }; -} catch (e) /* istanbul ignore next */ { - module.exports = { - concat, - mask: _mask, - toArrayBuffer, - toBuffer, - unmask: _unmask - }; + else bufferUtil.unmask(buffer, mask); + }; + } catch (e) { + // Continue regardless of the error. + } } diff --git a/core/node/connectome/node_modules/ws/lib/constants.js b/core/node/connectome/node_modules/ws/lib/constants.js index 4082981f8..d691b30a1 100644 --- a/core/node/connectome/node_modules/ws/lib/constants.js +++ b/core/node/connectome/node_modules/ws/lib/constants.js @@ -2,9 +2,11 @@ module.exports = { BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], + EMPTY_BUFFER: Buffer.alloc(0), GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + kForOnEventAttribute: Symbol('kIsForOnEventAttribute'), + kListener: Symbol('kListener'), kStatusCode: Symbol('status-code'), kWebSocket: Symbol('websocket'), - EMPTY_BUFFER: Buffer.alloc(0), NOOP: () => {} }; diff --git a/core/node/connectome/node_modules/ws/lib/event-target.js b/core/node/connectome/node_modules/ws/lib/event-target.js index a6fbe72b7..fea4cbc52 100644 --- a/core/node/connectome/node_modules/ws/lib/event-target.js +++ b/core/node/connectome/node_modules/ws/lib/event-target.js @@ -1,111 +1,172 @@ 'use strict'; +const { kForOnEventAttribute, kListener } = require('./constants'); + +const kCode = Symbol('kCode'); +const kData = Symbol('kData'); +const kError = Symbol('kError'); +const kMessage = Symbol('kMessage'); +const kReason = Symbol('kReason'); +const kTarget = Symbol('kTarget'); +const kType = Symbol('kType'); +const kWasClean = Symbol('kWasClean'); + /** * Class representing an event. - * - * @private */ class Event { /** * Create a new `Event`. * * @param {String} type The name of the event - * @param {Object} target A reference to the target to which the event was - * dispatched + * @throws {TypeError} If the `type` argument is not specified */ - constructor(type, target) { - this.target = target; - this.type = type; + constructor(type) { + this[kTarget] = null; + this[kType] = type; } -} -/** - * Class representing a message event. - * - * @extends Event - * @private - */ -class MessageEvent extends Event { /** - * Create a new `MessageEvent`. - * - * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @type {*} */ - constructor(data, target) { - super('message', target); + get target() { + return this[kTarget]; + } - this.data = data; + /** + * @type {String} + */ + get type() { + return this[kType]; } } +Object.defineProperty(Event.prototype, 'target', { enumerable: true }); +Object.defineProperty(Event.prototype, 'type', { enumerable: true }); + /** * Class representing a close event. * * @extends Event - * @private */ class CloseEvent extends Event { /** * Create a new `CloseEvent`. * - * @param {Number} code The status code explaining why the connection is being - * closed - * @param {String} reason A human-readable string explaining why the - * connection is closing - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {Number} [options.code=0] The status code explaining why the + * connection was closed + * @param {String} [options.reason=''] A human-readable string explaining why + * the connection was closed + * @param {Boolean} [options.wasClean=false] Indicates whether or not the + * connection was cleanly closed */ - constructor(code, reason, target) { - super('close', target); + constructor(type, options = {}) { + super(type); - this.wasClean = target._closeFrameReceived && target._closeFrameSent; - this.reason = reason; - this.code = code; + this[kCode] = options.code === undefined ? 0 : options.code; + this[kReason] = options.reason === undefined ? '' : options.reason; + this[kWasClean] = options.wasClean === undefined ? false : options.wasClean; + } + + /** + * @type {Number} + */ + get code() { + return this[kCode]; + } + + /** + * @type {String} + */ + get reason() { + return this[kReason]; + } + + /** + * @type {Boolean} + */ + get wasClean() { + return this[kWasClean]; } } +Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true }); + /** - * Class representing an open event. + * Class representing an error event. * * @extends Event - * @private */ -class OpenEvent extends Event { +class ErrorEvent extends Event { /** - * Create a new `OpenEvent`. + * Create a new `ErrorEvent`. * - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.error=null] The error that generated this event + * @param {String} [options.message=''] The error message + */ + constructor(type, options = {}) { + super(type); + + this[kError] = options.error === undefined ? null : options.error; + this[kMessage] = options.message === undefined ? '' : options.message; + } + + /** + * @type {*} */ - constructor(target) { - super('open', target); + get error() { + return this[kError]; + } + + /** + * @type {String} + */ + get message() { + return this[kMessage]; } } +Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true }); +Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true }); + /** - * Class representing an error event. + * Class representing a message event. * * @extends Event - * @private */ -class ErrorEvent extends Event { +class MessageEvent extends Event { /** - * Create a new `ErrorEvent`. + * Create a new `MessageEvent`. * - * @param {Object} error The error that generated this event - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.data=null] The message content */ - constructor(error, target) { - super('error', target); + constructor(type, options = {}) { + super(type); + + this[kData] = options.data === undefined ? null : options.data; + } - this.message = error.message; - this.error = error; + /** + * @type {*} + */ + get data() { + return this[kData]; } } +Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true }); + /** * This provides methods for emulating the `EventTarget` interface. It's not * meant to be used directly. @@ -117,49 +178,75 @@ const EventTarget = { * Register an event listener. * * @param {String} type A string representing the event type to listen for - * @param {Function} listener The listener to add + * @param {(Function|Object)} handler The listener to add * @param {Object} [options] An options object specifies characteristics about * the event listener - * @param {Boolean} [options.once=false] A `Boolean`` indicating that the + * @param {Boolean} [options.once=false] A `Boolean` indicating that the * listener should be invoked at most once after being added. If `true`, * the listener would be automatically removed when invoked. * @public */ - addEventListener(type, listener, options) { - if (typeof listener !== 'function') return; - - function onMessage(data) { - listener.call(this, new MessageEvent(data, this)); - } - - function onClose(code, message) { - listener.call(this, new CloseEvent(code, message, this)); - } - - function onError(error) { - listener.call(this, new ErrorEvent(error, this)); - } - - function onOpen() { - listener.call(this, new OpenEvent(this)); + addEventListener(type, handler, options = {}) { + for (const listener of this.listeners(type)) { + if ( + !options[kForOnEventAttribute] && + listener[kListener] === handler && + !listener[kForOnEventAttribute] + ) { + return; + } } - const method = options && options.once ? 'once' : 'on'; + let wrapper; if (type === 'message') { - onMessage._listener = listener; - this[method](type, onMessage); + wrapper = function onMessage(data, isBinary) { + const event = new MessageEvent('message', { + data: isBinary ? data : data.toString() + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'close') { - onClose._listener = listener; - this[method](type, onClose); + wrapper = function onClose(code, message) { + const event = new CloseEvent('close', { + code, + reason: message.toString(), + wasClean: this._closeFrameReceived && this._closeFrameSent + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'error') { - onError._listener = listener; - this[method](type, onError); + wrapper = function onError(error) { + const event = new ErrorEvent('error', { + error, + message: error.message + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'open') { - onOpen._listener = listener; - this[method](type, onOpen); + wrapper = function onOpen() { + const event = new Event('open'); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else { - this[method](type, listener); + return; + } + + wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; + wrapper[kListener] = handler; + + if (options.once) { + this.once(type, wrapper); + } else { + this.on(type, wrapper); } }, @@ -167,18 +254,39 @@ const EventTarget = { * Remove an event listener. * * @param {String} type A string representing the event type to remove - * @param {Function} listener The listener to remove + * @param {(Function|Object)} handler The listener to remove * @public */ - removeEventListener(type, listener) { - const listeners = this.listeners(type); - - for (let i = 0; i < listeners.length; i++) { - if (listeners[i] === listener || listeners[i]._listener === listener) { - this.removeListener(type, listeners[i]); + removeEventListener(type, handler) { + for (const listener of this.listeners(type)) { + if (listener[kListener] === handler && !listener[kForOnEventAttribute]) { + this.removeListener(type, listener); + break; } } } }; -module.exports = EventTarget; +module.exports = { + CloseEvent, + ErrorEvent, + Event, + EventTarget, + MessageEvent +}; + +/** + * Call an event listener + * + * @param {(Function|Object)} listener The listener to call + * @param {*} thisArg The value to use as `this`` when calling the listener + * @param {Event} event The event to pass to the listener + * @private + */ +function callListener(listener, thisArg, event) { + if (typeof listener === 'object' && listener.handleEvent) { + listener.handleEvent.call(listener, event); + } else { + listener.call(thisArg, event); + } +} diff --git a/core/node/connectome/node_modules/ws/lib/extension.js b/core/node/connectome/node_modules/ws/lib/extension.js index 87a421329..3d7895c1b 100644 --- a/core/node/connectome/node_modules/ws/lib/extension.js +++ b/core/node/connectome/node_modules/ws/lib/extension.js @@ -1,27 +1,6 @@ 'use strict'; -// -// Allowed token characters: -// -// '!', '#', '$', '%', '&', ''', '*', '+', '-', -// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' -// -// tokenChars[32] === 0 // ' ' -// tokenChars[33] === 1 // '!' -// tokenChars[34] === 0 // '"' -// ... -// -// prettier-ignore -const tokenChars = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 - 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 -]; +const { tokenChars } = require('./validation'); /** * Adds an offer to the map of extension offers or a parameter to the map of @@ -47,9 +26,6 @@ function push(dest, name, elem) { */ function parse(header) { const offers = Object.create(null); - - if (header === undefined || header === '') return offers; - let params = Object.create(null); let mustUnescape = false; let isEscaping = false; @@ -57,16 +33,20 @@ function parse(header) { let extensionName; let paramName; let start = -1; + let code = -1; let end = -1; let i = 0; for (; i < header.length; i++) { - const code = header.charCodeAt(i); + code = header.charCodeAt(i); if (extensionName === undefined) { if (end === -1 && tokenChars[code] === 1) { if (start === -1) start = i; - } else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) { + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { if (end === -1 && start !== -1) end = i; } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { if (start === -1) { @@ -167,7 +147,7 @@ function parse(header) { } } - if (start === -1 || inQuotes) { + if (start === -1 || inQuotes || code === 0x20 || code === 0x09) { throw new SyntaxError('Unexpected end of input'); } diff --git a/core/node/connectome/node_modules/ws/lib/permessage-deflate.js b/core/node/connectome/node_modules/ws/lib/permessage-deflate.js index a8974b988..77d918b55 100644 --- a/core/node/connectome/node_modules/ws/lib/permessage-deflate.js +++ b/core/node/connectome/node_modules/ws/lib/permessage-deflate.js @@ -4,8 +4,9 @@ const zlib = require('zlib'); const bufferUtil = require('./buffer-util'); const Limiter = require('./limiter'); -const { kStatusCode, NOOP } = require('./constants'); +const { kStatusCode } = require('./constants'); +const FastBuffer = Buffer[Symbol.species]; const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); const kPerMessageDeflate = Symbol('permessage-deflate'); const kTotalLength = Symbol('total-length'); @@ -30,22 +31,22 @@ class PerMessageDeflate { * Creates a PerMessageDeflate instance. * * @param {Object} [options] Configuration options - * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept - * disabling of server context takeover + * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support + * for, or request, a custom client window size * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ * acknowledge disabling of client context takeover + * @param {Number} [options.concurrencyLimit=10] The number of concurrent + * calls to zlib * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the * use of a custom server window size - * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support - * for, or request, a custom client window size + * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept + * disabling of server context takeover + * @param {Number} [options.threshold=1024] Size (in bytes) below which + * messages should not be compressed if context takeover is disabled * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on * deflate * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on * inflate - * @param {Number} [options.threshold=1024] Size (in bytes) below which - * messages should not be compressed - * @param {Number} [options.concurrencyLimit=10] The number of concurrent - * calls to zlib * @param {Boolean} [isServer=false] Create the instance in either server or * client mode * @param {Number} [maxPayload=0] The maximum allowed message length @@ -313,7 +314,7 @@ class PerMessageDeflate { /** * Compress data. Concurrency limited. * - * @param {Buffer} data Data to compress + * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @public @@ -395,7 +396,7 @@ class PerMessageDeflate { /** * Compress data. * - * @param {Buffer} data Data to compress + * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @private @@ -418,13 +419,6 @@ class PerMessageDeflate { this._deflate[kTotalLength] = 0; this._deflate[kBuffers] = []; - // - // An `'error'` event is emitted, only on Node.js < 10.0.0, if the - // `zlib.DeflateRaw` instance is closed while data is being processed. - // This can happen if `PerMessageDeflate#cleanup()` is called at the wrong - // time due to an abnormal WebSocket closure. - // - this._deflate.on('error', NOOP); this._deflate.on('data', deflateOnData); } @@ -444,7 +438,9 @@ class PerMessageDeflate { this._deflate[kTotalLength] ); - if (fin) data = data.slice(0, data.length - 4); + if (fin) { + data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4); + } // // Ensure that the callback will not be called again in @@ -495,6 +491,7 @@ function inflateOnData(chunk) { } this[kError] = new RangeError('Max payload size exceeded'); + this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; this[kError][kStatusCode] = 1009; this.removeListener('data', inflateOnData); this.reset(); diff --git a/core/node/connectome/node_modules/ws/lib/receiver.js b/core/node/connectome/node_modules/ws/lib/receiver.js index 65a5ab45f..96f572cb1 100644 --- a/core/node/connectome/node_modules/ws/lib/receiver.js +++ b/core/node/connectome/node_modules/ws/lib/receiver.js @@ -12,6 +12,7 @@ const { const { concat, toArrayBuffer, unmask } = require('./buffer-util'); const { isValidStatusCode, isValidUTF8 } = require('./validation'); +const FastBuffer = Buffer[Symbol.species]; const GET_INFO = 0; const GET_PAYLOAD_LENGTH_16 = 1; const GET_PAYLOAD_LENGTH_64 = 2; @@ -22,26 +23,31 @@ const INFLATING = 5; /** * HyBi Receiver implementation. * - * @extends stream.Writable + * @extends Writable */ class Receiver extends Writable { /** * Creates a Receiver instance. * - * @param {String} [binaryType=nodebuffer] The type for binary data - * @param {Object} [extensions] An object containing the negotiated extensions - * @param {Boolean} [isServer=false] Specifies whether to operate in client or - * server mode - * @param {Number} [maxPayload=0] The maximum allowed message length + * @param {Object} [options] Options object + * @param {String} [options.binaryType=nodebuffer] The type for binary data + * @param {Object} [options.extensions] An object containing the negotiated + * extensions + * @param {Boolean} [options.isServer=false] Specifies whether to operate in + * client or server mode + * @param {Number} [options.maxPayload=0] The maximum allowed message length + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages */ - constructor(binaryType, extensions, isServer, maxPayload) { + constructor(options = {}) { super(); - this._binaryType = binaryType || BINARY_TYPES[0]; + this._binaryType = options.binaryType || BINARY_TYPES[0]; + this._extensions = options.extensions || {}; + this._isServer = !!options.isServer; + this._maxPayload = options.maxPayload | 0; + this._skipUTF8Validation = !!options.skipUTF8Validation; this[kWebSocket] = undefined; - this._extensions = extensions || {}; - this._isServer = !!isServer; - this._maxPayload = maxPayload | 0; this._bufferedBytes = 0; this._buffers = []; @@ -92,8 +98,13 @@ class Receiver extends Writable { if (n < this._buffers[0].length) { const buf = this._buffers[0]; - this._buffers[0] = buf.slice(n); - return buf.slice(0, n); + this._buffers[0] = new FastBuffer( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); + + return new FastBuffer(buf.buffer, buf.byteOffset, n); } const dst = Buffer.allocUnsafe(n); @@ -106,7 +117,11 @@ class Receiver extends Writable { dst.set(this._buffers.shift(), offset); } else { dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); - this._buffers[0] = buf.slice(n); + this._buffers[0] = new FastBuffer( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); } n -= buf.length; @@ -168,14 +183,26 @@ class Receiver extends Writable { if ((buf[0] & 0x30) !== 0x00) { this._loop = false; - return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002); + return error( + RangeError, + 'RSV2 and RSV3 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_2_3' + ); } const compressed = (buf[0] & 0x40) === 0x40; if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } this._fin = (buf[0] & 0x80) === 0x80; @@ -185,45 +212,85 @@ class Receiver extends Writable { if (this._opcode === 0x00) { if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } if (!this._fragmented) { this._loop = false; - return error(RangeError, 'invalid opcode 0', true, 1002); + return error( + RangeError, + 'invalid opcode 0', + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._opcode = this._fragmented; } else if (this._opcode === 0x01 || this._opcode === 0x02) { if (this._fragmented) { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._compressed = compressed; } else if (this._opcode > 0x07 && this._opcode < 0x0b) { if (!this._fin) { this._loop = false; - return error(RangeError, 'FIN must be set', true, 1002); + return error( + RangeError, + 'FIN must be set', + true, + 1002, + 'WS_ERR_EXPECTED_FIN' + ); } if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } - if (this._payloadLength > 0x7d) { + if ( + this._payloadLength > 0x7d || + (this._opcode === 0x08 && this._payloadLength === 1) + ) { this._loop = false; return error( RangeError, `invalid payload length ${this._payloadLength}`, true, - 1002 + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' ); } } else { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } if (!this._fin && !this._fragmented) this._fragmented = this._opcode; @@ -232,11 +299,23 @@ class Receiver extends Writable { if (this._isServer) { if (!this._masked) { this._loop = false; - return error(RangeError, 'MASK must be set', true, 1002); + return error( + RangeError, + 'MASK must be set', + true, + 1002, + 'WS_ERR_EXPECTED_MASK' + ); } } else if (this._masked) { this._loop = false; - return error(RangeError, 'MASK must be clear', true, 1002); + return error( + RangeError, + 'MASK must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_MASK' + ); } if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; @@ -285,7 +364,8 @@ class Receiver extends Writable { RangeError, 'Unsupported WebSocket frame: payload length > 2^53 - 1', false, - 1009 + 1009, + 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' ); } @@ -304,7 +384,13 @@ class Receiver extends Writable { this._totalPayloadLength += this._payloadLength; if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { this._loop = false; - return error(RangeError, 'Max payload size exceeded', false, 1009); + return error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); } } @@ -344,7 +430,13 @@ class Receiver extends Writable { } data = this.consume(this._payloadLength); - if (this._masked) unmask(data, this._mask); + + if ( + this._masked && + (this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0 + ) { + unmask(data, this._mask); + } } if (this._opcode > 0x07) return this.controlMessage(data); @@ -357,7 +449,7 @@ class Receiver extends Writable { if (data.length) { // - // This message is not compressed so its lenght is the sum of the payload + // This message is not compressed so its length is the sum of the payload // length of all fragments. // this._messageLength = this._totalPayloadLength; @@ -384,7 +476,13 @@ class Receiver extends Writable { this._messageLength += buf.length; if (this._messageLength > this._maxPayload && this._maxPayload > 0) { return cb( - error(RangeError, 'Max payload size exceeded', false, 1009) + error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ) ); } @@ -425,16 +523,22 @@ class Receiver extends Writable { data = fragments; } - this.emit('message', data); + this.emit('message', data, true); } else { const buf = concat(fragments, messageLength); - if (!isValidUTF8(buf)) { + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { this._loop = false; - return error(Error, 'invalid UTF-8 sequence', true, 1007); + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } - this.emit('message', buf.toString()); + this.emit('message', buf, false); } } @@ -453,24 +557,38 @@ class Receiver extends Writable { this._loop = false; if (data.length === 0) { - this.emit('conclude', 1005, ''); + this.emit('conclude', 1005, EMPTY_BUFFER); this.end(); - } else if (data.length === 1) { - return error(RangeError, 'invalid payload length 1', true, 1002); } else { const code = data.readUInt16BE(0); if (!isValidStatusCode(code)) { - return error(RangeError, `invalid status code ${code}`, true, 1002); + return error( + RangeError, + `invalid status code ${code}`, + true, + 1002, + 'WS_ERR_INVALID_CLOSE_CODE' + ); } - const buf = data.slice(2); + const buf = new FastBuffer( + data.buffer, + data.byteOffset + 2, + data.length - 2 + ); - if (!isValidUTF8(buf)) { - return error(Error, 'invalid UTF-8 sequence', true, 1007); + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } - this.emit('conclude', code, buf.toString()); + this.emit('conclude', code, buf); this.end(); } } else if (this._opcode === 0x09) { @@ -488,20 +606,22 @@ module.exports = Receiver; /** * Builds an error object. * - * @param {(Error|RangeError)} ErrorCtor The error constructor + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor * @param {String} message The error message * @param {Boolean} prefix Specifies whether or not to add a default prefix to * `message` * @param {Number} statusCode The status code + * @param {String} errorCode The exposed error code * @return {(Error|RangeError)} The error * @private */ -function error(ErrorCtor, message, prefix, statusCode) { +function error(ErrorCtor, message, prefix, statusCode, errorCode) { const err = new ErrorCtor( prefix ? `Invalid WebSocket frame: ${message}` : message ); Error.captureStackTrace(err, error); + err.code = errorCode; err[kStatusCode] = statusCode; return err; } diff --git a/core/node/connectome/node_modules/ws/lib/sender.js b/core/node/connectome/node_modules/ws/lib/sender.js index ad71e1950..c84885362 100644 --- a/core/node/connectome/node_modules/ws/lib/sender.js +++ b/core/node/connectome/node_modules/ws/lib/sender.js @@ -1,5 +1,9 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls$" }] */ + 'use strict'; +const net = require('net'); +const tls = require('tls'); const { randomFillSync } = require('crypto'); const PerMessageDeflate = require('./permessage-deflate'); @@ -7,7 +11,8 @@ const { EMPTY_BUFFER } = require('./constants'); const { isValidStatusCode } = require('./validation'); const { mask: applyMask, toBuffer } = require('./buffer-util'); -const mask = Buffer.alloc(4); +const kByteLength = Symbol('kByteLength'); +const maskBuffer = Buffer.alloc(4); /** * HyBi Sender implementation. @@ -16,11 +21,19 @@ class Sender { /** * Creates a Sender instance. * - * @param {net.Socket} socket The connection socket + * @param {(net.Socket|tls.Socket)} socket The connection socket * @param {Object} [extensions] An object containing the negotiated extensions + * @param {Function} [generateMask] The function used to generate the masking + * key */ - constructor(socket, extensions) { + constructor(socket, extensions, generateMask) { this._extensions = extensions || {}; + + if (generateMask) { + this._generateMask = generateMask; + this._maskBuffer = Buffer.alloc(4); + } + this._socket = socket; this._firstFragment = true; @@ -34,34 +47,71 @@ class Sender { /** * Frames a piece of data according to the HyBi WebSocket protocol. * - * @param {Buffer} data The data to frame + * @param {(Buffer|String)} data The data to frame * @param {Object} options Options object - * @param {Number} options.opcode The opcode - * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be - * modified * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit - * @return {Buffer[]} The framed data as a list of `Buffer` instances + * @return {(Buffer|String)[]} The framed data * @public */ static frame(data, options) { - const merge = options.mask && options.readOnly; - let offset = options.mask ? 6 : 2; - let payloadLength = data.length; + let mask; + let merge = false; + let offset = 2; + let skipMasking = false; + + if (options.mask) { + mask = options.maskBuffer || maskBuffer; - if (data.length >= 65536) { + if (options.generateMask) { + options.generateMask(mask); + } else { + randomFillSync(mask, 0, 4); + } + + skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; + offset = 6; + } + + let dataLength; + + if (typeof data === 'string') { + if ( + (!options.mask || skipMasking) && + options[kByteLength] !== undefined + ) { + dataLength = options[kByteLength]; + } else { + data = Buffer.from(data); + dataLength = data.length; + } + } else { + dataLength = data.length; + merge = options.mask && options.readOnly && !skipMasking; + } + + let payloadLength = dataLength; + + if (dataLength >= 65536) { offset += 8; payloadLength = 127; - } else if (data.length > 125) { + } else if (dataLength > 125) { offset += 2; payloadLength = 126; } - const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); + const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset); target[0] = options.fin ? options.opcode | 0x80 : options.opcode; if (options.rsv1) target[0] |= 0x40; @@ -69,28 +119,28 @@ class Sender { target[1] = payloadLength; if (payloadLength === 126) { - target.writeUInt16BE(data.length, 2); + target.writeUInt16BE(dataLength, 2); } else if (payloadLength === 127) { - target.writeUInt32BE(0, 2); - target.writeUInt32BE(data.length, 6); + target[2] = target[3] = 0; + target.writeUIntBE(dataLength, 4, 6); } if (!options.mask) return [target, data]; - randomFillSync(mask, 0, 4); - target[1] |= 0x80; target[offset - 4] = mask[0]; target[offset - 3] = mask[1]; target[offset - 2] = mask[2]; target[offset - 1] = mask[3]; + if (skipMasking) return [target, data]; + if (merge) { - applyMask(data, mask, target, offset, data.length); + applyMask(data, mask, target, offset, dataLength); return [target]; } - applyMask(data, mask, data, 0, data.length); + applyMask(data, mask, data, 0, dataLength); return [target, data]; } @@ -98,7 +148,7 @@ class Sender { * Sends a close message to the other peer. * * @param {Number} [code] The status code component of the body - * @param {String} [data] The message component of the body + * @param {(String|Buffer)} [data] The message component of the body * @param {Boolean} [mask=false] Specifies whether or not to mask the message * @param {Function} [cb] Callback * @public @@ -110,7 +160,7 @@ class Sender { buf = EMPTY_BUFFER; } else if (typeof code !== 'number' || !isValidStatusCode(code)) { throw new TypeError('First argument must be a valid error code number'); - } else if (data === undefined || data === '') { + } else if (data === undefined || !data.length) { buf = Buffer.allocUnsafe(2); buf.writeUInt16BE(code, 0); } else { @@ -122,37 +172,32 @@ class Sender { buf = Buffer.allocUnsafe(2 + length); buf.writeUInt16BE(code, 0); - buf.write(data, 2); + + if (typeof data === 'string') { + buf.write(data, 2); + } else { + buf.set(data, 2); + } } + const options = { + [kByteLength]: buf.length, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x08, + readOnly: false, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doClose, buf, mask, cb]); + this.enqueue([this.dispatch, buf, false, options, cb]); } else { - this.doClose(buf, mask, cb); + this.sendFrame(Sender.frame(buf, options), cb); } } - /** - * Frames and sends a close message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Function} [cb] Callback - * @private - */ - doClose(data, mask, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x08, - mask, - readOnly: false - }), - cb - ); - } - /** * Sends a ping message to the other peer. * @@ -162,41 +207,40 @@ class Sender { * @public */ ping(data, mask, cb) { - const buf = toBuffer(data); + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } - if (buf.length > 125) { + if (byteLength > 125) { throw new RangeError('The data size must not be greater than 125 bytes'); } + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x09, + readOnly, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]); + this.enqueue([this.dispatch, data, false, options, cb]); } else { - this.doPing(buf, mask, toBuffer.readOnly, cb); + this.sendFrame(Sender.frame(data, options), cb); } } - /** - * Frames and sends a ping message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified - * @param {Function} [cb] Callback - * @private - */ - doPing(data, mask, readOnly, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x09, - mask, - readOnly - }), - cb - ); - } - /** * Sends a pong message to the other peer. * @@ -206,50 +250,49 @@ class Sender { * @public */ pong(data, mask, cb) { - const buf = toBuffer(data); + let byteLength; + let readOnly; - if (buf.length > 125) { + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + + if (byteLength > 125) { throw new RangeError('The data size must not be greater than 125 bytes'); } + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x0a, + readOnly, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]); + this.enqueue([this.dispatch, data, false, options, cb]); } else { - this.doPong(buf, mask, toBuffer.readOnly, cb); + this.sendFrame(Sender.frame(data, options), cb); } } - /** - * Frames and sends a pong message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified - * @param {Function} [cb] Callback - * @private - */ - doPong(data, mask, readOnly, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x0a, - mask, - readOnly - }), - cb - ); - } - /** * Sends a data message to the other peer. * * @param {*} data The message to send * @param {Object} options Options object - * @param {Boolean} [options.compress=false] Specifies whether or not to - * compress `data` * @param {Boolean} [options.binary=false] Specifies whether `data` is binary * or text + * @param {Boolean} [options.compress=false] Specifies whether or not to + * compress `data` * @param {Boolean} [options.fin=false] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask=false] Specifies whether or not to mask @@ -258,15 +301,34 @@ class Sender { * @public */ send(data, options, cb) { - const buf = toBuffer(data); const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; let opcode = options.binary ? 2 : 1; let rsv1 = options.compress; + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + if (this._firstFragment) { this._firstFragment = false; - if (rsv1 && perMessageDeflate) { - rsv1 = buf.length >= perMessageDeflate._threshold; + if ( + rsv1 && + perMessageDeflate && + perMessageDeflate.params[ + perMessageDeflate._isServer + ? 'server_no_context_takeover' + : 'client_no_context_takeover' + ] + ) { + rsv1 = byteLength >= perMessageDeflate._threshold; } this._compress = rsv1; } else { @@ -278,26 +340,32 @@ class Sender { if (perMessageDeflate) { const opts = { + [kByteLength]: byteLength, fin: options.fin, - rsv1, - opcode, + generateMask: this._generateMask, mask: options.mask, - readOnly: toBuffer.readOnly + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1 }; if (this._deflating) { - this.enqueue([this.dispatch, buf, this._compress, opts, cb]); + this.enqueue([this.dispatch, data, this._compress, opts, cb]); } else { - this.dispatch(buf, this._compress, opts, cb); + this.dispatch(data, this._compress, opts, cb); } } else { this.sendFrame( - Sender.frame(buf, { + Sender.frame(data, { + [kByteLength]: byteLength, fin: options.fin, - rsv1: false, - opcode, + generateMask: this._generateMask, mask: options.mask, - readOnly: toBuffer.readOnly + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1: false }), cb ); @@ -305,19 +373,23 @@ class Sender { } /** - * Dispatches a data message. + * Dispatches a message. * - * @param {Buffer} data The message to send + * @param {(Buffer|String)} data The message to send * @param {Boolean} [compress=false] Specifies whether or not to compress * `data` * @param {Object} options Options object - * @param {Number} options.opcode The opcode - * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be - * modified * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit * @param {Function} [cb] Callback @@ -331,7 +403,7 @@ class Sender { const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; - this._bufferedBytes += data.length; + this._bufferedBytes += options[kByteLength]; this._deflating = true; perMessageDeflate.compress(data, options.fin, (_, buf) => { if (this._socket.destroyed) { @@ -342,7 +414,8 @@ class Sender { if (typeof cb === 'function') cb(err); for (let i = 0; i < this._queue.length; i++) { - const callback = this._queue[i][4]; + const params = this._queue[i]; + const callback = params[params.length - 1]; if (typeof callback === 'function') callback(err); } @@ -350,7 +423,7 @@ class Sender { return; } - this._bufferedBytes -= data.length; + this._bufferedBytes -= options[kByteLength]; this._deflating = false; options.readOnly = false; this.sendFrame(Sender.frame(buf, options), cb); @@ -367,7 +440,7 @@ class Sender { while (!this._deflating && this._queue.length) { const params = this._queue.shift(); - this._bufferedBytes -= params[1].length; + this._bufferedBytes -= params[3][kByteLength]; Reflect.apply(params[0], this, params.slice(1)); } } @@ -379,7 +452,7 @@ class Sender { * @private */ enqueue(params) { - this._bufferedBytes += params[1].length; + this._bufferedBytes += params[3][kByteLength]; this._queue.push(params); } diff --git a/core/node/connectome/node_modules/ws/lib/stream.js b/core/node/connectome/node_modules/ws/lib/stream.js index 604cf366b..230734b79 100644 --- a/core/node/connectome/node_modules/ws/lib/stream.js +++ b/core/node/connectome/node_modules/ws/lib/stream.js @@ -5,7 +5,7 @@ const { Duplex } = require('stream'); /** * Emits the `'close'` event on a stream. * - * @param {stream.Duplex} The stream. + * @param {Duplex} stream The stream. * @private */ function emitClose(stream) { @@ -43,25 +43,11 @@ function duplexOnError(err) { * * @param {WebSocket} ws The `WebSocket` to wrap * @param {Object} [options] The options for the `Duplex` constructor - * @return {stream.Duplex} The duplex stream + * @return {Duplex} The duplex stream * @public */ function createWebSocketStream(ws, options) { - let resumeOnReceiverDrain = true; - - function receiverOnDrain() { - if (resumeOnReceiverDrain) ws._socket.resume(); - } - - if (ws.readyState === ws.CONNECTING) { - ws.once('open', function open() { - ws._receiver.removeAllListeners('drain'); - ws._receiver.on('drain', receiverOnDrain); - }); - } else { - ws._receiver.removeAllListeners('drain'); - ws._receiver.on('drain', receiverOnDrain); - } + let terminateOnDestroy = true; const duplex = new Duplex({ ...options, @@ -71,16 +57,26 @@ function createWebSocketStream(ws, options) { writableObjectMode: false }); - ws.on('message', function message(msg) { - if (!duplex.push(msg)) { - resumeOnReceiverDrain = false; - ws._socket.pause(); - } + ws.on('message', function message(msg, isBinary) { + const data = + !isBinary && duplex._readableState.objectMode ? msg.toString() : msg; + + if (!duplex.push(data)) ws.pause(); }); ws.once('error', function error(err) { if (duplex.destroyed) return; + // Prevent `ws.terminate()` from being called by `duplex._destroy()`. + // + // - If the `'error'` event is emitted before the `'open'` event, then + // `ws.terminate()` is a noop as no socket is assigned. + // - Otherwise, the error is re-emitted by the listener of the `'error'` + // event of the `Receiver` object. The listener already closes the + // connection by calling `ws.close()`. This allows a close frame to be + // sent to the other peer. If `ws.terminate()` is called right after this, + // then the close frame might not be sent. + terminateOnDestroy = false; duplex.destroy(err); }); @@ -108,7 +104,8 @@ function createWebSocketStream(ws, options) { if (!called) callback(err); process.nextTick(emitClose, duplex); }); - ws.terminate(); + + if (terminateOnDestroy) ws.terminate(); }; duplex._final = function (callback) { @@ -140,10 +137,7 @@ function createWebSocketStream(ws, options) { }; duplex._read = function () { - if (ws.readyState === ws.OPEN && !resumeOnReceiverDrain) { - resumeOnReceiverDrain = true; - if (!ws._receiver._writableState.needDrain) ws._socket.resume(); - } + if (ws.isPaused) ws.resume(); }; duplex._write = function (chunk, encoding, callback) { diff --git a/core/node/connectome/node_modules/ws/lib/subprotocol.js b/core/node/connectome/node_modules/ws/lib/subprotocol.js new file mode 100644 index 000000000..d4381e886 --- /dev/null +++ b/core/node/connectome/node_modules/ws/lib/subprotocol.js @@ -0,0 +1,62 @@ +'use strict'; + +const { tokenChars } = require('./validation'); + +/** + * Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names. + * + * @param {String} header The field value of the header + * @return {Set} The subprotocol names + * @public + */ +function parse(header) { + const protocols = new Set(); + let start = -1; + let end = -1; + let i = 0; + + for (i; i < header.length; i++) { + const code = header.charCodeAt(i); + + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x2c /* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + + const protocol = header.slice(start, end); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + + if (start === -1 || end !== -1) { + throw new SyntaxError('Unexpected end of input'); + } + + const protocol = header.slice(start, i); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + return protocols; +} + +module.exports = { parse }; diff --git a/core/node/connectome/node_modules/ws/lib/validation.js b/core/node/connectome/node_modules/ws/lib/validation.js index d8693fdb9..c352e6ea7 100644 --- a/core/node/connectome/node_modules/ws/lib/validation.js +++ b/core/node/connectome/node_modules/ws/lib/validation.js @@ -1,5 +1,30 @@ 'use strict'; +const { isUtf8 } = require('buffer'); + +// +// Allowed token characters: +// +// '!', '#', '$', '%', '&', ''', '*', '+', '-', +// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' +// +// tokenChars[32] === 0 // ' ' +// tokenChars[33] === 1 // '!' +// tokenChars[34] === 0 // '"' +// ... +// +// prettier-ignore +const tokenChars = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 +]; + /** * Checks if a status code is allowed in a close frame. * @@ -32,7 +57,7 @@ function _isValidUTF8(buf) { let i = 0; while (i < len) { - if (buf[i] < 0x80) { + if ((buf[i] & 0x80) === 0) { // 0xxxxxxx i++; } else if ((buf[i] & 0xe0) === 0xc0) { @@ -43,9 +68,9 @@ function _isValidUTF8(buf) { (buf[i] & 0xfe) === 0xc0 // Overlong ) { return false; - } else { - i += 2; } + + i += 2; } else if ((buf[i] & 0xf0) === 0xe0) { // 1110xxxx 10xxxxxx 10xxxxxx if ( @@ -56,9 +81,9 @@ function _isValidUTF8(buf) { (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) ) { return false; - } else { - i += 3; } + + i += 3; } else if ((buf[i] & 0xf8) === 0xf0) { // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx if ( @@ -71,9 +96,9 @@ function _isValidUTF8(buf) { buf[i] > 0xf4 // > U+10FFFF ) { return false; - } else { - i += 4; } + + i += 4; } else { return false; } @@ -82,23 +107,24 @@ function _isValidUTF8(buf) { return true; } -try { - let isValidUTF8 = require('utf-8-validate'); +module.exports = { + isValidStatusCode, + isValidUTF8: _isValidUTF8, + tokenChars +}; - /* istanbul ignore if */ - if (typeof isValidUTF8 === 'object') { - isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0 - } - - module.exports = { - isValidStatusCode, - isValidUTF8(buf) { - return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); - } - }; -} catch (e) /* istanbul ignore next */ { - module.exports = { - isValidStatusCode, - isValidUTF8: _isValidUTF8 +if (isUtf8) { + module.exports.isValidUTF8 = function (buf) { + return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf); }; +} /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) { + try { + const isValidUTF8 = require('utf-8-validate'); + + module.exports.isValidUTF8 = function (buf) { + return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf); + }; + } catch (e) { + // Continue regardless of the error. + } } diff --git a/core/node/connectome/node_modules/ws/lib/websocket-server.js b/core/node/connectome/node_modules/ws/lib/websocket-server.js index b99ad050a..bac30eb33 100644 --- a/core/node/connectome/node_modules/ws/lib/websocket-server.js +++ b/core/node/connectome/node_modules/ws/lib/websocket-server.js @@ -1,16 +1,26 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */ + 'use strict'; const EventEmitter = require('events'); +const http = require('http'); +const https = require('https'); +const net = require('net'); +const tls = require('tls'); const { createHash } = require('crypto'); -const { createServer, STATUS_CODES } = require('http'); +const extension = require('./extension'); const PerMessageDeflate = require('./permessage-deflate'); +const subprotocol = require('./subprotocol'); const WebSocket = require('./websocket'); -const { format, parse } = require('./extension'); const { GUID, kWebSocket } = require('./constants'); const keyRegex = /^[+/0-9A-Za-z]{22}==$/; +const RUNNING = 0; +const CLOSING = 1; +const CLOSED = 2; + /** * Class representing a WebSocket server. * @@ -34,8 +44,13 @@ class WebSocketServer extends EventEmitter { * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable * permessage-deflate * @param {Number} [options.port] The port where to bind the server - * @param {http.Server} [options.server] A pre-created HTTP/S server to use + * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S + * server to use + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @param {Function} [options.verifyClient] A hook to reject connections + * @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket` + * class to use. It must be the `WebSocket` class or class that extends it * @param {Function} [callback] A listener for the `listening` event */ constructor(options, callback) { @@ -43,6 +58,7 @@ class WebSocketServer extends EventEmitter { options = { maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: false, handleProtocols: null, clientTracking: true, @@ -53,18 +69,24 @@ class WebSocketServer extends EventEmitter { host: null, path: null, port: null, + WebSocket, ...options }; - if (options.port == null && !options.server && !options.noServer) { + if ( + (options.port == null && !options.server && !options.noServer) || + (options.port != null && (options.server || options.noServer)) || + (options.server && options.noServer) + ) { throw new TypeError( - 'One of the "port", "server", or "noServer" options must be specified' + 'One and only one of the "port", "server", or "noServer" options ' + + 'must be specified' ); } if (options.port != null) { - this._server = createServer((req, res) => { - const body = STATUS_CODES[426]; + this._server = http.createServer((req, res) => { + const body = http.STATUS_CODES[426]; res.writeHead(426, { 'Content-Length': body.length, @@ -95,8 +117,13 @@ class WebSocketServer extends EventEmitter { } if (options.perMessageDeflate === true) options.perMessageDeflate = {}; - if (options.clientTracking) this.clients = new Set(); + if (options.clientTracking) { + this.clients = new Set(); + this._shouldEmitClose = false; + } + this.options = options; + this._state = RUNNING; } /** @@ -118,37 +145,58 @@ class WebSocketServer extends EventEmitter { } /** - * Close the server. + * Stop the server from accepting new connections and emit the `'close'` event + * when all existing connections are closed. * - * @param {Function} [cb] Callback + * @param {Function} [cb] A one-time listener for the `'close'` event * @public */ close(cb) { - if (cb) this.once('close', cb); + if (this._state === CLOSED) { + if (cb) { + this.once('close', () => { + cb(new Error('The server is not running')); + }); + } - // - // Terminate all associated clients. - // - if (this.clients) { - for (const client of this.clients) client.terminate(); + process.nextTick(emitClose, this); + return; } - const server = this._server; + if (cb) this.once('close', cb); + + if (this._state === CLOSING) return; + this._state = CLOSING; + + if (this.options.noServer || this.options.server) { + if (this._server) { + this._removeListeners(); + this._removeListeners = this._server = null; + } + + if (this.clients) { + if (!this.clients.size) { + process.nextTick(emitClose, this); + } else { + this._shouldEmitClose = true; + } + } else { + process.nextTick(emitClose, this); + } + } else { + const server = this._server; - if (server) { this._removeListeners(); this._removeListeners = this._server = null; // - // Close the http server if it was internally created. + // The HTTP/S server was created internally. Close it, and rely on its + // `'close'` event. // - if (this.options.port != null) { - server.close(() => this.emit('close')); - return; - } + server.close(() => { + emitClose(this); + }); } - - process.nextTick(emitClose, this); } /** @@ -173,7 +221,8 @@ class WebSocketServer extends EventEmitter { * Handle a HTTP Upgrade request. * * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @public @@ -181,25 +230,58 @@ class WebSocketServer extends EventEmitter { handleUpgrade(req, socket, head, cb) { socket.on('error', socketOnError); - const key = - req.headers['sec-websocket-key'] !== undefined - ? req.headers['sec-websocket-key'].trim() - : false; + const key = req.headers['sec-websocket-key']; const version = +req.headers['sec-websocket-version']; + + if (req.method !== 'GET') { + const message = 'Invalid HTTP method'; + abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); + return; + } + + if (req.headers.upgrade.toLowerCase() !== 'websocket') { + const message = 'Invalid Upgrade header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (!key || !keyRegex.test(key)) { + const message = 'Missing or invalid Sec-WebSocket-Key header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (version !== 8 && version !== 13) { + const message = 'Missing or invalid Sec-WebSocket-Version header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (!this.shouldHandle(req)) { + abortHandshake(socket, 400); + return; + } + + const secWebSocketProtocol = req.headers['sec-websocket-protocol']; + let protocols = new Set(); + + if (secWebSocketProtocol !== undefined) { + try { + protocols = subprotocol.parse(secWebSocketProtocol); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Protocol header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + } + + const secWebSocketExtensions = req.headers['sec-websocket-extensions']; const extensions = {}; if ( - req.method !== 'GET' || - req.headers.upgrade.toLowerCase() !== 'websocket' || - !key || - !keyRegex.test(key) || - (version !== 8 && version !== 13) || - !this.shouldHandle(req) + this.options.perMessageDeflate && + secWebSocketExtensions !== undefined ) { - return abortHandshake(socket, 400); - } - - if (this.options.perMessageDeflate) { const perMessageDeflate = new PerMessageDeflate( this.options.perMessageDeflate, true, @@ -207,14 +289,17 @@ class WebSocketServer extends EventEmitter { ); try { - const offers = parse(req.headers['sec-websocket-extensions']); + const offers = extension.parse(secWebSocketExtensions); if (offers[PerMessageDeflate.extensionName]) { perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); extensions[PerMessageDeflate.extensionName] = perMessageDeflate; } } catch (err) { - return abortHandshake(socket, 400); + const message = + 'Invalid or unacceptable Sec-WebSocket-Extensions header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; } } @@ -235,7 +320,15 @@ class WebSocketServer extends EventEmitter { return abortHandshake(socket, code || 401, message, headers); } - this.completeUpgrade(key, extensions, req, socket, head, cb); + this.completeUpgrade( + extensions, + key, + protocols, + req, + socket, + head, + cb + ); }); return; } @@ -243,22 +336,24 @@ class WebSocketServer extends EventEmitter { if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); } - this.completeUpgrade(key, extensions, req, socket, head, cb); + this.completeUpgrade(extensions, key, protocols, req, socket, head, cb); } /** * Upgrade the connection to WebSocket. * - * @param {String} key The value of the `Sec-WebSocket-Key` header * @param {Object} extensions The accepted extensions + * @param {String} key The value of the `Sec-WebSocket-Key` header + * @param {Set} protocols The subprotocols * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @throws {Error} If called more than once with the same socket * @private */ - completeUpgrade(key, extensions, req, socket, head, cb) { + completeUpgrade(extensions, key, protocols, req, socket, head, cb) { // // Destroy the socket if the client has already sent a FIN packet. // @@ -271,6 +366,8 @@ class WebSocketServer extends EventEmitter { ); } + if (this._state > RUNNING) return abortHandshake(socket, 503); + const digest = createHash('sha1') .update(key + GUID) .digest('base64'); @@ -282,20 +379,15 @@ class WebSocketServer extends EventEmitter { `Sec-WebSocket-Accept: ${digest}` ]; - const ws = new WebSocket(null); - let protocol = req.headers['sec-websocket-protocol']; - - if (protocol) { - protocol = protocol.trim().split(/ *, */); + const ws = new this.options.WebSocket(null); + if (protocols.size) { // // Optionally call external protocol selection handler. // - if (this.options.handleProtocols) { - protocol = this.options.handleProtocols(protocol, req); - } else { - protocol = protocol[0]; - } + const protocol = this.options.handleProtocols + ? this.options.handleProtocols(protocols, req) + : protocols.values().next().value; if (protocol) { headers.push(`Sec-WebSocket-Protocol: ${protocol}`); @@ -305,7 +397,7 @@ class WebSocketServer extends EventEmitter { if (extensions[PerMessageDeflate.extensionName]) { const params = extensions[PerMessageDeflate.extensionName].params; - const value = format({ + const value = extension.format({ [PerMessageDeflate.extensionName]: [params] }); headers.push(`Sec-WebSocket-Extensions: ${value}`); @@ -320,11 +412,20 @@ class WebSocketServer extends EventEmitter { socket.write(headers.concat('\r\n').join('\r\n')); socket.removeListener('error', socketOnError); - ws.setSocket(socket, head, this.options.maxPayload); + ws.setSocket(socket, head, { + maxPayload: this.options.maxPayload, + skipUTF8Validation: this.options.skipUTF8Validation + }); if (this.clients) { this.clients.add(ws); - ws.on('close', () => this.clients.delete(ws)); + ws.on('close', () => { + this.clients.delete(ws); + + if (this._shouldEmitClose && !this.clients.size) { + process.nextTick(emitClose, this); + } + }); } cb(ws, req); @@ -360,11 +461,12 @@ function addListeners(server, map) { * @private */ function emitClose(server) { + server._state = CLOSED; server.emit('close'); } /** - * Handle premature socket errors. + * Handle socket errors. * * @private */ @@ -375,32 +477,59 @@ function socketOnError() { /** * Close the connection when preconditions are not fulfilled. * - * @param {net.Socket} socket The socket of the upgrade request + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} [message] The HTTP response body * @param {Object} [headers] Additional HTTP response headers * @private */ function abortHandshake(socket, code, message, headers) { - if (socket.writable) { - message = message || STATUS_CODES[code]; - headers = { - Connection: 'close', - 'Content-Type': 'text/html', - 'Content-Length': Buffer.byteLength(message), - ...headers - }; + // + // The socket is writable unless the user destroyed or ended it before calling + // `server.handleUpgrade()` or in the `verifyClient` function, which is a user + // error. Handling this does not make much sense as the worst that can happen + // is that some of the data written by the user might be discarded due to the + // call to `socket.end()` below, which triggers an `'error'` event that in + // turn causes the socket to be destroyed. + // + message = message || http.STATUS_CODES[code]; + headers = { + Connection: 'close', + 'Content-Type': 'text/html', + 'Content-Length': Buffer.byteLength(message), + ...headers + }; - socket.write( - `HTTP/1.1 ${code} ${STATUS_CODES[code]}\r\n` + - Object.keys(headers) - .map((h) => `${h}: ${headers[h]}`) - .join('\r\n') + - '\r\n\r\n' + - message - ); - } + socket.once('finish', socket.destroy); - socket.removeListener('error', socketOnError); - socket.destroy(); + socket.end( + `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + + Object.keys(headers) + .map((h) => `${h}: ${headers[h]}`) + .join('\r\n') + + '\r\n\r\n' + + message + ); +} + +/** + * Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least + * one listener for it, otherwise call `abortHandshake()`. + * + * @param {WebSocketServer} server The WebSocket server + * @param {http.IncomingMessage} req The request object + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} message The HTTP response body + * @private + */ +function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) { + if (server.listenerCount('wsClientError')) { + const err = new Error(message); + Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); + + server.emit('wsClientError', err, socket, req); + } else { + abortHandshake(socket, code, message); + } } diff --git a/core/node/connectome/node_modules/ws/lib/websocket.js b/core/node/connectome/node_modules/ws/lib/websocket.js index 539238190..b2b2b0926 100644 --- a/core/node/connectome/node_modules/ws/lib/websocket.js +++ b/core/node/connectome/node_modules/ws/lib/websocket.js @@ -1,3 +1,5 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Readable$" }] */ + 'use strict'; const EventEmitter = require('events'); @@ -6,6 +8,7 @@ const http = require('http'); const net = require('net'); const tls = require('tls'); const { randomBytes, createHash } = require('crypto'); +const { Readable } = require('stream'); const { URL } = require('url'); const PerMessageDeflate = require('./permessage-deflate'); @@ -15,17 +18,23 @@ const { BINARY_TYPES, EMPTY_BUFFER, GUID, + kForOnEventAttribute, + kListener, kStatusCode, kWebSocket, NOOP } = require('./constants'); -const { addEventListener, removeEventListener } = require('./event-target'); +const { + EventTarget: { addEventListener, removeEventListener } +} = require('./event-target'); const { format, parse } = require('./extension'); const { toBuffer } = require('./buffer-util'); -const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; -const protocolVersions = [8, 13]; const closeTimeout = 30 * 1000; +const kAborted = Symbol('kAborted'); +const protocolVersions = [8, 13]; +const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; +const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/; /** * Class representing a WebSocket. @@ -36,7 +45,7 @@ class WebSocket extends EventEmitter { /** * Create a new `WebSocket`. * - * @param {(String|url.URL)} address The URL to which to connect + * @param {(String|URL)} address The URL to which to connect * @param {(String|String[])} [protocols] The subprotocols * @param {Object} [options] Connection options */ @@ -47,9 +56,10 @@ class WebSocket extends EventEmitter { this._closeCode = 1006; this._closeFrameReceived = false; this._closeFrameSent = false; - this._closeMessage = ''; + this._closeMessage = EMPTY_BUFFER; this._closeTimer = null; this._extensions = {}; + this._paused = false; this._protocol = ''; this._readyState = WebSocket.CONNECTING; this._receiver = null; @@ -61,11 +71,15 @@ class WebSocket extends EventEmitter { this._isServer = false; this._redirects = 0; - if (Array.isArray(protocols)) { - protocols = protocols.join(', '); - } else if (typeof protocols === 'object' && protocols !== null) { - options = protocols; - protocols = undefined; + if (protocols === undefined) { + protocols = []; + } else if (!Array.isArray(protocols)) { + if (typeof protocols === 'object' && protocols !== null) { + options = protocols; + protocols = []; + } else { + protocols = [protocols]; + } } initAsClient(this, address, protocols, options); @@ -112,6 +126,45 @@ class WebSocket extends EventEmitter { return Object.keys(this._extensions).join(); } + /** + * @type {Boolean} + */ + get isPaused() { + return this._paused; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onclose() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onerror() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onopen() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onmessage() { + return null; + } + /** * @type {String} */ @@ -136,20 +189,27 @@ class WebSocket extends EventEmitter { /** * Set up the socket and the internal resources. * - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream - * @param {Number} [maxPayload=0] The maximum allowed message size + * @param {Object} options Options object + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Number} [options.maxPayload=0] The maximum allowed message size + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ - setSocket(socket, head, maxPayload) { - const receiver = new Receiver( - this.binaryType, - this._extensions, - this._isServer, - maxPayload - ); + setSocket(socket, head, options) { + const receiver = new Receiver({ + binaryType: this.binaryType, + extensions: this._extensions, + isServer: this._isServer, + maxPayload: options.maxPayload, + skipUTF8Validation: options.skipUTF8Validation + }); - this._sender = new Sender(socket, this._extensions); + this._sender = new Sender(socket, this._extensions, options.generateMask); this._receiver = receiver; this._socket = socket; @@ -214,18 +274,26 @@ class WebSocket extends EventEmitter { * +---+ * * @param {Number} [code] Status code explaining why the connection is closing - * @param {String} [data] A string explaining why the connection is closing + * @param {(String|Buffer)} [data] The reason why the connection is + * closing * @public */ close(code, data) { if (this.readyState === WebSocket.CLOSED) return; if (this.readyState === WebSocket.CONNECTING) { const msg = 'WebSocket was closed before the connection was established'; - return abortHandshake(this, this._req, msg); + abortHandshake(this, this._req, msg); + return; } if (this.readyState === WebSocket.CLOSING) { - if (this._closeFrameSent && this._closeFrameReceived) this._socket.end(); + if ( + this._closeFrameSent && + (this._closeFrameReceived || this._receiver._writableState.errorEmitted) + ) { + this._socket.end(); + } + return; } @@ -238,7 +306,13 @@ class WebSocket extends EventEmitter { if (err) return; this._closeFrameSent = true; - if (this._closeFrameReceived) this._socket.end(); + + if ( + this._closeFrameReceived || + this._receiver._writableState.errorEmitted + ) { + this._socket.end(); + } }); // @@ -250,6 +324,23 @@ class WebSocket extends EventEmitter { ); } + /** + * Pause the socket. + * + * @public + */ + pause() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = true; + this._socket.pause(); + } + /** * Send a ping. * @@ -314,15 +405,32 @@ class WebSocket extends EventEmitter { this._sender.pong(data || EMPTY_BUFFER, mask, cb); } + /** + * Resume the socket. + * + * @public + */ + resume() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = false; + if (!this._receiver._writableState.needDrain) this._socket.resume(); + } + /** * Send a data message. * * @param {*} data The message to send * @param {Object} [options] Options object - * @param {Boolean} [options.compress] Specifies whether or not to compress - * `data` * @param {Boolean} [options.binary] Specifies whether `data` is binary or * text + * @param {Boolean} [options.compress] Specifies whether or not to compress + * `data` * @param {Boolean} [options.fin=true] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask] Specifies whether or not to mask `data` @@ -370,7 +478,8 @@ class WebSocket extends EventEmitter { if (this.readyState === WebSocket.CLOSED) return; if (this.readyState === WebSocket.CONNECTING) { const msg = 'WebSocket was closed before the connection was established'; - return abortHandshake(this, this._req, msg); + abortHandshake(this, this._req, msg); + return; } if (this._socket) { @@ -380,17 +489,83 @@ class WebSocket extends EventEmitter { } } -readyStates.forEach((readyState, i) => { - const descriptor = { enumerable: true, value: i }; +/** + * @constant {Number} CONNECTING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); - Object.defineProperty(WebSocket.prototype, readyState, descriptor); - Object.defineProperty(WebSocket, readyState, descriptor); +/** + * @constant {Number} CONNECTING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') }); [ 'binaryType', 'bufferedAmount', 'extensions', + 'isPaused', 'protocol', 'readyState', 'url' @@ -404,37 +579,27 @@ readyStates.forEach((readyState, i) => { // ['open', 'error', 'close', 'message'].forEach((method) => { Object.defineProperty(WebSocket.prototype, `on${method}`, { - configurable: true, enumerable: true, - /** - * Return the listener of the event. - * - * @return {(Function|undefined)} The event listener or `undefined` - * @public - */ get() { - const listeners = this.listeners(method); - for (let i = 0; i < listeners.length; i++) { - if (listeners[i]._listener) return listeners[i]._listener; + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute]) return listener[kListener]; } - return undefined; + return null; }, - /** - * Add a listener for the event. - * - * @param {Function} listener The listener to add - * @public - */ - set(listener) { - const listeners = this.listeners(method); - for (let i = 0; i < listeners.length; i++) { - // - // Remove only the listeners added via `addEventListener`. - // - if (listeners[i]._listener) this.removeListener(method, listeners[i]); + set(handler) { + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute]) { + this.removeListener(method, listener); + break; + } } - this.addEventListener(method, listener); + + if (typeof handler !== 'function') return; + + this.addEventListener(method, handler, { + [kForOnEventAttribute]: true + }); } }); }); @@ -448,29 +613,34 @@ module.exports = WebSocket; * Initialize a WebSocket client. * * @param {WebSocket} websocket The client to initialize - * @param {(String|url.URL)} address The URL to which to connect - * @param {String} [protocols] The subprotocols + * @param {(String|URL)} address The URL to which to connect + * @param {Array} protocols The subprotocols * @param {Object} [options] Connection options - * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable - * permessage-deflate + * @param {Boolean} [options.followRedirects=false] Whether or not to follow + * redirects + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the * handshake request - * @param {Number} [options.protocolVersion=13] Value of the - * `Sec-WebSocket-Version` header - * @param {String} [options.origin] Value of the `Origin` or - * `Sec-WebSocket-Origin` header * @param {Number} [options.maxPayload=104857600] The maximum allowed message * size - * @param {Boolean} [options.followRedirects=false] Whether or not to follow - * redirects * @param {Number} [options.maxRedirects=10] The maximum number of redirects * allowed + * @param {String} [options.origin] Value of the `Origin` or + * `Sec-WebSocket-Origin` header + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable + * permessage-deflate + * @param {Number} [options.protocolVersion=13] Value of the + * `Sec-WebSocket-Version` header + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ function initAsClient(websocket, address, protocols, options) { const opts = { protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: true, followRedirects: false, maxRedirects: 10, @@ -480,7 +650,7 @@ function initAsClient(websocket, address, protocols, options) { hostname: undefined, protocol: undefined, timeout: undefined, - method: undefined, + method: 'GET', host: undefined, path: undefined, port: undefined @@ -499,21 +669,43 @@ function initAsClient(websocket, address, protocols, options) { parsedUrl = address; websocket._url = address.href; } else { - parsedUrl = new URL(address); + try { + parsedUrl = new URL(address); + } catch (e) { + throw new SyntaxError(`Invalid URL: ${address}`); + } + websocket._url = address; } - const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; + const isSecure = parsedUrl.protocol === 'wss:'; + const isIpcUrl = parsedUrl.protocol === 'ws+unix:'; + let invalidUrlMessage; + + if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { + invalidUrlMessage = + 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; + } else if (isIpcUrl && !parsedUrl.pathname) { + invalidUrlMessage = "The URL's pathname is empty"; + } else if (parsedUrl.hash) { + invalidUrlMessage = 'The URL contains a fragment identifier'; + } + + if (invalidUrlMessage) { + const err = new SyntaxError(invalidUrlMessage); - if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) { - throw new Error(`Invalid URL: ${websocket.url}`); + if (websocket._redirects === 0) { + throw err; + } else { + emitErrorAndClose(websocket, err); + return; + } } - const isSecure = - parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:'; const defaultPort = isSecure ? 443 : 80; const key = randomBytes(16).toString('base64'); - const get = isSecure ? https.get : http.get; + const request = isSecure ? https.request : http.request; + const protocolSet = new Set(); let perMessageDeflate; opts.createConnection = isSecure ? tlsConnect : netConnect; @@ -523,11 +715,11 @@ function initAsClient(websocket, address, protocols, options) { ? parsedUrl.hostname.slice(1, -1) : parsedUrl.hostname; opts.headers = { + ...opts.headers, 'Sec-WebSocket-Version': opts.protocolVersion, 'Sec-WebSocket-Key': key, Connection: 'Upgrade', - Upgrade: 'websocket', - ...opts.headers + Upgrade: 'websocket' }; opts.path = parsedUrl.pathname + parsedUrl.search; opts.timeout = opts.handshakeTimeout; @@ -542,8 +734,22 @@ function initAsClient(websocket, address, protocols, options) { [PerMessageDeflate.extensionName]: perMessageDeflate.offer() }); } - if (protocols) { - opts.headers['Sec-WebSocket-Protocol'] = protocols; + if (protocols.length) { + for (const protocol of protocols) { + if ( + typeof protocol !== 'string' || + !subprotocolRegex.test(protocol) || + protocolSet.has(protocol) + ) { + throw new SyntaxError( + 'An invalid or duplicated subprotocol was specified' + ); + } + + protocolSet.add(protocol); + } + + opts.headers['Sec-WebSocket-Protocol'] = protocols.join(','); } if (opts.origin) { if (opts.protocolVersion < 13) { @@ -556,14 +762,86 @@ function initAsClient(websocket, address, protocols, options) { opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; } - if (isUnixSocket) { + if (isIpcUrl) { const parts = opts.path.split(':'); opts.socketPath = parts[0]; opts.path = parts[1]; } - let req = (websocket._req = get(opts)); + let req; + + if (opts.followRedirects) { + if (websocket._redirects === 0) { + websocket._originalIpc = isIpcUrl; + websocket._originalSecure = isSecure; + websocket._originalHostOrSocketPath = isIpcUrl + ? opts.socketPath + : parsedUrl.host; + + const headers = options && options.headers; + + // + // Shallow copy the user provided options so that headers can be changed + // without mutating the original object. + // + options = { ...options, headers: {} }; + + if (headers) { + for (const [key, value] of Object.entries(headers)) { + options.headers[key.toLowerCase()] = value; + } + } + } else if (websocket.listenerCount('redirect') === 0) { + const isSameHost = isIpcUrl + ? websocket._originalIpc + ? opts.socketPath === websocket._originalHostOrSocketPath + : false + : websocket._originalIpc + ? false + : parsedUrl.host === websocket._originalHostOrSocketPath; + + if (!isSameHost || (websocket._originalSecure && !isSecure)) { + // + // Match curl 7.77.0 behavior and drop the following headers. These + // headers are also dropped when following a redirect to a subdomain. + // + delete opts.headers.authorization; + delete opts.headers.cookie; + + if (!isSameHost) delete opts.headers.host; + + opts.auth = undefined; + } + } + + // + // Match curl 7.77.0 behavior and make the first `Authorization` header win. + // If the `Authorization` header is set, then there is nothing to do as it + // will take precedence. + // + if (opts.auth && !options.headers.authorization) { + options.headers.authorization = + 'Basic ' + Buffer.from(opts.auth).toString('base64'); + } + + req = websocket._req = request(opts); + + if (websocket._redirects) { + // + // Unlike what is done for the `'upgrade'` event, no early exit is + // triggered here if the user calls `websocket.close()` or + // `websocket.terminate()` from a listener of the `'redirect'` event. This + // is because the user can also call `request.destroy()` with an error + // before calling `websocket.close()` or `websocket.terminate()` and this + // would result in an error being emitted on the `request` object with no + // `'error'` event listeners attached. + // + websocket.emit('redirect', websocket.url, req); + } + } else { + req = websocket._req = request(opts); + } if (opts.timeout) { req.on('timeout', () => { @@ -572,12 +850,10 @@ function initAsClient(websocket, address, protocols, options) { } req.on('error', (err) => { - if (req === null || req.aborted) return; + if (req === null || req[kAborted]) return; req = websocket._req = null; - websocket._readyState = WebSocket.CLOSING; - websocket.emit('error', err); - websocket.emitClose(); + emitErrorAndClose(websocket, err); }); req.on('response', (res) => { @@ -597,7 +873,15 @@ function initAsClient(websocket, address, protocols, options) { req.abort(); - const addr = new URL(location, address); + let addr; + + try { + addr = new URL(location, address); + } catch (e) { + const err = new SyntaxError(`Invalid URL: ${location}`); + emitErrorAndClose(websocket, err); + return; + } initAsClient(websocket, addr, protocols, options); } else if (!websocket.emit('unexpected-response', req, res)) { @@ -613,13 +897,18 @@ function initAsClient(websocket, address, protocols, options) { websocket.emit('upgrade', res); // - // The user may have closed the connection from a listener of the `upgrade` - // event. + // The user may have closed the connection from a listener of the + // `'upgrade'` event. // if (websocket.readyState !== WebSocket.CONNECTING) return; req = websocket._req = null; + if (res.headers.upgrade.toLowerCase() !== 'websocket') { + abortHandshake(websocket, socket, 'Invalid Upgrade header'); + return; + } + const digest = createHash('sha1') .update(key + GUID) .digest('base64'); @@ -630,15 +919,16 @@ function initAsClient(websocket, address, protocols, options) { } const serverProt = res.headers['sec-websocket-protocol']; - const protList = (protocols || '').split(/, */); let protError; - if (!protocols && serverProt) { - protError = 'Server sent a subprotocol but none was requested'; - } else if (protocols && !serverProt) { + if (serverProt !== undefined) { + if (!protocolSet.size) { + protError = 'Server sent a subprotocol but none was requested'; + } else if (!protocolSet.has(serverProt)) { + protError = 'Server sent an invalid subprotocol'; + } + } else if (protocolSet.size) { protError = 'Server sent no subprotocol'; - } else if (serverProt && !protList.includes(serverProt)) { - protError = 'Server sent an invalid subprotocol'; } if (protError) { @@ -648,28 +938,75 @@ function initAsClient(websocket, address, protocols, options) { if (serverProt) websocket._protocol = serverProt; - if (perMessageDeflate) { + const secWebSocketExtensions = res.headers['sec-websocket-extensions']; + + if (secWebSocketExtensions !== undefined) { + if (!perMessageDeflate) { + const message = + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested'; + abortHandshake(websocket, socket, message); + return; + } + + let extensions; + try { - const extensions = parse(res.headers['sec-websocket-extensions']); + extensions = parse(secWebSocketExtensions); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } - if (extensions[PerMessageDeflate.extensionName]) { - perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); - websocket._extensions[ - PerMessageDeflate.extensionName - ] = perMessageDeflate; - } + const extensionNames = Object.keys(extensions); + + if ( + extensionNames.length !== 1 || + extensionNames[0] !== PerMessageDeflate.extensionName + ) { + const message = 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } + + try { + perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); } catch (err) { - abortHandshake( - websocket, - socket, - 'Invalid Sec-WebSocket-Extensions header' - ); + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); return; } + + websocket._extensions[PerMessageDeflate.extensionName] = + perMessageDeflate; } - websocket.setSocket(socket, head, opts.maxPayload); + websocket.setSocket(socket, head, { + generateMask: opts.generateMask, + maxPayload: opts.maxPayload, + skipUTF8Validation: opts.skipUTF8Validation + }); }); + + if (opts.finishRequest) { + opts.finishRequest(req, websocket); + } else { + req.end(); + } +} + +/** + * Emit the `'error'` and `'close'` events. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {Error} The error to emit + * @private + */ +function emitErrorAndClose(websocket, err) { + websocket._readyState = WebSocket.CLOSING; + websocket.emit('error', err); + websocket.emitClose(); } /** @@ -705,8 +1042,8 @@ function tlsConnect(options) { * Abort the handshake and emit an error. * * @param {WebSocket} websocket The WebSocket instance - * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the - * socket to destroy + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to + * abort or the socket to destroy * @param {String} message The error message * @private */ @@ -717,6 +1054,7 @@ function abortHandshake(websocket, stream, message) { Error.captureStackTrace(err, abortHandshake); if (stream.setHeader) { + stream[kAborted] = true; stream.abort(); if (stream.socket && !stream.socket.destroyed) { @@ -728,8 +1066,7 @@ function abortHandshake(websocket, stream, message) { stream.socket.destroy(); } - stream.once('abort', websocket.emitClose.bind(websocket)); - websocket.emit('error', err); + process.nextTick(emitErrorAndClose, websocket, err); } else { stream.destroy(err); stream.once('error', websocket.emit.bind(websocket, 'error')); @@ -765,7 +1102,7 @@ function sendAfterClose(websocket, data, cb) { `WebSocket is not open: readyState ${websocket.readyState} ` + `(${readyStates[websocket.readyState]})` ); - cb(err); + process.nextTick(cb, err); } } @@ -773,19 +1110,21 @@ function sendAfterClose(websocket, data, cb) { * The listener of the `Receiver` `'conclude'` event. * * @param {Number} code The status code - * @param {String} reason The reason for closing + * @param {Buffer} reason The reason for closing * @private */ function receiverOnConclude(code, reason) { const websocket = this[kWebSocket]; - websocket._socket.removeListener('data', socketOnData); - websocket._socket.resume(); - websocket._closeFrameReceived = true; websocket._closeMessage = reason; websocket._closeCode = code; + if (websocket._socket[kWebSocket] === undefined) return; + + websocket._socket.removeListener('data', socketOnData); + process.nextTick(resume, websocket._socket); + if (code === 1005) websocket.close(); else websocket.close(code, reason); } @@ -796,7 +1135,9 @@ function receiverOnConclude(code, reason) { * @private */ function receiverOnDrain() { - this[kWebSocket]._socket.resume(); + const websocket = this[kWebSocket]; + + if (!websocket.isPaused) websocket._socket.resume(); } /** @@ -808,12 +1149,19 @@ function receiverOnDrain() { function receiverOnError(err) { const websocket = this[kWebSocket]; - websocket._socket.removeListener('data', socketOnData); + if (websocket._socket[kWebSocket] !== undefined) { + websocket._socket.removeListener('data', socketOnData); + + // + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See + // https://github.com/websockets/ws/issues/1940. + // + process.nextTick(resume, websocket._socket); + + websocket.close(err[kStatusCode]); + } - websocket._readyState = WebSocket.CLOSING; - websocket._closeCode = err[kStatusCode]; websocket.emit('error', err); - websocket._socket.destroy(); } /** @@ -828,11 +1176,12 @@ function receiverOnFinish() { /** * The listener of the `Receiver` `'message'` event. * - * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Boolean} isBinary Specifies whether the message is binary or not * @private */ -function receiverOnMessage(data) { - this[kWebSocket].emit('message', data); +function receiverOnMessage(data, isBinary) { + this[kWebSocket].emit('message', data, isBinary); } /** @@ -858,6 +1207,16 @@ function receiverOnPong(data) { this[kWebSocket].emit('pong', data); } +/** + * Resume a readable stream + * + * @param {Readable} stream The readable stream + * @private + */ +function resume(stream) { + stream.resume(); +} + /** * The listener of the `net.Socket` `'close'` event. * @@ -867,10 +1226,13 @@ function socketOnClose() { const websocket = this[kWebSocket]; this.removeListener('close', socketOnClose); + this.removeListener('data', socketOnData); this.removeListener('end', socketOnEnd); websocket._readyState = WebSocket.CLOSING; + let chunk; + // // The close frame might not have been received or the `'end'` event emitted, // for example, if the socket was destroyed due to an error. Ensure that the @@ -878,13 +1240,19 @@ function socketOnClose() { // it. If the readable side of the socket is in flowing mode then there is no // buffered data as everything has been already written and `readable.read()` // will return `null`. If instead, the socket is paused, any possible buffered - // data will be read as a single chunk and emitted synchronously in a single - // `'data'` event. + // data will be read as a single chunk. // - websocket._socket.read(); + if ( + !this._readableState.endEmitted && + !websocket._closeFrameReceived && + !websocket._receiver._writableState.errorEmitted && + (chunk = websocket._socket.read()) !== null + ) { + websocket._receiver.write(chunk); + } + websocket._receiver.end(); - this.removeListener('data', socketOnData); this[kWebSocket] = undefined; clearTimeout(websocket._closeTimer); diff --git a/core/node/connectome/node_modules/ws/package.json b/core/node/connectome/node_modules/ws/package.json index c2d63afe1..4b5d92bdc 100644 --- a/core/node/connectome/node_modules/ws/package.json +++ b/core/node/connectome/node_modules/ws/package.json @@ -1,6 +1,6 @@ { "name": "ws", - "version": "7.4.5", + "version": "8.13.0", "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ "HyBi", @@ -16,14 +16,23 @@ "author": "Einar Otto Stangvik (http://2x.io)", "license": "MIT", "main": "index.js", + "exports": { + ".": { + "browser": "./browser.js", + "import": "./wrapper.mjs", + "require": "./index.js" + }, + "./package.json": "./package.json" + }, "browser": "browser.js", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "files": [ "browser.js", "index.js", - "lib/*.js" + "lib/*.js", + "wrapper.mjs" ], "scripts": { "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js", @@ -32,7 +41,7 @@ }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -45,12 +54,12 @@ "devDependencies": { "benchmark": "^2.1.4", "bufferutil": "^4.0.1", - "eslint": "^7.2.0", + "eslint": "^8.0.0", "eslint-config-prettier": "^8.1.0", - "eslint-plugin-prettier": "^3.0.1", - "mocha": "^7.0.0", + "eslint-plugin-prettier": "^4.0.0", + "mocha": "^8.4.0", "nyc": "^15.0.0", "prettier": "^2.0.5", - "utf-8-validate": "^5.0.2" + "utf-8-validate": "^6.0.0" } } diff --git a/core/node/connectome/node_modules/ws/wrapper.mjs b/core/node/connectome/node_modules/ws/wrapper.mjs new file mode 100644 index 000000000..7245ad15d --- /dev/null +++ b/core/node/connectome/node_modules/ws/wrapper.mjs @@ -0,0 +1,8 @@ +import createWebSocketStream from './lib/stream.js'; +import Receiver from './lib/receiver.js'; +import Sender from './lib/sender.js'; +import WebSocket from './lib/websocket.js'; +import WebSocketServer from './lib/websocket-server.js'; + +export { createWebSocketStream, Receiver, Sender, WebSocket, WebSocketServer }; +export default WebSocket; diff --git a/core/node/connectome/package-lock.json b/core/node/connectome/package-lock.json index db0ee3600..e19c285ff 100644 --- a/core/node/connectome/package-lock.json +++ b/core/node/connectome/package-lock.json @@ -1,12 +1,12 @@ { "name": "connectome", - "version": "0.2.8", + "version": "0.2.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "connectome", - "version": "0.2.8", + "version": "0.2.12", "license": "ISC", "dependencies": { "browser-util-inspect": "^0.2.0", @@ -17,7 +17,7 @@ "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "utf-8-validate": "^5.0.3", - "ws": "^7.4.5" + "ws": "^8.13.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^16.0.0", @@ -407,11 +407,23 @@ "dev": true }, "node_modules/ws": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", - "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } } }, @@ -741,9 +753,10 @@ "dev": true }, "ws": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", - "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "requires": {} } } } diff --git a/core/node/connectome/package.json b/core/node/connectome/package.json index dbe8e089e..294a72421 100644 --- a/core/node/connectome/package.json +++ b/core/node/connectome/package.json @@ -1,6 +1,6 @@ { "name": "connectome", - "version": "0.2.8", + "version": "0.2.12", "description": "Dynamic realtime connectivity and state management", "typings": "typings/src/client/index.d.ts", "scripts": { @@ -55,7 +55,7 @@ "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "utf-8-validate": "^5.0.3", - "ws": "^7.4.5" + "ws": "^8.13.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^16.0.0", diff --git a/core/node/connectome/server/index.js b/core/node/connectome/server/index.js index f95849970..3a3b49bf6 100644 --- a/core/node/connectome/server/index.js +++ b/core/node/connectome/server/index.js @@ -7,13 +7,14 @@ var https = require('https'); var http = require('http'); var net = require('net'); var tls = require('tls'); -var require$$0$1 = require('crypto'); -var require$$1 = require('url'); +var require$$0$2 = require('crypto'); +var require$$0$1 = require('stream'); +var require$$2 = require('url'); var zlib = require('zlib'); var fs = require('fs'); var path = require('path'); var os = require('os'); -var require$$0 = require('stream'); +var require$$0 = require('buffer'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } @@ -22,8 +23,9 @@ var https__default = /*#__PURE__*/_interopDefaultLegacy(https); var http__default = /*#__PURE__*/_interopDefaultLegacy(http); var net__default = /*#__PURE__*/_interopDefaultLegacy(net); var tls__default = /*#__PURE__*/_interopDefaultLegacy(tls); +var require$$0__default$2 = /*#__PURE__*/_interopDefaultLegacy(require$$0$2); var require$$0__default$1 = /*#__PURE__*/_interopDefaultLegacy(require$$0$1); -var require$$1__default = /*#__PURE__*/_interopDefaultLegacy(require$$1); +var require$$2__default = /*#__PURE__*/_interopDefaultLegacy(require$$2); var zlib__default = /*#__PURE__*/_interopDefaultLegacy(zlib); var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); var path__default = /*#__PURE__*/_interopDefaultLegacy(path); @@ -48,10 +50,12 @@ function commonjsRequire () { var constants = { BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], + EMPTY_BUFFER: Buffer.alloc(0), GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + kForOnEventAttribute: Symbol('kIsForOnEventAttribute'), + kListener: Symbol('kListener'), kStatusCode: Symbol('status-code'), kWebSocket: Symbol('websocket'), - EMPTY_BUFFER: Buffer.alloc(0), NOOP: () => {} }; @@ -264,6 +268,8 @@ var bufferUtil = createCommonjsModule(function (module) { const { EMPTY_BUFFER } = constants; +const FastBuffer = Buffer[Symbol.species]; + /** * Merges an array of buffers into a new buffer. * @@ -285,7 +291,9 @@ function concat(list, totalLength) { offset += buf.length; } - if (offset < totalLength) return target.slice(0, offset); + if (offset < totalLength) { + return new FastBuffer(target.buffer, target.byteOffset, offset); + } return target; } @@ -314,9 +322,7 @@ function _mask(source, mask, output, offset, length) { * @public */ function _unmask(buffer, mask) { - // Required until https://github.com/nodejs/node/issues/9006 is resolved. - const length = buffer.length; - for (let i = 0; i < length; i++) { + for (let i = 0; i < buffer.length; i++) { buffer[i] ^= mask[i & 3]; } } @@ -329,11 +335,11 @@ function _unmask(buffer, mask) { * @public */ function toArrayBuffer(buf) { - if (buf.byteLength === buf.buffer.byteLength) { + if (buf.length === buf.buffer.byteLength) { return buf.buffer; } - return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length); } /** @@ -352,9 +358,9 @@ function toBuffer(data) { let buf; if (data instanceof ArrayBuffer) { - buf = Buffer.from(data); + buf = new FastBuffer(data); } else if (ArrayBuffer.isView(data)) { - buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); + buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength); } else { buf = Buffer.from(data); toBuffer.readOnly = false; @@ -363,31 +369,31 @@ function toBuffer(data) { return buf; } -try { - const bufferUtil = bufferutil; - const bu = bufferUtil.BufferUtil || bufferUtil; +module.exports = { + concat, + mask: _mask, + toArrayBuffer, + toBuffer, + unmask: _unmask +}; + +/* istanbul ignore else */ +if (!process.env.WS_NO_BUFFER_UTIL) { + try { + const bufferUtil = bufferutil; - module.exports = { - concat, - mask(source, mask, output, offset, length) { + module.exports.mask = function (source, mask, output, offset, length) { if (length < 48) _mask(source, mask, output, offset, length); - else bu.mask(source, mask, output, offset, length); - }, - toArrayBuffer, - toBuffer, - unmask(buffer, mask) { + else bufferUtil.mask(source, mask, output, offset, length); + }; + + module.exports.unmask = function (buffer, mask) { if (buffer.length < 32) _unmask(buffer, mask); - else bu.unmask(buffer, mask); - } - }; -} catch (e) /* istanbul ignore next */ { - module.exports = { - concat, - mask: _mask, - toArrayBuffer, - toBuffer, - unmask: _unmask - }; + else bufferUtil.unmask(buffer, mask); + }; + } catch (e) { + // Continue regardless of the error. + } } }); @@ -445,8 +451,9 @@ class Limiter { var limiter = Limiter; -const { kStatusCode, NOOP } = constants; +const { kStatusCode } = constants; +const FastBuffer = Buffer[Symbol.species]; const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); const kPerMessageDeflate = Symbol('permessage-deflate'); const kTotalLength = Symbol('total-length'); @@ -471,22 +478,22 @@ class PerMessageDeflate { * Creates a PerMessageDeflate instance. * * @param {Object} [options] Configuration options - * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept - * disabling of server context takeover + * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support + * for, or request, a custom client window size * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ * acknowledge disabling of client context takeover + * @param {Number} [options.concurrencyLimit=10] The number of concurrent + * calls to zlib * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the * use of a custom server window size - * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support - * for, or request, a custom client window size + * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept + * disabling of server context takeover + * @param {Number} [options.threshold=1024] Size (in bytes) below which + * messages should not be compressed if context takeover is disabled * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on * deflate * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on * inflate - * @param {Number} [options.threshold=1024] Size (in bytes) below which - * messages should not be compressed - * @param {Number} [options.concurrencyLimit=10] The number of concurrent - * calls to zlib * @param {Boolean} [isServer=false] Create the instance in either server or * client mode * @param {Number} [maxPayload=0] The maximum allowed message length @@ -754,7 +761,7 @@ class PerMessageDeflate { /** * Compress data. Concurrency limited. * - * @param {Buffer} data Data to compress + * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @public @@ -836,7 +843,7 @@ class PerMessageDeflate { /** * Compress data. * - * @param {Buffer} data Data to compress + * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @private @@ -859,13 +866,6 @@ class PerMessageDeflate { this._deflate[kTotalLength] = 0; this._deflate[kBuffers] = []; - // - // An `'error'` event is emitted, only on Node.js < 10.0.0, if the - // `zlib.DeflateRaw` instance is closed while data is being processed. - // This can happen if `PerMessageDeflate#cleanup()` is called at the wrong - // time due to an abnormal WebSocket closure. - // - this._deflate.on('error', NOOP); this._deflate.on('data', deflateOnData); } @@ -885,7 +885,9 @@ class PerMessageDeflate { this._deflate[kTotalLength] ); - if (fin) data = data.slice(0, data.length - 4); + if (fin) { + data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4); + } // // Ensure that the callback will not be called again in @@ -936,6 +938,7 @@ function inflateOnData(chunk) { } this[kError] = new RangeError('Max payload size exceeded'); + this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; this[kError][kStatusCode] = 1009; this.removeListener('data', inflateOnData); this.reset(); @@ -1029,6 +1032,31 @@ try { var validation = createCommonjsModule(function (module) { +const { isUtf8 } = require$$0__default['default']; + +// +// Allowed token characters: +// +// '!', '#', '$', '%', '&', ''', '*', '+', '-', +// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' +// +// tokenChars[32] === 0 // ' ' +// tokenChars[33] === 1 // '!' +// tokenChars[34] === 0 // '"' +// ... +// +// prettier-ignore +const tokenChars = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 +]; + /** * Checks if a status code is allowed in a close frame. * @@ -1061,7 +1089,7 @@ function _isValidUTF8(buf) { let i = 0; while (i < len) { - if (buf[i] < 0x80) { + if ((buf[i] & 0x80) === 0) { // 0xxxxxxx i++; } else if ((buf[i] & 0xe0) === 0xc0) { @@ -1072,9 +1100,9 @@ function _isValidUTF8(buf) { (buf[i] & 0xfe) === 0xc0 // Overlong ) { return false; - } else { - i += 2; } + + i += 2; } else if ((buf[i] & 0xf0) === 0xe0) { // 1110xxxx 10xxxxxx 10xxxxxx if ( @@ -1085,9 +1113,9 @@ function _isValidUTF8(buf) { (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) ) { return false; - } else { - i += 3; } + + i += 3; } else if ((buf[i] & 0xf8) === 0xf0) { // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx if ( @@ -1100,9 +1128,9 @@ function _isValidUTF8(buf) { buf[i] > 0xf4 // > U+10FFFF ) { return false; - } else { - i += 4; } + + i += 4; } else { return false; } @@ -1111,29 +1139,30 @@ function _isValidUTF8(buf) { return true; } -try { - let isValidUTF8 = utf8Validate; - - /* istanbul ignore if */ - if (typeof isValidUTF8 === 'object') { - isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0 - } +module.exports = { + isValidStatusCode, + isValidUTF8: _isValidUTF8, + tokenChars +}; - module.exports = { - isValidStatusCode, - isValidUTF8(buf) { - return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); - } - }; -} catch (e) /* istanbul ignore next */ { - module.exports = { - isValidStatusCode, - isValidUTF8: _isValidUTF8 +if (isUtf8) { + module.exports.isValidUTF8 = function (buf) { + return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf); }; +} /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) { + try { + const isValidUTF8 = utf8Validate; + + module.exports.isValidUTF8 = function (buf) { + return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf); + }; + } catch (e) { + // Continue regardless of the error. + } } }); -const { Writable } = require$$0__default['default']; +const { Writable } = require$$0__default$1['default']; const { @@ -1145,6 +1174,7 @@ const { const { concat, toArrayBuffer, unmask: unmask$1 } = bufferUtil; const { isValidStatusCode, isValidUTF8: isValidUTF8$1 } = validation; +const FastBuffer$1 = Buffer[Symbol.species]; const GET_INFO = 0; const GET_PAYLOAD_LENGTH_16 = 1; const GET_PAYLOAD_LENGTH_64 = 2; @@ -1155,26 +1185,31 @@ const INFLATING = 5; /** * HyBi Receiver implementation. * - * @extends stream.Writable + * @extends Writable */ class Receiver extends Writable { /** * Creates a Receiver instance. * - * @param {String} [binaryType=nodebuffer] The type for binary data - * @param {Object} [extensions] An object containing the negotiated extensions - * @param {Boolean} [isServer=false] Specifies whether to operate in client or - * server mode - * @param {Number} [maxPayload=0] The maximum allowed message length + * @param {Object} [options] Options object + * @param {String} [options.binaryType=nodebuffer] The type for binary data + * @param {Object} [options.extensions] An object containing the negotiated + * extensions + * @param {Boolean} [options.isServer=false] Specifies whether to operate in + * client or server mode + * @param {Number} [options.maxPayload=0] The maximum allowed message length + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages */ - constructor(binaryType, extensions, isServer, maxPayload) { + constructor(options = {}) { super(); - this._binaryType = binaryType || BINARY_TYPES[0]; + this._binaryType = options.binaryType || BINARY_TYPES[0]; + this._extensions = options.extensions || {}; + this._isServer = !!options.isServer; + this._maxPayload = options.maxPayload | 0; + this._skipUTF8Validation = !!options.skipUTF8Validation; this[kWebSocket] = undefined; - this._extensions = extensions || {}; - this._isServer = !!isServer; - this._maxPayload = maxPayload | 0; this._bufferedBytes = 0; this._buffers = []; @@ -1225,8 +1260,13 @@ class Receiver extends Writable { if (n < this._buffers[0].length) { const buf = this._buffers[0]; - this._buffers[0] = buf.slice(n); - return buf.slice(0, n); + this._buffers[0] = new FastBuffer$1( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); + + return new FastBuffer$1(buf.buffer, buf.byteOffset, n); } const dst = Buffer.allocUnsafe(n); @@ -1239,7 +1279,11 @@ class Receiver extends Writable { dst.set(this._buffers.shift(), offset); } else { dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); - this._buffers[0] = buf.slice(n); + this._buffers[0] = new FastBuffer$1( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); } n -= buf.length; @@ -1301,14 +1345,26 @@ class Receiver extends Writable { if ((buf[0] & 0x30) !== 0x00) { this._loop = false; - return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002); + return error( + RangeError, + 'RSV2 and RSV3 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_2_3' + ); } const compressed = (buf[0] & 0x40) === 0x40; if (compressed && !this._extensions[permessageDeflate.extensionName]) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } this._fin = (buf[0] & 0x80) === 0x80; @@ -1318,45 +1374,85 @@ class Receiver extends Writable { if (this._opcode === 0x00) { if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } if (!this._fragmented) { this._loop = false; - return error(RangeError, 'invalid opcode 0', true, 1002); + return error( + RangeError, + 'invalid opcode 0', + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._opcode = this._fragmented; } else if (this._opcode === 0x01 || this._opcode === 0x02) { if (this._fragmented) { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._compressed = compressed; } else if (this._opcode > 0x07 && this._opcode < 0x0b) { if (!this._fin) { this._loop = false; - return error(RangeError, 'FIN must be set', true, 1002); + return error( + RangeError, + 'FIN must be set', + true, + 1002, + 'WS_ERR_EXPECTED_FIN' + ); } if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } - if (this._payloadLength > 0x7d) { + if ( + this._payloadLength > 0x7d || + (this._opcode === 0x08 && this._payloadLength === 1) + ) { this._loop = false; return error( RangeError, `invalid payload length ${this._payloadLength}`, true, - 1002 + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' ); } } else { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } if (!this._fin && !this._fragmented) this._fragmented = this._opcode; @@ -1365,11 +1461,23 @@ class Receiver extends Writable { if (this._isServer) { if (!this._masked) { this._loop = false; - return error(RangeError, 'MASK must be set', true, 1002); + return error( + RangeError, + 'MASK must be set', + true, + 1002, + 'WS_ERR_EXPECTED_MASK' + ); } } else if (this._masked) { this._loop = false; - return error(RangeError, 'MASK must be clear', true, 1002); + return error( + RangeError, + 'MASK must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_MASK' + ); } if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; @@ -1418,7 +1526,8 @@ class Receiver extends Writable { RangeError, 'Unsupported WebSocket frame: payload length > 2^53 - 1', false, - 1009 + 1009, + 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' ); } @@ -1437,7 +1546,13 @@ class Receiver extends Writable { this._totalPayloadLength += this._payloadLength; if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { this._loop = false; - return error(RangeError, 'Max payload size exceeded', false, 1009); + return error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); } } @@ -1477,7 +1592,13 @@ class Receiver extends Writable { } data = this.consume(this._payloadLength); - if (this._masked) unmask$1(data, this._mask); + + if ( + this._masked && + (this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0 + ) { + unmask$1(data, this._mask); + } } if (this._opcode > 0x07) return this.controlMessage(data); @@ -1490,7 +1611,7 @@ class Receiver extends Writable { if (data.length) { // - // This message is not compressed so its lenght is the sum of the payload + // This message is not compressed so its length is the sum of the payload // length of all fragments. // this._messageLength = this._totalPayloadLength; @@ -1517,7 +1638,13 @@ class Receiver extends Writable { this._messageLength += buf.length; if (this._messageLength > this._maxPayload && this._maxPayload > 0) { return cb( - error(RangeError, 'Max payload size exceeded', false, 1009) + error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ) ); } @@ -1558,16 +1685,22 @@ class Receiver extends Writable { data = fragments; } - this.emit('message', data); + this.emit('message', data, true); } else { const buf = concat(fragments, messageLength); - if (!isValidUTF8$1(buf)) { + if (!this._skipUTF8Validation && !isValidUTF8$1(buf)) { this._loop = false; - return error(Error, 'invalid UTF-8 sequence', true, 1007); + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } - this.emit('message', buf.toString()); + this.emit('message', buf, false); } } @@ -1586,24 +1719,38 @@ class Receiver extends Writable { this._loop = false; if (data.length === 0) { - this.emit('conclude', 1005, ''); + this.emit('conclude', 1005, EMPTY_BUFFER); this.end(); - } else if (data.length === 1) { - return error(RangeError, 'invalid payload length 1', true, 1002); } else { const code = data.readUInt16BE(0); if (!isValidStatusCode(code)) { - return error(RangeError, `invalid status code ${code}`, true, 1002); + return error( + RangeError, + `invalid status code ${code}`, + true, + 1002, + 'WS_ERR_INVALID_CLOSE_CODE' + ); } - const buf = data.slice(2); + const buf = new FastBuffer$1( + data.buffer, + data.byteOffset + 2, + data.length - 2 + ); - if (!isValidUTF8$1(buf)) { - return error(Error, 'invalid UTF-8 sequence', true, 1007); + if (!this._skipUTF8Validation && !isValidUTF8$1(buf)) { + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } - this.emit('conclude', code, buf.toString()); + this.emit('conclude', code, buf); this.end(); } } else if (this._opcode === 0x09) { @@ -1621,32 +1768,35 @@ var receiver = Receiver; /** * Builds an error object. * - * @param {(Error|RangeError)} ErrorCtor The error constructor + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor * @param {String} message The error message * @param {Boolean} prefix Specifies whether or not to add a default prefix to * `message` * @param {Number} statusCode The status code + * @param {String} errorCode The exposed error code * @return {(Error|RangeError)} The error * @private */ -function error(ErrorCtor, message, prefix, statusCode) { +function error(ErrorCtor, message, prefix, statusCode, errorCode) { const err = new ErrorCtor( prefix ? `Invalid WebSocket frame: ${message}` : message ); Error.captureStackTrace(err, error); + err.code = errorCode; err[kStatusCode$1] = statusCode; return err; } -const { randomFillSync } = require$$0__default$1['default']; +const { randomFillSync } = require$$0__default$2['default']; const { EMPTY_BUFFER: EMPTY_BUFFER$1 } = constants; const { isValidStatusCode: isValidStatusCode$1 } = validation; const { mask: applyMask, toBuffer } = bufferUtil; -const mask$1 = Buffer.alloc(4); +const kByteLength = Symbol('kByteLength'); +const maskBuffer = Buffer.alloc(4); /** * HyBi Sender implementation. @@ -1655,11 +1805,19 @@ class Sender { /** * Creates a Sender instance. * - * @param {net.Socket} socket The connection socket + * @param {(net.Socket|tls.Socket)} socket The connection socket * @param {Object} [extensions] An object containing the negotiated extensions + * @param {Function} [generateMask] The function used to generate the masking + * key */ - constructor(socket, extensions) { + constructor(socket, extensions, generateMask) { this._extensions = extensions || {}; + + if (generateMask) { + this._generateMask = generateMask; + this._maskBuffer = Buffer.alloc(4); + } + this._socket = socket; this._firstFragment = true; @@ -1673,34 +1831,71 @@ class Sender { /** * Frames a piece of data according to the HyBi WebSocket protocol. * - * @param {Buffer} data The data to frame + * @param {(Buffer|String)} data The data to frame * @param {Object} options Options object - * @param {Number} options.opcode The opcode - * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be - * modified * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit - * @return {Buffer[]} The framed data as a list of `Buffer` instances + * @return {(Buffer|String)[]} The framed data * @public */ static frame(data, options) { - const merge = options.mask && options.readOnly; - let offset = options.mask ? 6 : 2; - let payloadLength = data.length; + let mask; + let merge = false; + let offset = 2; + let skipMasking = false; + + if (options.mask) { + mask = options.maskBuffer || maskBuffer; + + if (options.generateMask) { + options.generateMask(mask); + } else { + randomFillSync(mask, 0, 4); + } + + skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; + offset = 6; + } + + let dataLength; + + if (typeof data === 'string') { + if ( + (!options.mask || skipMasking) && + options[kByteLength] !== undefined + ) { + dataLength = options[kByteLength]; + } else { + data = Buffer.from(data); + dataLength = data.length; + } + } else { + dataLength = data.length; + merge = options.mask && options.readOnly && !skipMasking; + } + + let payloadLength = dataLength; - if (data.length >= 65536) { + if (dataLength >= 65536) { offset += 8; payloadLength = 127; - } else if (data.length > 125) { + } else if (dataLength > 125) { offset += 2; payloadLength = 126; } - const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); + const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset); target[0] = options.fin ? options.opcode | 0x80 : options.opcode; if (options.rsv1) target[0] |= 0x40; @@ -1708,28 +1903,28 @@ class Sender { target[1] = payloadLength; if (payloadLength === 126) { - target.writeUInt16BE(data.length, 2); + target.writeUInt16BE(dataLength, 2); } else if (payloadLength === 127) { - target.writeUInt32BE(0, 2); - target.writeUInt32BE(data.length, 6); + target[2] = target[3] = 0; + target.writeUIntBE(dataLength, 4, 6); } if (!options.mask) return [target, data]; - randomFillSync(mask$1, 0, 4); - target[1] |= 0x80; - target[offset - 4] = mask$1[0]; - target[offset - 3] = mask$1[1]; - target[offset - 2] = mask$1[2]; - target[offset - 1] = mask$1[3]; + target[offset - 4] = mask[0]; + target[offset - 3] = mask[1]; + target[offset - 2] = mask[2]; + target[offset - 1] = mask[3]; + + if (skipMasking) return [target, data]; if (merge) { - applyMask(data, mask$1, target, offset, data.length); + applyMask(data, mask, target, offset, dataLength); return [target]; } - applyMask(data, mask$1, data, 0, data.length); + applyMask(data, mask, data, 0, dataLength); return [target, data]; } @@ -1737,7 +1932,7 @@ class Sender { * Sends a close message to the other peer. * * @param {Number} [code] The status code component of the body - * @param {String} [data] The message component of the body + * @param {(String|Buffer)} [data] The message component of the body * @param {Boolean} [mask=false] Specifies whether or not to mask the message * @param {Function} [cb] Callback * @public @@ -1749,7 +1944,7 @@ class Sender { buf = EMPTY_BUFFER$1; } else if (typeof code !== 'number' || !isValidStatusCode$1(code)) { throw new TypeError('First argument must be a valid error code number'); - } else if (data === undefined || data === '') { + } else if (data === undefined || !data.length) { buf = Buffer.allocUnsafe(2); buf.writeUInt16BE(code, 0); } else { @@ -1761,37 +1956,32 @@ class Sender { buf = Buffer.allocUnsafe(2 + length); buf.writeUInt16BE(code, 0); - buf.write(data, 2); + + if (typeof data === 'string') { + buf.write(data, 2); + } else { + buf.set(data, 2); + } } + const options = { + [kByteLength]: buf.length, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x08, + readOnly: false, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doClose, buf, mask, cb]); + this.enqueue([this.dispatch, buf, false, options, cb]); } else { - this.doClose(buf, mask, cb); + this.sendFrame(Sender.frame(buf, options), cb); } } - /** - * Frames and sends a close message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Function} [cb] Callback - * @private - */ - doClose(data, mask, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x08, - mask, - readOnly: false - }), - cb - ); - } - /** * Sends a ping message to the other peer. * @@ -1801,41 +1991,40 @@ class Sender { * @public */ ping(data, mask, cb) { - const buf = toBuffer(data); + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } - if (buf.length > 125) { + if (byteLength > 125) { throw new RangeError('The data size must not be greater than 125 bytes'); } + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x09, + readOnly, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]); + this.enqueue([this.dispatch, data, false, options, cb]); } else { - this.doPing(buf, mask, toBuffer.readOnly, cb); + this.sendFrame(Sender.frame(data, options), cb); } } - /** - * Frames and sends a ping message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified - * @param {Function} [cb] Callback - * @private - */ - doPing(data, mask, readOnly, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x09, - mask, - readOnly - }), - cb - ); - } - /** * Sends a pong message to the other peer. * @@ -1845,50 +2034,49 @@ class Sender { * @public */ pong(data, mask, cb) { - const buf = toBuffer(data); + let byteLength; + let readOnly; - if (buf.length > 125) { + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + + if (byteLength > 125) { throw new RangeError('The data size must not be greater than 125 bytes'); } + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x0a, + readOnly, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]); + this.enqueue([this.dispatch, data, false, options, cb]); } else { - this.doPong(buf, mask, toBuffer.readOnly, cb); + this.sendFrame(Sender.frame(data, options), cb); } } - /** - * Frames and sends a pong message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified - * @param {Function} [cb] Callback - * @private - */ - doPong(data, mask, readOnly, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x0a, - mask, - readOnly - }), - cb - ); - } - /** * Sends a data message to the other peer. * * @param {*} data The message to send * @param {Object} options Options object - * @param {Boolean} [options.compress=false] Specifies whether or not to - * compress `data` * @param {Boolean} [options.binary=false] Specifies whether `data` is binary * or text + * @param {Boolean} [options.compress=false] Specifies whether or not to + * compress `data` * @param {Boolean} [options.fin=false] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask=false] Specifies whether or not to mask @@ -1897,15 +2085,34 @@ class Sender { * @public */ send(data, options, cb) { - const buf = toBuffer(data); const perMessageDeflate = this._extensions[permessageDeflate.extensionName]; let opcode = options.binary ? 2 : 1; let rsv1 = options.compress; + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + if (this._firstFragment) { this._firstFragment = false; - if (rsv1 && perMessageDeflate) { - rsv1 = buf.length >= perMessageDeflate._threshold; + if ( + rsv1 && + perMessageDeflate && + perMessageDeflate.params[ + perMessageDeflate._isServer + ? 'server_no_context_takeover' + : 'client_no_context_takeover' + ] + ) { + rsv1 = byteLength >= perMessageDeflate._threshold; } this._compress = rsv1; } else { @@ -1917,26 +2124,32 @@ class Sender { if (perMessageDeflate) { const opts = { + [kByteLength]: byteLength, fin: options.fin, - rsv1, - opcode, + generateMask: this._generateMask, mask: options.mask, - readOnly: toBuffer.readOnly + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1 }; if (this._deflating) { - this.enqueue([this.dispatch, buf, this._compress, opts, cb]); + this.enqueue([this.dispatch, data, this._compress, opts, cb]); } else { - this.dispatch(buf, this._compress, opts, cb); + this.dispatch(data, this._compress, opts, cb); } } else { this.sendFrame( - Sender.frame(buf, { + Sender.frame(data, { + [kByteLength]: byteLength, fin: options.fin, - rsv1: false, - opcode, + generateMask: this._generateMask, mask: options.mask, - readOnly: toBuffer.readOnly + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1: false }), cb ); @@ -1944,19 +2157,23 @@ class Sender { } /** - * Dispatches a data message. + * Dispatches a message. * - * @param {Buffer} data The message to send + * @param {(Buffer|String)} data The message to send * @param {Boolean} [compress=false] Specifies whether or not to compress * `data` * @param {Object} options Options object - * @param {Number} options.opcode The opcode - * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be - * modified * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit * @param {Function} [cb] Callback @@ -1970,7 +2187,7 @@ class Sender { const perMessageDeflate = this._extensions[permessageDeflate.extensionName]; - this._bufferedBytes += data.length; + this._bufferedBytes += options[kByteLength]; this._deflating = true; perMessageDeflate.compress(data, options.fin, (_, buf) => { if (this._socket.destroyed) { @@ -1981,7 +2198,8 @@ class Sender { if (typeof cb === 'function') cb(err); for (let i = 0; i < this._queue.length; i++) { - const callback = this._queue[i][4]; + const params = this._queue[i]; + const callback = params[params.length - 1]; if (typeof callback === 'function') callback(err); } @@ -1989,7 +2207,7 @@ class Sender { return; } - this._bufferedBytes -= data.length; + this._bufferedBytes -= options[kByteLength]; this._deflating = false; options.readOnly = false; this.sendFrame(Sender.frame(buf, options), cb); @@ -2006,7 +2224,7 @@ class Sender { while (!this._deflating && this._queue.length) { const params = this._queue.shift(); - this._bufferedBytes -= params[1].length; + this._bufferedBytes -= params[3][kByteLength]; Reflect.apply(params[0], this, params.slice(1)); } } @@ -2018,7 +2236,7 @@ class Sender { * @private */ enqueue(params) { - this._bufferedBytes += params[1].length; + this._bufferedBytes += params[3][kByteLength]; this._queue.push(params); } @@ -2043,112 +2261,173 @@ class Sender { var sender = Sender; +const { kForOnEventAttribute, kListener } = constants; + +const kCode = Symbol('kCode'); +const kData = Symbol('kData'); +const kError$1 = Symbol('kError'); +const kMessage = Symbol('kMessage'); +const kReason = Symbol('kReason'); +const kTarget = Symbol('kTarget'); +const kType = Symbol('kType'); +const kWasClean = Symbol('kWasClean'); + /** * Class representing an event. - * - * @private */ class Event { /** * Create a new `Event`. * * @param {String} type The name of the event - * @param {Object} target A reference to the target to which the event was - * dispatched + * @throws {TypeError} If the `type` argument is not specified */ - constructor(type, target) { - this.target = target; - this.type = type; + constructor(type) { + this[kTarget] = null; + this[kType] = type; } -} -/** - * Class representing a message event. - * - * @extends Event - * @private - */ -class MessageEvent extends Event { /** - * Create a new `MessageEvent`. - * - * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @type {*} */ - constructor(data, target) { - super('message', target); + get target() { + return this[kTarget]; + } - this.data = data; + /** + * @type {String} + */ + get type() { + return this[kType]; } } +Object.defineProperty(Event.prototype, 'target', { enumerable: true }); +Object.defineProperty(Event.prototype, 'type', { enumerable: true }); + /** * Class representing a close event. * * @extends Event - * @private */ class CloseEvent extends Event { /** * Create a new `CloseEvent`. * - * @param {Number} code The status code explaining why the connection is being - * closed - * @param {String} reason A human-readable string explaining why the - * connection is closing - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {Number} [options.code=0] The status code explaining why the + * connection was closed + * @param {String} [options.reason=''] A human-readable string explaining why + * the connection was closed + * @param {Boolean} [options.wasClean=false] Indicates whether or not the + * connection was cleanly closed */ - constructor(code, reason, target) { - super('close', target); + constructor(type, options = {}) { + super(type); + + this[kCode] = options.code === undefined ? 0 : options.code; + this[kReason] = options.reason === undefined ? '' : options.reason; + this[kWasClean] = options.wasClean === undefined ? false : options.wasClean; + } - this.wasClean = target._closeFrameReceived && target._closeFrameSent; - this.reason = reason; - this.code = code; + /** + * @type {Number} + */ + get code() { + return this[kCode]; + } + + /** + * @type {String} + */ + get reason() { + return this[kReason]; + } + + /** + * @type {Boolean} + */ + get wasClean() { + return this[kWasClean]; } } +Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true }); + /** - * Class representing an open event. + * Class representing an error event. * * @extends Event - * @private */ -class OpenEvent extends Event { +class ErrorEvent extends Event { /** - * Create a new `OpenEvent`. + * Create a new `ErrorEvent`. * - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.error=null] The error that generated this event + * @param {String} [options.message=''] The error message + */ + constructor(type, options = {}) { + super(type); + + this[kError$1] = options.error === undefined ? null : options.error; + this[kMessage] = options.message === undefined ? '' : options.message; + } + + /** + * @type {*} + */ + get error() { + return this[kError$1]; + } + + /** + * @type {String} */ - constructor(target) { - super('open', target); + get message() { + return this[kMessage]; } } +Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true }); +Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true }); + /** - * Class representing an error event. + * Class representing a message event. * * @extends Event - * @private */ -class ErrorEvent extends Event { +class MessageEvent extends Event { /** - * Create a new `ErrorEvent`. + * Create a new `MessageEvent`. * - * @param {Object} error The error that generated this event - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.data=null] The message content */ - constructor(error, target) { - super('error', target); + constructor(type, options = {}) { + super(type); - this.message = error.message; - this.error = error; + this[kData] = options.data === undefined ? null : options.data; + } + + /** + * @type {*} + */ + get data() { + return this[kData]; } } +Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true }); + /** * This provides methods for emulating the `EventTarget` interface. It's not * meant to be used directly. @@ -2160,49 +2439,75 @@ const EventTarget = { * Register an event listener. * * @param {String} type A string representing the event type to listen for - * @param {Function} listener The listener to add + * @param {(Function|Object)} handler The listener to add * @param {Object} [options] An options object specifies characteristics about * the event listener - * @param {Boolean} [options.once=false] A `Boolean`` indicating that the + * @param {Boolean} [options.once=false] A `Boolean` indicating that the * listener should be invoked at most once after being added. If `true`, * the listener would be automatically removed when invoked. * @public */ - addEventListener(type, listener, options) { - if (typeof listener !== 'function') return; - - function onMessage(data) { - listener.call(this, new MessageEvent(data, this)); - } - - function onClose(code, message) { - listener.call(this, new CloseEvent(code, message, this)); - } - - function onError(error) { - listener.call(this, new ErrorEvent(error, this)); - } - - function onOpen() { - listener.call(this, new OpenEvent(this)); + addEventListener(type, handler, options = {}) { + for (const listener of this.listeners(type)) { + if ( + !options[kForOnEventAttribute] && + listener[kListener] === handler && + !listener[kForOnEventAttribute] + ) { + return; + } } - const method = options && options.once ? 'once' : 'on'; + let wrapper; if (type === 'message') { - onMessage._listener = listener; - this[method](type, onMessage); + wrapper = function onMessage(data, isBinary) { + const event = new MessageEvent('message', { + data: isBinary ? data : data.toString() + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'close') { - onClose._listener = listener; - this[method](type, onClose); + wrapper = function onClose(code, message) { + const event = new CloseEvent('close', { + code, + reason: message.toString(), + wasClean: this._closeFrameReceived && this._closeFrameSent + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'error') { - onError._listener = listener; - this[method](type, onError); + wrapper = function onError(error) { + const event = new ErrorEvent('error', { + error, + message: error.message + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'open') { - onOpen._listener = listener; - this[method](type, onOpen); + wrapper = function onOpen() { + const event = new Event('open'); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else { - this[method](type, listener); + return; + } + + wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; + wrapper[kListener] = handler; + + if (options.once) { + this.once(type, wrapper); + } else { + this.on(type, wrapper); } }, @@ -2210,44 +2515,44 @@ const EventTarget = { * Remove an event listener. * * @param {String} type A string representing the event type to remove - * @param {Function} listener The listener to remove + * @param {(Function|Object)} handler The listener to remove * @public */ - removeEventListener(type, listener) { - const listeners = this.listeners(type); - - for (let i = 0; i < listeners.length; i++) { - if (listeners[i] === listener || listeners[i]._listener === listener) { - this.removeListener(type, listeners[i]); + removeEventListener(type, handler) { + for (const listener of this.listeners(type)) { + if (listener[kListener] === handler && !listener[kForOnEventAttribute]) { + this.removeListener(type, listener); + break; } } } }; -var eventTarget = EventTarget; +var eventTarget = { + CloseEvent, + ErrorEvent, + Event, + EventTarget, + MessageEvent +}; + +/** + * Call an event listener + * + * @param {(Function|Object)} listener The listener to call + * @param {*} thisArg The value to use as `this`` when calling the listener + * @param {Event} event The event to pass to the listener + * @private + */ +function callListener(listener, thisArg, event) { + if (typeof listener === 'object' && listener.handleEvent) { + listener.handleEvent.call(listener, event); + } else { + listener.call(thisArg, event); + } +} -// -// Allowed token characters: -// -// '!', '#', '$', '%', '&', ''', '*', '+', '-', -// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' -// -// tokenChars[32] === 0 // ' ' -// tokenChars[33] === 1 // '!' -// tokenChars[34] === 0 // '"' -// ... -// -// prettier-ignore -const tokenChars = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 - 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 -]; +const { tokenChars } = validation; /** * Adds an offer to the map of extension offers or a parameter to the map of @@ -2273,24 +2578,25 @@ function push(dest, name, elem) { */ function parse(header) { const offers = Object.create(null); - - if (header === undefined || header === '') return offers; - let params = Object.create(null); let inQuotes = false; let extensionName; let paramName; let start = -1; + let code = -1; let end = -1; let i = 0; for (; i < header.length; i++) { - const code = header.charCodeAt(i); + code = header.charCodeAt(i); if (extensionName === undefined) { if (end === -1 && tokenChars[code] === 1) { if (start === -1) start = i; - } else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) { + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { if (end === -1 && start !== -1) end = i; } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { if (start === -1) { @@ -2378,7 +2684,7 @@ function parse(header) { } } - if (start === -1 || inQuotes) { + if (start === -1 || inQuotes || code === 0x20 || code === 0x09) { throw new SyntaxError('Unexpected end of input'); } @@ -2431,8 +2737,8 @@ function format(extensions) { var extension = { format, parse }; -const { randomBytes, createHash } = require$$0__default$1['default']; -const { URL } = require$$1__default['default']; +const { randomBytes, createHash } = require$$0__default$2['default']; +const { URL } = require$$2__default['default']; @@ -2441,17 +2747,23 @@ const { BINARY_TYPES: BINARY_TYPES$1, EMPTY_BUFFER: EMPTY_BUFFER$2, GUID, + kForOnEventAttribute: kForOnEventAttribute$1, + kListener: kListener$1, kStatusCode: kStatusCode$2, kWebSocket: kWebSocket$1, - NOOP: NOOP$1 + NOOP } = constants; -const { addEventListener, removeEventListener } = eventTarget; +const { + EventTarget: { addEventListener, removeEventListener } +} = eventTarget; const { format: format$1, parse: parse$1 } = extension; const { toBuffer: toBuffer$1 } = bufferUtil; -const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; -const protocolVersions = [8, 13]; const closeTimeout = 30 * 1000; +const kAborted = Symbol('kAborted'); +const protocolVersions = [8, 13]; +const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; +const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/; /** * Class representing a WebSocket. @@ -2462,7 +2774,7 @@ class WebSocket extends EventEmitter__default['default'] { /** * Create a new `WebSocket`. * - * @param {(String|url.URL)} address The URL to which to connect + * @param {(String|URL)} address The URL to which to connect * @param {(String|String[])} [protocols] The subprotocols * @param {Object} [options] Connection options */ @@ -2473,9 +2785,10 @@ class WebSocket extends EventEmitter__default['default'] { this._closeCode = 1006; this._closeFrameReceived = false; this._closeFrameSent = false; - this._closeMessage = ''; + this._closeMessage = EMPTY_BUFFER$2; this._closeTimer = null; this._extensions = {}; + this._paused = false; this._protocol = ''; this._readyState = WebSocket.CONNECTING; this._receiver = null; @@ -2487,11 +2800,15 @@ class WebSocket extends EventEmitter__default['default'] { this._isServer = false; this._redirects = 0; - if (Array.isArray(protocols)) { - protocols = protocols.join(', '); - } else if (typeof protocols === 'object' && protocols !== null) { - options = protocols; - protocols = undefined; + if (protocols === undefined) { + protocols = []; + } else if (!Array.isArray(protocols)) { + if (typeof protocols === 'object' && protocols !== null) { + options = protocols; + protocols = []; + } else { + protocols = [protocols]; + } } initAsClient(this, address, protocols, options); @@ -2538,6 +2855,45 @@ class WebSocket extends EventEmitter__default['default'] { return Object.keys(this._extensions).join(); } + /** + * @type {Boolean} + */ + get isPaused() { + return this._paused; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onclose() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onerror() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onopen() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onmessage() { + return null; + } + /** * @type {String} */ @@ -2562,20 +2918,27 @@ class WebSocket extends EventEmitter__default['default'] { /** * Set up the socket and the internal resources. * - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream - * @param {Number} [maxPayload=0] The maximum allowed message size + * @param {Object} options Options object + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Number} [options.maxPayload=0] The maximum allowed message size + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ - setSocket(socket, head, maxPayload) { - const receiver$1 = new receiver( - this.binaryType, - this._extensions, - this._isServer, - maxPayload - ); + setSocket(socket, head, options) { + const receiver$1 = new receiver({ + binaryType: this.binaryType, + extensions: this._extensions, + isServer: this._isServer, + maxPayload: options.maxPayload, + skipUTF8Validation: options.skipUTF8Validation + }); - this._sender = new sender(socket, this._extensions); + this._sender = new sender(socket, this._extensions, options.generateMask); this._receiver = receiver$1; this._socket = socket; @@ -2640,18 +3003,26 @@ class WebSocket extends EventEmitter__default['default'] { * +---+ * * @param {Number} [code] Status code explaining why the connection is closing - * @param {String} [data] A string explaining why the connection is closing + * @param {(String|Buffer)} [data] The reason why the connection is + * closing * @public */ close(code, data) { if (this.readyState === WebSocket.CLOSED) return; if (this.readyState === WebSocket.CONNECTING) { const msg = 'WebSocket was closed before the connection was established'; - return abortHandshake(this, this._req, msg); + abortHandshake(this, this._req, msg); + return; } if (this.readyState === WebSocket.CLOSING) { - if (this._closeFrameSent && this._closeFrameReceived) this._socket.end(); + if ( + this._closeFrameSent && + (this._closeFrameReceived || this._receiver._writableState.errorEmitted) + ) { + this._socket.end(); + } + return; } @@ -2664,7 +3035,13 @@ class WebSocket extends EventEmitter__default['default'] { if (err) return; this._closeFrameSent = true; - if (this._closeFrameReceived) this._socket.end(); + + if ( + this._closeFrameReceived || + this._receiver._writableState.errorEmitted + ) { + this._socket.end(); + } }); // @@ -2676,6 +3053,23 @@ class WebSocket extends EventEmitter__default['default'] { ); } + /** + * Pause the socket. + * + * @public + */ + pause() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = true; + this._socket.pause(); + } + /** * Send a ping. * @@ -2740,15 +3134,32 @@ class WebSocket extends EventEmitter__default['default'] { this._sender.pong(data || EMPTY_BUFFER$2, mask, cb); } + /** + * Resume the socket. + * + * @public + */ + resume() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = false; + if (!this._receiver._writableState.needDrain) this._socket.resume(); + } + /** * Send a data message. * * @param {*} data The message to send * @param {Object} [options] Options object - * @param {Boolean} [options.compress] Specifies whether or not to compress - * `data` * @param {Boolean} [options.binary] Specifies whether `data` is binary or * text + * @param {Boolean} [options.compress] Specifies whether or not to compress + * `data` * @param {Boolean} [options.fin=true] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask] Specifies whether or not to mask `data` @@ -2796,7 +3207,8 @@ class WebSocket extends EventEmitter__default['default'] { if (this.readyState === WebSocket.CLOSED) return; if (this.readyState === WebSocket.CONNECTING) { const msg = 'WebSocket was closed before the connection was established'; - return abortHandshake(this, this._req, msg); + abortHandshake(this, this._req, msg); + return; } if (this._socket) { @@ -2806,17 +3218,83 @@ class WebSocket extends EventEmitter__default['default'] { } } -readyStates.forEach((readyState, i) => { - const descriptor = { enumerable: true, value: i }; +/** + * @constant {Number} CONNECTING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} CONNECTING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); - Object.defineProperty(WebSocket.prototype, readyState, descriptor); - Object.defineProperty(WebSocket, readyState, descriptor); +/** + * @constant {Number} CLOSED + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') }); [ 'binaryType', 'bufferedAmount', 'extensions', + 'isPaused', 'protocol', 'readyState', 'url' @@ -2830,37 +3308,27 @@ readyStates.forEach((readyState, i) => { // ['open', 'error', 'close', 'message'].forEach((method) => { Object.defineProperty(WebSocket.prototype, `on${method}`, { - configurable: true, enumerable: true, - /** - * Return the listener of the event. - * - * @return {(Function|undefined)} The event listener or `undefined` - * @public - */ get() { - const listeners = this.listeners(method); - for (let i = 0; i < listeners.length; i++) { - if (listeners[i]._listener) return listeners[i]._listener; + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute$1]) return listener[kListener$1]; } - return undefined; + return null; }, - /** - * Add a listener for the event. - * - * @param {Function} listener The listener to add - * @public - */ - set(listener) { - const listeners = this.listeners(method); - for (let i = 0; i < listeners.length; i++) { - // - // Remove only the listeners added via `addEventListener`. - // - if (listeners[i]._listener) this.removeListener(method, listeners[i]); + set(handler) { + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute$1]) { + this.removeListener(method, listener); + break; + } } - this.addEventListener(method, listener); + + if (typeof handler !== 'function') return; + + this.addEventListener(method, handler, { + [kForOnEventAttribute$1]: true + }); } }); }); @@ -2874,29 +3342,34 @@ var websocket = WebSocket; * Initialize a WebSocket client. * * @param {WebSocket} websocket The client to initialize - * @param {(String|url.URL)} address The URL to which to connect - * @param {String} [protocols] The subprotocols + * @param {(String|URL)} address The URL to which to connect + * @param {Array} protocols The subprotocols * @param {Object} [options] Connection options - * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable - * permessage-deflate + * @param {Boolean} [options.followRedirects=false] Whether or not to follow + * redirects + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the * handshake request - * @param {Number} [options.protocolVersion=13] Value of the - * `Sec-WebSocket-Version` header - * @param {String} [options.origin] Value of the `Origin` or - * `Sec-WebSocket-Origin` header * @param {Number} [options.maxPayload=104857600] The maximum allowed message * size - * @param {Boolean} [options.followRedirects=false] Whether or not to follow - * redirects * @param {Number} [options.maxRedirects=10] The maximum number of redirects * allowed + * @param {String} [options.origin] Value of the `Origin` or + * `Sec-WebSocket-Origin` header + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable + * permessage-deflate + * @param {Number} [options.protocolVersion=13] Value of the + * `Sec-WebSocket-Version` header + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ function initAsClient(websocket, address, protocols, options) { const opts = { protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: true, followRedirects: false, maxRedirects: 10, @@ -2906,7 +3379,7 @@ function initAsClient(websocket, address, protocols, options) { hostname: undefined, protocol: undefined, timeout: undefined, - method: undefined, + method: 'GET', host: undefined, path: undefined, port: undefined @@ -2925,21 +3398,43 @@ function initAsClient(websocket, address, protocols, options) { parsedUrl = address; websocket._url = address.href; } else { - parsedUrl = new URL(address); + try { + parsedUrl = new URL(address); + } catch (e) { + throw new SyntaxError(`Invalid URL: ${address}`); + } + websocket._url = address; } - const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; + const isSecure = parsedUrl.protocol === 'wss:'; + const isIpcUrl = parsedUrl.protocol === 'ws+unix:'; + let invalidUrlMessage; - if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) { - throw new Error(`Invalid URL: ${websocket.url}`); + if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { + invalidUrlMessage = + 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; + } else if (isIpcUrl && !parsedUrl.pathname) { + invalidUrlMessage = "The URL's pathname is empty"; + } else if (parsedUrl.hash) { + invalidUrlMessage = 'The URL contains a fragment identifier'; + } + + if (invalidUrlMessage) { + const err = new SyntaxError(invalidUrlMessage); + + if (websocket._redirects === 0) { + throw err; + } else { + emitErrorAndClose(websocket, err); + return; + } } - const isSecure = - parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:'; const defaultPort = isSecure ? 443 : 80; const key = randomBytes(16).toString('base64'); - const get = isSecure ? https__default['default'].get : http__default['default'].get; + const request = isSecure ? https__default['default'].request : http__default['default'].request; + const protocolSet = new Set(); let perMessageDeflate; opts.createConnection = isSecure ? tlsConnect : netConnect; @@ -2949,11 +3444,11 @@ function initAsClient(websocket, address, protocols, options) { ? parsedUrl.hostname.slice(1, -1) : parsedUrl.hostname; opts.headers = { + ...opts.headers, 'Sec-WebSocket-Version': opts.protocolVersion, 'Sec-WebSocket-Key': key, Connection: 'Upgrade', - Upgrade: 'websocket', - ...opts.headers + Upgrade: 'websocket' }; opts.path = parsedUrl.pathname + parsedUrl.search; opts.timeout = opts.handshakeTimeout; @@ -2968,8 +3463,22 @@ function initAsClient(websocket, address, protocols, options) { [permessageDeflate.extensionName]: perMessageDeflate.offer() }); } - if (protocols) { - opts.headers['Sec-WebSocket-Protocol'] = protocols; + if (protocols.length) { + for (const protocol of protocols) { + if ( + typeof protocol !== 'string' || + !subprotocolRegex.test(protocol) || + protocolSet.has(protocol) + ) { + throw new SyntaxError( + 'An invalid or duplicated subprotocol was specified' + ); + } + + protocolSet.add(protocol); + } + + opts.headers['Sec-WebSocket-Protocol'] = protocols.join(','); } if (opts.origin) { if (opts.protocolVersion < 13) { @@ -2982,14 +3491,86 @@ function initAsClient(websocket, address, protocols, options) { opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; } - if (isUnixSocket) { + if (isIpcUrl) { const parts = opts.path.split(':'); opts.socketPath = parts[0]; opts.path = parts[1]; } - let req = (websocket._req = get(opts)); + let req; + + if (opts.followRedirects) { + if (websocket._redirects === 0) { + websocket._originalIpc = isIpcUrl; + websocket._originalSecure = isSecure; + websocket._originalHostOrSocketPath = isIpcUrl + ? opts.socketPath + : parsedUrl.host; + + const headers = options && options.headers; + + // + // Shallow copy the user provided options so that headers can be changed + // without mutating the original object. + // + options = { ...options, headers: {} }; + + if (headers) { + for (const [key, value] of Object.entries(headers)) { + options.headers[key.toLowerCase()] = value; + } + } + } else if (websocket.listenerCount('redirect') === 0) { + const isSameHost = isIpcUrl + ? websocket._originalIpc + ? opts.socketPath === websocket._originalHostOrSocketPath + : false + : websocket._originalIpc + ? false + : parsedUrl.host === websocket._originalHostOrSocketPath; + + if (!isSameHost || (websocket._originalSecure && !isSecure)) { + // + // Match curl 7.77.0 behavior and drop the following headers. These + // headers are also dropped when following a redirect to a subdomain. + // + delete opts.headers.authorization; + delete opts.headers.cookie; + + if (!isSameHost) delete opts.headers.host; + + opts.auth = undefined; + } + } + + // + // Match curl 7.77.0 behavior and make the first `Authorization` header win. + // If the `Authorization` header is set, then there is nothing to do as it + // will take precedence. + // + if (opts.auth && !options.headers.authorization) { + options.headers.authorization = + 'Basic ' + Buffer.from(opts.auth).toString('base64'); + } + + req = websocket._req = request(opts); + + if (websocket._redirects) { + // + // Unlike what is done for the `'upgrade'` event, no early exit is + // triggered here if the user calls `websocket.close()` or + // `websocket.terminate()` from a listener of the `'redirect'` event. This + // is because the user can also call `request.destroy()` with an error + // before calling `websocket.close()` or `websocket.terminate()` and this + // would result in an error being emitted on the `request` object with no + // `'error'` event listeners attached. + // + websocket.emit('redirect', websocket.url, req); + } + } else { + req = websocket._req = request(opts); + } if (opts.timeout) { req.on('timeout', () => { @@ -2998,12 +3579,10 @@ function initAsClient(websocket, address, protocols, options) { } req.on('error', (err) => { - if (req === null || req.aborted) return; + if (req === null || req[kAborted]) return; req = websocket._req = null; - websocket._readyState = WebSocket.CLOSING; - websocket.emit('error', err); - websocket.emitClose(); + emitErrorAndClose(websocket, err); }); req.on('response', (res) => { @@ -3023,7 +3602,15 @@ function initAsClient(websocket, address, protocols, options) { req.abort(); - const addr = new URL(location, address); + let addr; + + try { + addr = new URL(location, address); + } catch (e) { + const err = new SyntaxError(`Invalid URL: ${location}`); + emitErrorAndClose(websocket, err); + return; + } initAsClient(websocket, addr, protocols, options); } else if (!websocket.emit('unexpected-response', req, res)) { @@ -3039,13 +3626,18 @@ function initAsClient(websocket, address, protocols, options) { websocket.emit('upgrade', res); // - // The user may have closed the connection from a listener of the `upgrade` - // event. + // The user may have closed the connection from a listener of the + // `'upgrade'` event. // if (websocket.readyState !== WebSocket.CONNECTING) return; req = websocket._req = null; + if (res.headers.upgrade.toLowerCase() !== 'websocket') { + abortHandshake(websocket, socket, 'Invalid Upgrade header'); + return; + } + const digest = createHash('sha1') .update(key + GUID) .digest('base64'); @@ -3056,15 +3648,16 @@ function initAsClient(websocket, address, protocols, options) { } const serverProt = res.headers['sec-websocket-protocol']; - const protList = (protocols || '').split(/, */); let protError; - if (!protocols && serverProt) { - protError = 'Server sent a subprotocol but none was requested'; - } else if (protocols && !serverProt) { + if (serverProt !== undefined) { + if (!protocolSet.size) { + protError = 'Server sent a subprotocol but none was requested'; + } else if (!protocolSet.has(serverProt)) { + protError = 'Server sent an invalid subprotocol'; + } + } else if (protocolSet.size) { protError = 'Server sent no subprotocol'; - } else if (serverProt && !protList.includes(serverProt)) { - protError = 'Server sent an invalid subprotocol'; } if (protError) { @@ -3074,28 +3667,75 @@ function initAsClient(websocket, address, protocols, options) { if (serverProt) websocket._protocol = serverProt; - if (perMessageDeflate) { + const secWebSocketExtensions = res.headers['sec-websocket-extensions']; + + if (secWebSocketExtensions !== undefined) { + if (!perMessageDeflate) { + const message = + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested'; + abortHandshake(websocket, socket, message); + return; + } + + let extensions; + try { - const extensions = parse$1(res.headers['sec-websocket-extensions']); + extensions = parse$1(secWebSocketExtensions); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } - if (extensions[permessageDeflate.extensionName]) { - perMessageDeflate.accept(extensions[permessageDeflate.extensionName]); - websocket._extensions[ - permessageDeflate.extensionName - ] = perMessageDeflate; - } + const extensionNames = Object.keys(extensions); + + if ( + extensionNames.length !== 1 || + extensionNames[0] !== permessageDeflate.extensionName + ) { + const message = 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } + + try { + perMessageDeflate.accept(extensions[permessageDeflate.extensionName]); } catch (err) { - abortHandshake( - websocket, - socket, - 'Invalid Sec-WebSocket-Extensions header' - ); + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); return; } + + websocket._extensions[permessageDeflate.extensionName] = + perMessageDeflate; } - websocket.setSocket(socket, head, opts.maxPayload); + websocket.setSocket(socket, head, { + generateMask: opts.generateMask, + maxPayload: opts.maxPayload, + skipUTF8Validation: opts.skipUTF8Validation + }); }); + + if (opts.finishRequest) { + opts.finishRequest(req, websocket); + } else { + req.end(); + } +} + +/** + * Emit the `'error'` and `'close'` events. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {Error} The error to emit + * @private + */ +function emitErrorAndClose(websocket, err) { + websocket._readyState = WebSocket.CLOSING; + websocket.emit('error', err); + websocket.emitClose(); } /** @@ -3131,8 +3771,8 @@ function tlsConnect(options) { * Abort the handshake and emit an error. * * @param {WebSocket} websocket The WebSocket instance - * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the - * socket to destroy + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to + * abort or the socket to destroy * @param {String} message The error message * @private */ @@ -3143,6 +3783,7 @@ function abortHandshake(websocket, stream, message) { Error.captureStackTrace(err, abortHandshake); if (stream.setHeader) { + stream[kAborted] = true; stream.abort(); if (stream.socket && !stream.socket.destroyed) { @@ -3154,8 +3795,7 @@ function abortHandshake(websocket, stream, message) { stream.socket.destroy(); } - stream.once('abort', websocket.emitClose.bind(websocket)); - websocket.emit('error', err); + process.nextTick(emitErrorAndClose, websocket, err); } else { stream.destroy(err); stream.once('error', websocket.emit.bind(websocket, 'error')); @@ -3191,7 +3831,7 @@ function sendAfterClose(websocket, data, cb) { `WebSocket is not open: readyState ${websocket.readyState} ` + `(${readyStates[websocket.readyState]})` ); - cb(err); + process.nextTick(cb, err); } } @@ -3199,19 +3839,21 @@ function sendAfterClose(websocket, data, cb) { * The listener of the `Receiver` `'conclude'` event. * * @param {Number} code The status code - * @param {String} reason The reason for closing + * @param {Buffer} reason The reason for closing * @private */ function receiverOnConclude(code, reason) { const websocket = this[kWebSocket$1]; - websocket._socket.removeListener('data', socketOnData); - websocket._socket.resume(); - websocket._closeFrameReceived = true; websocket._closeMessage = reason; websocket._closeCode = code; + if (websocket._socket[kWebSocket$1] === undefined) return; + + websocket._socket.removeListener('data', socketOnData); + process.nextTick(resume, websocket._socket); + if (code === 1005) websocket.close(); else websocket.close(code, reason); } @@ -3222,7 +3864,9 @@ function receiverOnConclude(code, reason) { * @private */ function receiverOnDrain() { - this[kWebSocket$1]._socket.resume(); + const websocket = this[kWebSocket$1]; + + if (!websocket.isPaused) websocket._socket.resume(); } /** @@ -3234,12 +3878,19 @@ function receiverOnDrain() { function receiverOnError(err) { const websocket = this[kWebSocket$1]; - websocket._socket.removeListener('data', socketOnData); + if (websocket._socket[kWebSocket$1] !== undefined) { + websocket._socket.removeListener('data', socketOnData); + + // + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See + // https://github.com/websockets/ws/issues/1940. + // + process.nextTick(resume, websocket._socket); + + websocket.close(err[kStatusCode$2]); + } - websocket._readyState = WebSocket.CLOSING; - websocket._closeCode = err[kStatusCode$2]; websocket.emit('error', err); - websocket._socket.destroy(); } /** @@ -3254,11 +3905,12 @@ function receiverOnFinish() { /** * The listener of the `Receiver` `'message'` event. * - * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Boolean} isBinary Specifies whether the message is binary or not * @private */ -function receiverOnMessage(data) { - this[kWebSocket$1].emit('message', data); +function receiverOnMessage(data, isBinary) { + this[kWebSocket$1].emit('message', data, isBinary); } /** @@ -3270,7 +3922,7 @@ function receiverOnMessage(data) { function receiverOnPing(data) { const websocket = this[kWebSocket$1]; - websocket.pong(data, !websocket._isServer, NOOP$1); + websocket.pong(data, !websocket._isServer, NOOP); websocket.emit('ping', data); } @@ -3284,6 +3936,16 @@ function receiverOnPong(data) { this[kWebSocket$1].emit('pong', data); } +/** + * Resume a readable stream + * + * @param {Readable} stream The readable stream + * @private + */ +function resume(stream) { + stream.resume(); +} + /** * The listener of the `net.Socket` `'close'` event. * @@ -3293,10 +3955,13 @@ function socketOnClose() { const websocket = this[kWebSocket$1]; this.removeListener('close', socketOnClose); + this.removeListener('data', socketOnData); this.removeListener('end', socketOnEnd); websocket._readyState = WebSocket.CLOSING; + let chunk; + // // The close frame might not have been received or the `'end'` event emitted, // for example, if the socket was destroyed due to an error. Ensure that the @@ -3304,13 +3969,19 @@ function socketOnClose() { // it. If the readable side of the socket is in flowing mode then there is no // buffered data as everything has been already written and `readable.read()` // will return `null`. If instead, the socket is paused, any possible buffered - // data will be read as a single chunk and emitted synchronously in a single - // `'data'` event. + // data will be read as a single chunk. // - websocket._socket.read(); + if ( + !this._readableState.endEmitted && + !websocket._closeFrameReceived && + !websocket._receiver._writableState.errorEmitted && + (chunk = websocket._socket.read()) !== null + ) { + websocket._receiver.write(chunk); + } + websocket._receiver.end(); - this.removeListener('data', socketOnData); this[kWebSocket$1] = undefined; clearTimeout(websocket._closeTimer); @@ -3360,7 +4031,7 @@ function socketOnError() { const websocket = this[kWebSocket$1]; this.removeListener('error', socketOnError); - this.on('error', NOOP$1); + this.on('error', NOOP); if (websocket) { websocket._readyState = WebSocket.CLOSING; @@ -3368,12 +4039,12 @@ function socketOnError() { } } -const { Duplex } = require$$0__default['default']; +const { Duplex } = require$$0__default$1['default']; /** * Emits the `'close'` event on a stream. * - * @param {stream.Duplex} The stream. + * @param {Duplex} stream The stream. * @private */ function emitClose(stream) { @@ -3411,25 +4082,11 @@ function duplexOnError(err) { * * @param {WebSocket} ws The `WebSocket` to wrap * @param {Object} [options] The options for the `Duplex` constructor - * @return {stream.Duplex} The duplex stream + * @return {Duplex} The duplex stream * @public */ function createWebSocketStream(ws, options) { - let resumeOnReceiverDrain = true; - - function receiverOnDrain() { - if (resumeOnReceiverDrain) ws._socket.resume(); - } - - if (ws.readyState === ws.CONNECTING) { - ws.once('open', function open() { - ws._receiver.removeAllListeners('drain'); - ws._receiver.on('drain', receiverOnDrain); - }); - } else { - ws._receiver.removeAllListeners('drain'); - ws._receiver.on('drain', receiverOnDrain); - } + let terminateOnDestroy = true; const duplex = new Duplex({ ...options, @@ -3439,16 +4096,26 @@ function createWebSocketStream(ws, options) { writableObjectMode: false }); - ws.on('message', function message(msg) { - if (!duplex.push(msg)) { - resumeOnReceiverDrain = false; - ws._socket.pause(); - } + ws.on('message', function message(msg, isBinary) { + const data = + !isBinary && duplex._readableState.objectMode ? msg.toString() : msg; + + if (!duplex.push(data)) ws.pause(); }); ws.once('error', function error(err) { if (duplex.destroyed) return; + // Prevent `ws.terminate()` from being called by `duplex._destroy()`. + // + // - If the `'error'` event is emitted before the `'open'` event, then + // `ws.terminate()` is a noop as no socket is assigned. + // - Otherwise, the error is re-emitted by the listener of the `'error'` + // event of the `Receiver` object. The listener already closes the + // connection by calling `ws.close()`. This allows a close frame to be + // sent to the other peer. If `ws.terminate()` is called right after this, + // then the close frame might not be sent. + terminateOnDestroy = false; duplex.destroy(err); }); @@ -3476,7 +4143,8 @@ function createWebSocketStream(ws, options) { if (!called) callback(err); process.nextTick(emitClose, duplex); }); - ws.terminate(); + + if (terminateOnDestroy) ws.terminate(); }; duplex._final = function (callback) { @@ -3508,10 +4176,7 @@ function createWebSocketStream(ws, options) { }; duplex._read = function () { - if (ws.readyState === ws.OPEN && !resumeOnReceiverDrain) { - resumeOnReceiverDrain = true; - if (!ws._receiver._writableState.needDrain) ws._socket.resume(); - } + if (ws.isPaused) ws.resume(); }; duplex._write = function (chunk, encoding, callback) { @@ -3532,16 +4197,81 @@ function createWebSocketStream(ws, options) { var stream = createWebSocketStream; -const { createHash: createHash$1 } = require$$0__default$1['default']; -const { createServer, STATUS_CODES } = http__default['default']; +const { tokenChars: tokenChars$1 } = validation; + +/** + * Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names. + * + * @param {String} header The field value of the header + * @return {Set} The subprotocol names + * @public + */ +function parse$2(header) { + const protocols = new Set(); + let start = -1; + let end = -1; + let i = 0; + + for (i; i < header.length; i++) { + const code = header.charCodeAt(i); + + if (end === -1 && tokenChars$1[code] === 1) { + if (start === -1) start = i; + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x2c /* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + + const protocol = header.slice(start, end); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + + if (start === -1 || end !== -1) { + throw new SyntaxError('Unexpected end of input'); + } + + const protocol = header.slice(start, i); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + return protocols; +} + +var subprotocol = { parse: parse$2 }; + +const { createHash: createHash$1 } = require$$0__default$2['default']; + + -const { format: format$2, parse: parse$2 } = extension; const { GUID: GUID$1, kWebSocket: kWebSocket$2 } = constants; const keyRegex = /^[+/0-9A-Za-z]{22}==$/; +const RUNNING = 0; +const CLOSING = 1; +const CLOSED = 2; + /** * Class representing a WebSocket server. * @@ -3565,8 +4295,13 @@ class WebSocketServer extends EventEmitter__default['default'] { * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable * permessage-deflate * @param {Number} [options.port] The port where to bind the server - * @param {http.Server} [options.server] A pre-created HTTP/S server to use + * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S + * server to use + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @param {Function} [options.verifyClient] A hook to reject connections + * @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket` + * class to use. It must be the `WebSocket` class or class that extends it * @param {Function} [callback] A listener for the `listening` event */ constructor(options, callback) { @@ -3574,6 +4309,7 @@ class WebSocketServer extends EventEmitter__default['default'] { options = { maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: false, handleProtocols: null, clientTracking: true, @@ -3584,18 +4320,24 @@ class WebSocketServer extends EventEmitter__default['default'] { host: null, path: null, port: null, + WebSocket: websocket, ...options }; - if (options.port == null && !options.server && !options.noServer) { + if ( + (options.port == null && !options.server && !options.noServer) || + (options.port != null && (options.server || options.noServer)) || + (options.server && options.noServer) + ) { throw new TypeError( - 'One of the "port", "server", or "noServer" options must be specified' + 'One and only one of the "port", "server", or "noServer" options ' + + 'must be specified' ); } if (options.port != null) { - this._server = createServer((req, res) => { - const body = STATUS_CODES[426]; + this._server = http__default['default'].createServer((req, res) => { + const body = http__default['default'].STATUS_CODES[426]; res.writeHead(426, { 'Content-Length': body.length, @@ -3626,8 +4368,13 @@ class WebSocketServer extends EventEmitter__default['default'] { } if (options.perMessageDeflate === true) options.perMessageDeflate = {}; - if (options.clientTracking) this.clients = new Set(); + if (options.clientTracking) { + this.clients = new Set(); + this._shouldEmitClose = false; + } + this.options = options; + this._state = RUNNING; } /** @@ -3649,37 +4396,58 @@ class WebSocketServer extends EventEmitter__default['default'] { } /** - * Close the server. + * Stop the server from accepting new connections and emit the `'close'` event + * when all existing connections are closed. * - * @param {Function} [cb] Callback + * @param {Function} [cb] A one-time listener for the `'close'` event * @public */ close(cb) { - if (cb) this.once('close', cb); + if (this._state === CLOSED) { + if (cb) { + this.once('close', () => { + cb(new Error('The server is not running')); + }); + } - // - // Terminate all associated clients. - // - if (this.clients) { - for (const client of this.clients) client.terminate(); + process.nextTick(emitClose$1, this); + return; } - const server = this._server; + if (cb) this.once('close', cb); + + if (this._state === CLOSING) return; + this._state = CLOSING; + + if (this.options.noServer || this.options.server) { + if (this._server) { + this._removeListeners(); + this._removeListeners = this._server = null; + } + + if (this.clients) { + if (!this.clients.size) { + process.nextTick(emitClose$1, this); + } else { + this._shouldEmitClose = true; + } + } else { + process.nextTick(emitClose$1, this); + } + } else { + const server = this._server; - if (server) { this._removeListeners(); this._removeListeners = this._server = null; // - // Close the http server if it was internally created. + // The HTTP/S server was created internally. Close it, and rely on its + // `'close'` event. // - if (this.options.port != null) { - server.close(() => this.emit('close')); - return; - } + server.close(() => { + emitClose$1(this); + }); } - - process.nextTick(emitClose$1, this); } /** @@ -3704,7 +4472,8 @@ class WebSocketServer extends EventEmitter__default['default'] { * Handle a HTTP Upgrade request. * * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @public @@ -3712,25 +4481,58 @@ class WebSocketServer extends EventEmitter__default['default'] { handleUpgrade(req, socket, head, cb) { socket.on('error', socketOnError$1); - const key = - req.headers['sec-websocket-key'] !== undefined - ? req.headers['sec-websocket-key'].trim() - : false; + const key = req.headers['sec-websocket-key']; const version = +req.headers['sec-websocket-version']; + + if (req.method !== 'GET') { + const message = 'Invalid HTTP method'; + abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); + return; + } + + if (req.headers.upgrade.toLowerCase() !== 'websocket') { + const message = 'Invalid Upgrade header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (!key || !keyRegex.test(key)) { + const message = 'Missing or invalid Sec-WebSocket-Key header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (version !== 8 && version !== 13) { + const message = 'Missing or invalid Sec-WebSocket-Version header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (!this.shouldHandle(req)) { + abortHandshake$1(socket, 400); + return; + } + + const secWebSocketProtocol = req.headers['sec-websocket-protocol']; + let protocols = new Set(); + + if (secWebSocketProtocol !== undefined) { + try { + protocols = subprotocol.parse(secWebSocketProtocol); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Protocol header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + } + + const secWebSocketExtensions = req.headers['sec-websocket-extensions']; const extensions = {}; if ( - req.method !== 'GET' || - req.headers.upgrade.toLowerCase() !== 'websocket' || - !key || - !keyRegex.test(key) || - (version !== 8 && version !== 13) || - !this.shouldHandle(req) + this.options.perMessageDeflate && + secWebSocketExtensions !== undefined ) { - return abortHandshake$1(socket, 400); - } - - if (this.options.perMessageDeflate) { const perMessageDeflate = new permessageDeflate( this.options.perMessageDeflate, true, @@ -3738,14 +4540,17 @@ class WebSocketServer extends EventEmitter__default['default'] { ); try { - const offers = parse$2(req.headers['sec-websocket-extensions']); + const offers = extension.parse(secWebSocketExtensions); if (offers[permessageDeflate.extensionName]) { perMessageDeflate.accept(offers[permessageDeflate.extensionName]); extensions[permessageDeflate.extensionName] = perMessageDeflate; } } catch (err) { - return abortHandshake$1(socket, 400); + const message = + 'Invalid or unacceptable Sec-WebSocket-Extensions header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; } } @@ -3766,7 +4571,15 @@ class WebSocketServer extends EventEmitter__default['default'] { return abortHandshake$1(socket, code || 401, message, headers); } - this.completeUpgrade(key, extensions, req, socket, head, cb); + this.completeUpgrade( + extensions, + key, + protocols, + req, + socket, + head, + cb + ); }); return; } @@ -3774,22 +4587,24 @@ class WebSocketServer extends EventEmitter__default['default'] { if (!this.options.verifyClient(info)) return abortHandshake$1(socket, 401); } - this.completeUpgrade(key, extensions, req, socket, head, cb); + this.completeUpgrade(extensions, key, protocols, req, socket, head, cb); } /** * Upgrade the connection to WebSocket. * - * @param {String} key The value of the `Sec-WebSocket-Key` header * @param {Object} extensions The accepted extensions + * @param {String} key The value of the `Sec-WebSocket-Key` header + * @param {Set} protocols The subprotocols * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @throws {Error} If called more than once with the same socket * @private */ - completeUpgrade(key, extensions, req, socket, head, cb) { + completeUpgrade(extensions, key, protocols, req, socket, head, cb) { // // Destroy the socket if the client has already sent a FIN packet. // @@ -3802,6 +4617,8 @@ class WebSocketServer extends EventEmitter__default['default'] { ); } + if (this._state > RUNNING) return abortHandshake$1(socket, 503); + const digest = createHash$1('sha1') .update(key + GUID$1) .digest('base64'); @@ -3813,20 +4630,15 @@ class WebSocketServer extends EventEmitter__default['default'] { `Sec-WebSocket-Accept: ${digest}` ]; - const ws = new websocket(null); - let protocol = req.headers['sec-websocket-protocol']; - - if (protocol) { - protocol = protocol.trim().split(/ *, */); + const ws = new this.options.WebSocket(null); + if (protocols.size) { // // Optionally call external protocol selection handler. // - if (this.options.handleProtocols) { - protocol = this.options.handleProtocols(protocol, req); - } else { - protocol = protocol[0]; - } + const protocol = this.options.handleProtocols + ? this.options.handleProtocols(protocols, req) + : protocols.values().next().value; if (protocol) { headers.push(`Sec-WebSocket-Protocol: ${protocol}`); @@ -3836,7 +4648,7 @@ class WebSocketServer extends EventEmitter__default['default'] { if (extensions[permessageDeflate.extensionName]) { const params = extensions[permessageDeflate.extensionName].params; - const value = format$2({ + const value = extension.format({ [permessageDeflate.extensionName]: [params] }); headers.push(`Sec-WebSocket-Extensions: ${value}`); @@ -3851,11 +4663,20 @@ class WebSocketServer extends EventEmitter__default['default'] { socket.write(headers.concat('\r\n').join('\r\n')); socket.removeListener('error', socketOnError$1); - ws.setSocket(socket, head, this.options.maxPayload); + ws.setSocket(socket, head, { + maxPayload: this.options.maxPayload, + skipUTF8Validation: this.options.skipUTF8Validation + }); if (this.clients) { this.clients.add(ws); - ws.on('close', () => this.clients.delete(ws)); + ws.on('close', () => { + this.clients.delete(ws); + + if (this._shouldEmitClose && !this.clients.size) { + process.nextTick(emitClose$1, this); + } + }); } cb(ws, req); @@ -3891,11 +4712,12 @@ function addListeners(server, map) { * @private */ function emitClose$1(server) { + server._state = CLOSED; server.emit('close'); } /** - * Handle premature socket errors. + * Handle socket errors. * * @private */ @@ -3906,34 +4728,61 @@ function socketOnError$1() { /** * Close the connection when preconditions are not fulfilled. * - * @param {net.Socket} socket The socket of the upgrade request + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} [message] The HTTP response body * @param {Object} [headers] Additional HTTP response headers * @private */ function abortHandshake$1(socket, code, message, headers) { - if (socket.writable) { - message = message || STATUS_CODES[code]; - headers = { - Connection: 'close', - 'Content-Type': 'text/html', - 'Content-Length': Buffer.byteLength(message), - ...headers - }; + // + // The socket is writable unless the user destroyed or ended it before calling + // `server.handleUpgrade()` or in the `verifyClient` function, which is a user + // error. Handling this does not make much sense as the worst that can happen + // is that some of the data written by the user might be discarded due to the + // call to `socket.end()` below, which triggers an `'error'` event that in + // turn causes the socket to be destroyed. + // + message = message || http__default['default'].STATUS_CODES[code]; + headers = { + Connection: 'close', + 'Content-Type': 'text/html', + 'Content-Length': Buffer.byteLength(message), + ...headers + }; - socket.write( - `HTTP/1.1 ${code} ${STATUS_CODES[code]}\r\n` + - Object.keys(headers) - .map((h) => `${h}: ${headers[h]}`) - .join('\r\n') + - '\r\n\r\n' + - message - ); - } + socket.once('finish', socket.destroy); + + socket.end( + `HTTP/1.1 ${code} ${http__default['default'].STATUS_CODES[code]}\r\n` + + Object.keys(headers) + .map((h) => `${h}: ${headers[h]}`) + .join('\r\n') + + '\r\n\r\n' + + message + ); +} + +/** + * Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least + * one listener for it, otherwise call `abortHandshake()`. + * + * @param {WebSocketServer} server The WebSocket server + * @param {http.IncomingMessage} req The request object + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} message The HTTP response body + * @private + */ +function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) { + if (server.listenerCount('wsClientError')) { + const err = new Error(message); + Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); - socket.removeListener('error', socketOnError$1); - socket.destroy(); + server.emit('wsClientError', err, socket, req); + } else { + abortHandshake$1(socket, code, message); + } } websocket.createWebSocketStream = stream; @@ -3941,6 +4790,9 @@ websocket.Server = websocketServer; websocket.Receiver = receiver; websocket.Sender = sender; +websocket.WebSocket = websocket; +websocket.WebSocketServer = websocket.Server; + var ws = websocket; function noop() {} @@ -6447,7 +7299,7 @@ nacl.setPRNG = function(fn) { }); } else if (typeof commonjsRequire !== 'undefined') { // Node.js. - crypto = require$$0__default$1['default']; + crypto = require$$0__default$2['default']; if (crypto && crypto.randomBytes) { nacl.setPRNG(function(x, n) { var i, v = crypto.randomBytes(n); @@ -7032,20 +7884,24 @@ naclFast.util = naclUtil; function send({ message, channel }) { const { log } = channel; + // logger.red(log, `Sending over channel ${channel.ident} ws id ${channel.ws.__id}`); + // logger.red(log, message); + if (isObject(message)) { message = JSON.stringify(message); } + const prefix = `Channel #${channel.ident} ${channel.remoteAddress() || ''} ${ + channel.remotePubkeyHex() ? `to ${channel.remotePubkeyHex()}` : '' + }`; + const nonce = new Uint8Array(integerToByteArray(2 * channel.sentCount + 1, 24)); if (channel.verbose) { if (channel.sharedSecret) { - logger.write( - log, - `Channel ${channel.remoteAddress()} → Sending encrypted message #${channel.sentCount}:` - ); + logger.cyan(log, `${prefix} → Sending encrypted message #${channel.sentCount}:`); } else { - logger.write(log, `Channel ${channel.remoteAddress()} → Sending message #${channel.sentCount}:`); + logger.green(log, `${prefix} → Sending message #${channel.sentCount}:`); } logger.write(log, message); @@ -7124,12 +7980,16 @@ function handleMessage(channel, message) { function messageReceived({ message, channel }) { const { log } = channel; + const prefix = `Channel #${channel.ident} ${channel.remoteAddress() || ''} ${ + channel.remotePubkeyHex() ? `to ${channel.remotePubkeyHex()}` : '' + }`; + channel.lastMessageAt = Date.now(); const nonce = new Uint8Array(integerToByteArray(2 * channel.receivedCount, 24)); if (channel.verbose) { - logger.write(log, `Channel ${channel.remoteAddress()} → Received message #${channel.receivedCount} ↴`); + logger.yellow(log, `${prefix} → Received message #${channel.receivedCount} ↴`); } //if (channel.sharedSecret) { @@ -7144,6 +8004,10 @@ function messageReceived({ message, channel }) { try { // handshake phase if (!channel.sharedSecret) { + if (channel.verbose) { + logger.write(log, `${prefix} handshake message: ${message}`); + } + //const jsonData = JSON.parse(message); handleMessage(channel, message); return; @@ -7833,6 +8697,8 @@ class Channel$1 extends Eev { super(); this.ws = ws; + this.ident = Math.round(10 ** 5 * Math.random()).toString(); + this.log = log; this.verbose = verbose; @@ -8002,16 +8868,12 @@ class WsServer extends Eev { super(); process.nextTick(() => { - // const handleProtocols = (protocols, request) => { - // return protocols[0]; - // }; - if (server) { - this.webSocketServer = new ws.Server({ server }); - //this.webSocketServer = new WebSocket.Server({ server, handleProtocols }); + //this.webSocketServer = new WebSocket.Server({ server }); + this.webSocketServer = new ws.WebSocketServer({ server }); } else { - this.webSocketServer = new ws.Server({ port }); - //this.webSocketServer = new WebSocket.Server({ port, handleProtocols }); + //this.webSocketServer = new WebSocket.Server({ port }); + this.webSocketServer = new ws.WebSocketServer({ port }); } this.continueSetup({ log, verbose }); @@ -8020,8 +8882,25 @@ class WsServer extends Eev { continueSetup({ log, verbose }) { this.webSocketServer.on('connection', (ws, req) => { + // https://github.com/websockets/ws/issues/1354 + // Websocket RangeError: Invalid WebSocket frame: RSV2 and RSV3 must be clear + // https://stackoverflow.com/questions/45303733/error-rsv2-and-rsv3-must-be-clear-in-ws + // this should possibly help, not yet confirmed! + ws.on('error', e => { + const log2 = log.yellow || log; + log2('Handled Websocket issue (probably a malformed websocket connection):'); + log2(e); + // log.red => assume dmt logger + // log => assume console.log + }); + const channel = new Channel$1(ws, { log, verbose }); + // const wsId = Math.round(10 ** 5 * Math.random()).toString(); + // ws.__id = wsId; + // const log3 = log.red || log; + // log3(`Created new channel ${channel.ident}, ws id: ${wsId}`); + channel._remoteIp = getRemoteIp(req); channel._remoteAddress = getRemoteHost(req); @@ -8267,6 +9146,10 @@ function orderBy(key, key2, order = 'asc') { //import ProtocolStore from '../../stores/back/protocolStore.js'; +//⚠️ when not going directly to ws port but instead for example through ligthttpd websocket-upgrade +// this channelList will behave strangely... probably just a lag between connections actually disappearing +// so to count active connections through proxy it is not accurate, hopefully just a time lag but test... + class ChannelList extends Eev { constructor({ protocol }) { super(); @@ -8330,13 +9213,11 @@ class ChannelList extends Eev { reportStatus() { const connList = this.channels.map(channel => { - const result = { + return { ip: channel.remoteIp(), address: channel.remoteAddress(), remotePubkeyHex: channel.remotePubkeyHex() }; - - return result; }); this.emit('status', { connList }); diff --git a/core/node/connectome/server/index.mjs b/core/node/connectome/server/index.mjs index 12d080433..16cfd3576 100644 --- a/core/node/connectome/server/index.mjs +++ b/core/node/connectome/server/index.mjs @@ -3,13 +3,14 @@ import https from 'https'; import http from 'http'; import net from 'net'; import tls from 'tls'; -import require$$0$1 from 'crypto'; -import require$$1 from 'url'; +import require$$0$2 from 'crypto'; +import require$$0$1 from 'stream'; +import require$$2 from 'url'; import zlib from 'zlib'; import fs from 'fs'; import path from 'path'; import os from 'os'; -import require$$0 from 'stream'; +import require$$0 from 'buffer'; var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; @@ -29,10 +30,12 @@ function commonjsRequire () { var constants = { BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], + EMPTY_BUFFER: Buffer.alloc(0), GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + kForOnEventAttribute: Symbol('kIsForOnEventAttribute'), + kListener: Symbol('kListener'), kStatusCode: Symbol('status-code'), kWebSocket: Symbol('websocket'), - EMPTY_BUFFER: Buffer.alloc(0), NOOP: () => {} }; @@ -245,6 +248,8 @@ var bufferUtil = createCommonjsModule(function (module) { const { EMPTY_BUFFER } = constants; +const FastBuffer = Buffer[Symbol.species]; + /** * Merges an array of buffers into a new buffer. * @@ -266,7 +271,9 @@ function concat(list, totalLength) { offset += buf.length; } - if (offset < totalLength) return target.slice(0, offset); + if (offset < totalLength) { + return new FastBuffer(target.buffer, target.byteOffset, offset); + } return target; } @@ -295,9 +302,7 @@ function _mask(source, mask, output, offset, length) { * @public */ function _unmask(buffer, mask) { - // Required until https://github.com/nodejs/node/issues/9006 is resolved. - const length = buffer.length; - for (let i = 0; i < length; i++) { + for (let i = 0; i < buffer.length; i++) { buffer[i] ^= mask[i & 3]; } } @@ -310,11 +315,11 @@ function _unmask(buffer, mask) { * @public */ function toArrayBuffer(buf) { - if (buf.byteLength === buf.buffer.byteLength) { + if (buf.length === buf.buffer.byteLength) { return buf.buffer; } - return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length); } /** @@ -333,9 +338,9 @@ function toBuffer(data) { let buf; if (data instanceof ArrayBuffer) { - buf = Buffer.from(data); + buf = new FastBuffer(data); } else if (ArrayBuffer.isView(data)) { - buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); + buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength); } else { buf = Buffer.from(data); toBuffer.readOnly = false; @@ -344,31 +349,31 @@ function toBuffer(data) { return buf; } -try { - const bufferUtil = bufferutil; - const bu = bufferUtil.BufferUtil || bufferUtil; +module.exports = { + concat, + mask: _mask, + toArrayBuffer, + toBuffer, + unmask: _unmask +}; + +/* istanbul ignore else */ +if (!process.env.WS_NO_BUFFER_UTIL) { + try { + const bufferUtil = bufferutil; - module.exports = { - concat, - mask(source, mask, output, offset, length) { + module.exports.mask = function (source, mask, output, offset, length) { if (length < 48) _mask(source, mask, output, offset, length); - else bu.mask(source, mask, output, offset, length); - }, - toArrayBuffer, - toBuffer, - unmask(buffer, mask) { + else bufferUtil.mask(source, mask, output, offset, length); + }; + + module.exports.unmask = function (buffer, mask) { if (buffer.length < 32) _unmask(buffer, mask); - else bu.unmask(buffer, mask); - } - }; -} catch (e) /* istanbul ignore next */ { - module.exports = { - concat, - mask: _mask, - toArrayBuffer, - toBuffer, - unmask: _unmask - }; + else bufferUtil.unmask(buffer, mask); + }; + } catch (e) { + // Continue regardless of the error. + } } }); @@ -426,8 +431,9 @@ class Limiter { var limiter = Limiter; -const { kStatusCode, NOOP } = constants; +const { kStatusCode } = constants; +const FastBuffer = Buffer[Symbol.species]; const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); const kPerMessageDeflate = Symbol('permessage-deflate'); const kTotalLength = Symbol('total-length'); @@ -452,22 +458,22 @@ class PerMessageDeflate { * Creates a PerMessageDeflate instance. * * @param {Object} [options] Configuration options - * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept - * disabling of server context takeover + * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support + * for, or request, a custom client window size * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ * acknowledge disabling of client context takeover + * @param {Number} [options.concurrencyLimit=10] The number of concurrent + * calls to zlib * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the * use of a custom server window size - * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support - * for, or request, a custom client window size + * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept + * disabling of server context takeover + * @param {Number} [options.threshold=1024] Size (in bytes) below which + * messages should not be compressed if context takeover is disabled * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on * deflate * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on * inflate - * @param {Number} [options.threshold=1024] Size (in bytes) below which - * messages should not be compressed - * @param {Number} [options.concurrencyLimit=10] The number of concurrent - * calls to zlib * @param {Boolean} [isServer=false] Create the instance in either server or * client mode * @param {Number} [maxPayload=0] The maximum allowed message length @@ -735,7 +741,7 @@ class PerMessageDeflate { /** * Compress data. Concurrency limited. * - * @param {Buffer} data Data to compress + * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @public @@ -817,7 +823,7 @@ class PerMessageDeflate { /** * Compress data. * - * @param {Buffer} data Data to compress + * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @private @@ -840,13 +846,6 @@ class PerMessageDeflate { this._deflate[kTotalLength] = 0; this._deflate[kBuffers] = []; - // - // An `'error'` event is emitted, only on Node.js < 10.0.0, if the - // `zlib.DeflateRaw` instance is closed while data is being processed. - // This can happen if `PerMessageDeflate#cleanup()` is called at the wrong - // time due to an abnormal WebSocket closure. - // - this._deflate.on('error', NOOP); this._deflate.on('data', deflateOnData); } @@ -866,7 +865,9 @@ class PerMessageDeflate { this._deflate[kTotalLength] ); - if (fin) data = data.slice(0, data.length - 4); + if (fin) { + data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4); + } // // Ensure that the callback will not be called again in @@ -917,6 +918,7 @@ function inflateOnData(chunk) { } this[kError] = new RangeError('Max payload size exceeded'); + this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; this[kError][kStatusCode] = 1009; this.removeListener('data', inflateOnData); this.reset(); @@ -1010,6 +1012,31 @@ try { var validation = createCommonjsModule(function (module) { +const { isUtf8 } = require$$0; + +// +// Allowed token characters: +// +// '!', '#', '$', '%', '&', ''', '*', '+', '-', +// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' +// +// tokenChars[32] === 0 // ' ' +// tokenChars[33] === 1 // '!' +// tokenChars[34] === 0 // '"' +// ... +// +// prettier-ignore +const tokenChars = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 +]; + /** * Checks if a status code is allowed in a close frame. * @@ -1042,7 +1069,7 @@ function _isValidUTF8(buf) { let i = 0; while (i < len) { - if (buf[i] < 0x80) { + if ((buf[i] & 0x80) === 0) { // 0xxxxxxx i++; } else if ((buf[i] & 0xe0) === 0xc0) { @@ -1053,9 +1080,9 @@ function _isValidUTF8(buf) { (buf[i] & 0xfe) === 0xc0 // Overlong ) { return false; - } else { - i += 2; } + + i += 2; } else if ((buf[i] & 0xf0) === 0xe0) { // 1110xxxx 10xxxxxx 10xxxxxx if ( @@ -1066,9 +1093,9 @@ function _isValidUTF8(buf) { (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) ) { return false; - } else { - i += 3; } + + i += 3; } else if ((buf[i] & 0xf8) === 0xf0) { // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx if ( @@ -1081,9 +1108,9 @@ function _isValidUTF8(buf) { buf[i] > 0xf4 // > U+10FFFF ) { return false; - } else { - i += 4; } + + i += 4; } else { return false; } @@ -1092,29 +1119,30 @@ function _isValidUTF8(buf) { return true; } -try { - let isValidUTF8 = utf8Validate; - - /* istanbul ignore if */ - if (typeof isValidUTF8 === 'object') { - isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0 - } +module.exports = { + isValidStatusCode, + isValidUTF8: _isValidUTF8, + tokenChars +}; - module.exports = { - isValidStatusCode, - isValidUTF8(buf) { - return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); - } - }; -} catch (e) /* istanbul ignore next */ { - module.exports = { - isValidStatusCode, - isValidUTF8: _isValidUTF8 +if (isUtf8) { + module.exports.isValidUTF8 = function (buf) { + return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf); }; +} /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) { + try { + const isValidUTF8 = utf8Validate; + + module.exports.isValidUTF8 = function (buf) { + return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf); + }; + } catch (e) { + // Continue regardless of the error. + } } }); -const { Writable } = require$$0; +const { Writable } = require$$0$1; const { @@ -1126,6 +1154,7 @@ const { const { concat, toArrayBuffer, unmask: unmask$1 } = bufferUtil; const { isValidStatusCode, isValidUTF8: isValidUTF8$1 } = validation; +const FastBuffer$1 = Buffer[Symbol.species]; const GET_INFO = 0; const GET_PAYLOAD_LENGTH_16 = 1; const GET_PAYLOAD_LENGTH_64 = 2; @@ -1136,26 +1165,31 @@ const INFLATING = 5; /** * HyBi Receiver implementation. * - * @extends stream.Writable + * @extends Writable */ class Receiver extends Writable { /** * Creates a Receiver instance. * - * @param {String} [binaryType=nodebuffer] The type for binary data - * @param {Object} [extensions] An object containing the negotiated extensions - * @param {Boolean} [isServer=false] Specifies whether to operate in client or - * server mode - * @param {Number} [maxPayload=0] The maximum allowed message length + * @param {Object} [options] Options object + * @param {String} [options.binaryType=nodebuffer] The type for binary data + * @param {Object} [options.extensions] An object containing the negotiated + * extensions + * @param {Boolean} [options.isServer=false] Specifies whether to operate in + * client or server mode + * @param {Number} [options.maxPayload=0] The maximum allowed message length + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages */ - constructor(binaryType, extensions, isServer, maxPayload) { + constructor(options = {}) { super(); - this._binaryType = binaryType || BINARY_TYPES[0]; + this._binaryType = options.binaryType || BINARY_TYPES[0]; + this._extensions = options.extensions || {}; + this._isServer = !!options.isServer; + this._maxPayload = options.maxPayload | 0; + this._skipUTF8Validation = !!options.skipUTF8Validation; this[kWebSocket] = undefined; - this._extensions = extensions || {}; - this._isServer = !!isServer; - this._maxPayload = maxPayload | 0; this._bufferedBytes = 0; this._buffers = []; @@ -1206,8 +1240,13 @@ class Receiver extends Writable { if (n < this._buffers[0].length) { const buf = this._buffers[0]; - this._buffers[0] = buf.slice(n); - return buf.slice(0, n); + this._buffers[0] = new FastBuffer$1( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); + + return new FastBuffer$1(buf.buffer, buf.byteOffset, n); } const dst = Buffer.allocUnsafe(n); @@ -1220,7 +1259,11 @@ class Receiver extends Writable { dst.set(this._buffers.shift(), offset); } else { dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); - this._buffers[0] = buf.slice(n); + this._buffers[0] = new FastBuffer$1( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); } n -= buf.length; @@ -1282,14 +1325,26 @@ class Receiver extends Writable { if ((buf[0] & 0x30) !== 0x00) { this._loop = false; - return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002); + return error( + RangeError, + 'RSV2 and RSV3 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_2_3' + ); } const compressed = (buf[0] & 0x40) === 0x40; if (compressed && !this._extensions[permessageDeflate.extensionName]) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } this._fin = (buf[0] & 0x80) === 0x80; @@ -1299,45 +1354,85 @@ class Receiver extends Writable { if (this._opcode === 0x00) { if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } if (!this._fragmented) { this._loop = false; - return error(RangeError, 'invalid opcode 0', true, 1002); + return error( + RangeError, + 'invalid opcode 0', + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._opcode = this._fragmented; } else if (this._opcode === 0x01 || this._opcode === 0x02) { if (this._fragmented) { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._compressed = compressed; } else if (this._opcode > 0x07 && this._opcode < 0x0b) { if (!this._fin) { this._loop = false; - return error(RangeError, 'FIN must be set', true, 1002); + return error( + RangeError, + 'FIN must be set', + true, + 1002, + 'WS_ERR_EXPECTED_FIN' + ); } if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } - if (this._payloadLength > 0x7d) { + if ( + this._payloadLength > 0x7d || + (this._opcode === 0x08 && this._payloadLength === 1) + ) { this._loop = false; return error( RangeError, `invalid payload length ${this._payloadLength}`, true, - 1002 + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' ); } } else { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } if (!this._fin && !this._fragmented) this._fragmented = this._opcode; @@ -1346,11 +1441,23 @@ class Receiver extends Writable { if (this._isServer) { if (!this._masked) { this._loop = false; - return error(RangeError, 'MASK must be set', true, 1002); + return error( + RangeError, + 'MASK must be set', + true, + 1002, + 'WS_ERR_EXPECTED_MASK' + ); } } else if (this._masked) { this._loop = false; - return error(RangeError, 'MASK must be clear', true, 1002); + return error( + RangeError, + 'MASK must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_MASK' + ); } if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; @@ -1399,7 +1506,8 @@ class Receiver extends Writable { RangeError, 'Unsupported WebSocket frame: payload length > 2^53 - 1', false, - 1009 + 1009, + 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' ); } @@ -1418,7 +1526,13 @@ class Receiver extends Writable { this._totalPayloadLength += this._payloadLength; if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { this._loop = false; - return error(RangeError, 'Max payload size exceeded', false, 1009); + return error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); } } @@ -1458,7 +1572,13 @@ class Receiver extends Writable { } data = this.consume(this._payloadLength); - if (this._masked) unmask$1(data, this._mask); + + if ( + this._masked && + (this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0 + ) { + unmask$1(data, this._mask); + } } if (this._opcode > 0x07) return this.controlMessage(data); @@ -1471,7 +1591,7 @@ class Receiver extends Writable { if (data.length) { // - // This message is not compressed so its lenght is the sum of the payload + // This message is not compressed so its length is the sum of the payload // length of all fragments. // this._messageLength = this._totalPayloadLength; @@ -1498,7 +1618,13 @@ class Receiver extends Writable { this._messageLength += buf.length; if (this._messageLength > this._maxPayload && this._maxPayload > 0) { return cb( - error(RangeError, 'Max payload size exceeded', false, 1009) + error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ) ); } @@ -1539,16 +1665,22 @@ class Receiver extends Writable { data = fragments; } - this.emit('message', data); + this.emit('message', data, true); } else { const buf = concat(fragments, messageLength); - if (!isValidUTF8$1(buf)) { + if (!this._skipUTF8Validation && !isValidUTF8$1(buf)) { this._loop = false; - return error(Error, 'invalid UTF-8 sequence', true, 1007); + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } - this.emit('message', buf.toString()); + this.emit('message', buf, false); } } @@ -1567,24 +1699,38 @@ class Receiver extends Writable { this._loop = false; if (data.length === 0) { - this.emit('conclude', 1005, ''); + this.emit('conclude', 1005, EMPTY_BUFFER); this.end(); - } else if (data.length === 1) { - return error(RangeError, 'invalid payload length 1', true, 1002); } else { const code = data.readUInt16BE(0); if (!isValidStatusCode(code)) { - return error(RangeError, `invalid status code ${code}`, true, 1002); + return error( + RangeError, + `invalid status code ${code}`, + true, + 1002, + 'WS_ERR_INVALID_CLOSE_CODE' + ); } - const buf = data.slice(2); + const buf = new FastBuffer$1( + data.buffer, + data.byteOffset + 2, + data.length - 2 + ); - if (!isValidUTF8$1(buf)) { - return error(Error, 'invalid UTF-8 sequence', true, 1007); + if (!this._skipUTF8Validation && !isValidUTF8$1(buf)) { + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } - this.emit('conclude', code, buf.toString()); + this.emit('conclude', code, buf); this.end(); } } else if (this._opcode === 0x09) { @@ -1602,32 +1748,35 @@ var receiver = Receiver; /** * Builds an error object. * - * @param {(Error|RangeError)} ErrorCtor The error constructor + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor * @param {String} message The error message * @param {Boolean} prefix Specifies whether or not to add a default prefix to * `message` * @param {Number} statusCode The status code + * @param {String} errorCode The exposed error code * @return {(Error|RangeError)} The error * @private */ -function error(ErrorCtor, message, prefix, statusCode) { +function error(ErrorCtor, message, prefix, statusCode, errorCode) { const err = new ErrorCtor( prefix ? `Invalid WebSocket frame: ${message}` : message ); Error.captureStackTrace(err, error); + err.code = errorCode; err[kStatusCode$1] = statusCode; return err; } -const { randomFillSync } = require$$0$1; +const { randomFillSync } = require$$0$2; const { EMPTY_BUFFER: EMPTY_BUFFER$1 } = constants; const { isValidStatusCode: isValidStatusCode$1 } = validation; const { mask: applyMask, toBuffer } = bufferUtil; -const mask$1 = Buffer.alloc(4); +const kByteLength = Symbol('kByteLength'); +const maskBuffer = Buffer.alloc(4); /** * HyBi Sender implementation. @@ -1636,11 +1785,19 @@ class Sender { /** * Creates a Sender instance. * - * @param {net.Socket} socket The connection socket + * @param {(net.Socket|tls.Socket)} socket The connection socket * @param {Object} [extensions] An object containing the negotiated extensions + * @param {Function} [generateMask] The function used to generate the masking + * key */ - constructor(socket, extensions) { + constructor(socket, extensions, generateMask) { this._extensions = extensions || {}; + + if (generateMask) { + this._generateMask = generateMask; + this._maskBuffer = Buffer.alloc(4); + } + this._socket = socket; this._firstFragment = true; @@ -1654,34 +1811,71 @@ class Sender { /** * Frames a piece of data according to the HyBi WebSocket protocol. * - * @param {Buffer} data The data to frame + * @param {(Buffer|String)} data The data to frame * @param {Object} options Options object - * @param {Number} options.opcode The opcode - * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be - * modified * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit - * @return {Buffer[]} The framed data as a list of `Buffer` instances + * @return {(Buffer|String)[]} The framed data * @public */ static frame(data, options) { - const merge = options.mask && options.readOnly; - let offset = options.mask ? 6 : 2; - let payloadLength = data.length; + let mask; + let merge = false; + let offset = 2; + let skipMasking = false; + + if (options.mask) { + mask = options.maskBuffer || maskBuffer; + + if (options.generateMask) { + options.generateMask(mask); + } else { + randomFillSync(mask, 0, 4); + } + + skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; + offset = 6; + } + + let dataLength; + + if (typeof data === 'string') { + if ( + (!options.mask || skipMasking) && + options[kByteLength] !== undefined + ) { + dataLength = options[kByteLength]; + } else { + data = Buffer.from(data); + dataLength = data.length; + } + } else { + dataLength = data.length; + merge = options.mask && options.readOnly && !skipMasking; + } + + let payloadLength = dataLength; - if (data.length >= 65536) { + if (dataLength >= 65536) { offset += 8; payloadLength = 127; - } else if (data.length > 125) { + } else if (dataLength > 125) { offset += 2; payloadLength = 126; } - const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); + const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset); target[0] = options.fin ? options.opcode | 0x80 : options.opcode; if (options.rsv1) target[0] |= 0x40; @@ -1689,28 +1883,28 @@ class Sender { target[1] = payloadLength; if (payloadLength === 126) { - target.writeUInt16BE(data.length, 2); + target.writeUInt16BE(dataLength, 2); } else if (payloadLength === 127) { - target.writeUInt32BE(0, 2); - target.writeUInt32BE(data.length, 6); + target[2] = target[3] = 0; + target.writeUIntBE(dataLength, 4, 6); } if (!options.mask) return [target, data]; - randomFillSync(mask$1, 0, 4); - target[1] |= 0x80; - target[offset - 4] = mask$1[0]; - target[offset - 3] = mask$1[1]; - target[offset - 2] = mask$1[2]; - target[offset - 1] = mask$1[3]; + target[offset - 4] = mask[0]; + target[offset - 3] = mask[1]; + target[offset - 2] = mask[2]; + target[offset - 1] = mask[3]; + + if (skipMasking) return [target, data]; if (merge) { - applyMask(data, mask$1, target, offset, data.length); + applyMask(data, mask, target, offset, dataLength); return [target]; } - applyMask(data, mask$1, data, 0, data.length); + applyMask(data, mask, data, 0, dataLength); return [target, data]; } @@ -1718,7 +1912,7 @@ class Sender { * Sends a close message to the other peer. * * @param {Number} [code] The status code component of the body - * @param {String} [data] The message component of the body + * @param {(String|Buffer)} [data] The message component of the body * @param {Boolean} [mask=false] Specifies whether or not to mask the message * @param {Function} [cb] Callback * @public @@ -1730,7 +1924,7 @@ class Sender { buf = EMPTY_BUFFER$1; } else if (typeof code !== 'number' || !isValidStatusCode$1(code)) { throw new TypeError('First argument must be a valid error code number'); - } else if (data === undefined || data === '') { + } else if (data === undefined || !data.length) { buf = Buffer.allocUnsafe(2); buf.writeUInt16BE(code, 0); } else { @@ -1742,37 +1936,32 @@ class Sender { buf = Buffer.allocUnsafe(2 + length); buf.writeUInt16BE(code, 0); - buf.write(data, 2); + + if (typeof data === 'string') { + buf.write(data, 2); + } else { + buf.set(data, 2); + } } + const options = { + [kByteLength]: buf.length, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x08, + readOnly: false, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doClose, buf, mask, cb]); + this.enqueue([this.dispatch, buf, false, options, cb]); } else { - this.doClose(buf, mask, cb); + this.sendFrame(Sender.frame(buf, options), cb); } } - /** - * Frames and sends a close message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Function} [cb] Callback - * @private - */ - doClose(data, mask, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x08, - mask, - readOnly: false - }), - cb - ); - } - /** * Sends a ping message to the other peer. * @@ -1782,41 +1971,40 @@ class Sender { * @public */ ping(data, mask, cb) { - const buf = toBuffer(data); + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } - if (buf.length > 125) { + if (byteLength > 125) { throw new RangeError('The data size must not be greater than 125 bytes'); } + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x09, + readOnly, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]); + this.enqueue([this.dispatch, data, false, options, cb]); } else { - this.doPing(buf, mask, toBuffer.readOnly, cb); + this.sendFrame(Sender.frame(data, options), cb); } } - /** - * Frames and sends a ping message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified - * @param {Function} [cb] Callback - * @private - */ - doPing(data, mask, readOnly, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x09, - mask, - readOnly - }), - cb - ); - } - /** * Sends a pong message to the other peer. * @@ -1826,50 +2014,49 @@ class Sender { * @public */ pong(data, mask, cb) { - const buf = toBuffer(data); + let byteLength; + let readOnly; - if (buf.length > 125) { + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + + if (byteLength > 125) { throw new RangeError('The data size must not be greater than 125 bytes'); } + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x0a, + readOnly, + rsv1: false + }; + if (this._deflating) { - this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]); + this.enqueue([this.dispatch, data, false, options, cb]); } else { - this.doPong(buf, mask, toBuffer.readOnly, cb); + this.sendFrame(Sender.frame(data, options), cb); } } - /** - * Frames and sends a pong message. - * - * @param {Buffer} data The message to send - * @param {Boolean} [mask=false] Specifies whether or not to mask `data` - * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified - * @param {Function} [cb] Callback - * @private - */ - doPong(data, mask, readOnly, cb) { - this.sendFrame( - Sender.frame(data, { - fin: true, - rsv1: false, - opcode: 0x0a, - mask, - readOnly - }), - cb - ); - } - /** * Sends a data message to the other peer. * * @param {*} data The message to send * @param {Object} options Options object - * @param {Boolean} [options.compress=false] Specifies whether or not to - * compress `data` * @param {Boolean} [options.binary=false] Specifies whether `data` is binary * or text + * @param {Boolean} [options.compress=false] Specifies whether or not to + * compress `data` * @param {Boolean} [options.fin=false] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask=false] Specifies whether or not to mask @@ -1878,15 +2065,34 @@ class Sender { * @public */ send(data, options, cb) { - const buf = toBuffer(data); const perMessageDeflate = this._extensions[permessageDeflate.extensionName]; let opcode = options.binary ? 2 : 1; let rsv1 = options.compress; + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + if (this._firstFragment) { this._firstFragment = false; - if (rsv1 && perMessageDeflate) { - rsv1 = buf.length >= perMessageDeflate._threshold; + if ( + rsv1 && + perMessageDeflate && + perMessageDeflate.params[ + perMessageDeflate._isServer + ? 'server_no_context_takeover' + : 'client_no_context_takeover' + ] + ) { + rsv1 = byteLength >= perMessageDeflate._threshold; } this._compress = rsv1; } else { @@ -1898,26 +2104,32 @@ class Sender { if (perMessageDeflate) { const opts = { + [kByteLength]: byteLength, fin: options.fin, - rsv1, - opcode, + generateMask: this._generateMask, mask: options.mask, - readOnly: toBuffer.readOnly + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1 }; if (this._deflating) { - this.enqueue([this.dispatch, buf, this._compress, opts, cb]); + this.enqueue([this.dispatch, data, this._compress, opts, cb]); } else { - this.dispatch(buf, this._compress, opts, cb); + this.dispatch(data, this._compress, opts, cb); } } else { this.sendFrame( - Sender.frame(buf, { + Sender.frame(data, { + [kByteLength]: byteLength, fin: options.fin, - rsv1: false, - opcode, + generateMask: this._generateMask, mask: options.mask, - readOnly: toBuffer.readOnly + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1: false }), cb ); @@ -1925,19 +2137,23 @@ class Sender { } /** - * Dispatches a data message. + * Dispatches a message. * - * @param {Buffer} data The message to send + * @param {(Buffer|String)} data The message to send * @param {Boolean} [compress=false] Specifies whether or not to compress * `data` * @param {Object} options Options object - * @param {Number} options.opcode The opcode - * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be - * modified * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit * @param {Function} [cb] Callback @@ -1951,7 +2167,7 @@ class Sender { const perMessageDeflate = this._extensions[permessageDeflate.extensionName]; - this._bufferedBytes += data.length; + this._bufferedBytes += options[kByteLength]; this._deflating = true; perMessageDeflate.compress(data, options.fin, (_, buf) => { if (this._socket.destroyed) { @@ -1962,7 +2178,8 @@ class Sender { if (typeof cb === 'function') cb(err); for (let i = 0; i < this._queue.length; i++) { - const callback = this._queue[i][4]; + const params = this._queue[i]; + const callback = params[params.length - 1]; if (typeof callback === 'function') callback(err); } @@ -1970,7 +2187,7 @@ class Sender { return; } - this._bufferedBytes -= data.length; + this._bufferedBytes -= options[kByteLength]; this._deflating = false; options.readOnly = false; this.sendFrame(Sender.frame(buf, options), cb); @@ -1987,7 +2204,7 @@ class Sender { while (!this._deflating && this._queue.length) { const params = this._queue.shift(); - this._bufferedBytes -= params[1].length; + this._bufferedBytes -= params[3][kByteLength]; Reflect.apply(params[0], this, params.slice(1)); } } @@ -1999,7 +2216,7 @@ class Sender { * @private */ enqueue(params) { - this._bufferedBytes += params[1].length; + this._bufferedBytes += params[3][kByteLength]; this._queue.push(params); } @@ -2024,112 +2241,173 @@ class Sender { var sender = Sender; +const { kForOnEventAttribute, kListener } = constants; + +const kCode = Symbol('kCode'); +const kData = Symbol('kData'); +const kError$1 = Symbol('kError'); +const kMessage = Symbol('kMessage'); +const kReason = Symbol('kReason'); +const kTarget = Symbol('kTarget'); +const kType = Symbol('kType'); +const kWasClean = Symbol('kWasClean'); + /** * Class representing an event. - * - * @private */ class Event { /** * Create a new `Event`. * * @param {String} type The name of the event - * @param {Object} target A reference to the target to which the event was - * dispatched + * @throws {TypeError} If the `type` argument is not specified */ - constructor(type, target) { - this.target = target; - this.type = type; + constructor(type) { + this[kTarget] = null; + this[kType] = type; } -} -/** - * Class representing a message event. - * - * @extends Event - * @private - */ -class MessageEvent extends Event { /** - * Create a new `MessageEvent`. - * - * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @type {*} */ - constructor(data, target) { - super('message', target); + get target() { + return this[kTarget]; + } - this.data = data; + /** + * @type {String} + */ + get type() { + return this[kType]; } } +Object.defineProperty(Event.prototype, 'target', { enumerable: true }); +Object.defineProperty(Event.prototype, 'type', { enumerable: true }); + /** * Class representing a close event. * * @extends Event - * @private */ class CloseEvent extends Event { /** * Create a new `CloseEvent`. * - * @param {Number} code The status code explaining why the connection is being - * closed - * @param {String} reason A human-readable string explaining why the - * connection is closing - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {Number} [options.code=0] The status code explaining why the + * connection was closed + * @param {String} [options.reason=''] A human-readable string explaining why + * the connection was closed + * @param {Boolean} [options.wasClean=false] Indicates whether or not the + * connection was cleanly closed */ - constructor(code, reason, target) { - super('close', target); + constructor(type, options = {}) { + super(type); + + this[kCode] = options.code === undefined ? 0 : options.code; + this[kReason] = options.reason === undefined ? '' : options.reason; + this[kWasClean] = options.wasClean === undefined ? false : options.wasClean; + } - this.wasClean = target._closeFrameReceived && target._closeFrameSent; - this.reason = reason; - this.code = code; + /** + * @type {Number} + */ + get code() { + return this[kCode]; + } + + /** + * @type {String} + */ + get reason() { + return this[kReason]; + } + + /** + * @type {Boolean} + */ + get wasClean() { + return this[kWasClean]; } } +Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true }); + /** - * Class representing an open event. + * Class representing an error event. * * @extends Event - * @private */ -class OpenEvent extends Event { +class ErrorEvent extends Event { /** - * Create a new `OpenEvent`. + * Create a new `ErrorEvent`. * - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.error=null] The error that generated this event + * @param {String} [options.message=''] The error message + */ + constructor(type, options = {}) { + super(type); + + this[kError$1] = options.error === undefined ? null : options.error; + this[kMessage] = options.message === undefined ? '' : options.message; + } + + /** + * @type {*} + */ + get error() { + return this[kError$1]; + } + + /** + * @type {String} */ - constructor(target) { - super('open', target); + get message() { + return this[kMessage]; } } +Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true }); +Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true }); + /** - * Class representing an error event. + * Class representing a message event. * * @extends Event - * @private */ -class ErrorEvent extends Event { +class MessageEvent extends Event { /** - * Create a new `ErrorEvent`. + * Create a new `MessageEvent`. * - * @param {Object} error The error that generated this event - * @param {WebSocket} target A reference to the target to which the event was - * dispatched + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.data=null] The message content */ - constructor(error, target) { - super('error', target); + constructor(type, options = {}) { + super(type); - this.message = error.message; - this.error = error; + this[kData] = options.data === undefined ? null : options.data; + } + + /** + * @type {*} + */ + get data() { + return this[kData]; } } +Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true }); + /** * This provides methods for emulating the `EventTarget` interface. It's not * meant to be used directly. @@ -2141,49 +2419,75 @@ const EventTarget = { * Register an event listener. * * @param {String} type A string representing the event type to listen for - * @param {Function} listener The listener to add + * @param {(Function|Object)} handler The listener to add * @param {Object} [options] An options object specifies characteristics about * the event listener - * @param {Boolean} [options.once=false] A `Boolean`` indicating that the + * @param {Boolean} [options.once=false] A `Boolean` indicating that the * listener should be invoked at most once after being added. If `true`, * the listener would be automatically removed when invoked. * @public */ - addEventListener(type, listener, options) { - if (typeof listener !== 'function') return; - - function onMessage(data) { - listener.call(this, new MessageEvent(data, this)); - } - - function onClose(code, message) { - listener.call(this, new CloseEvent(code, message, this)); - } - - function onError(error) { - listener.call(this, new ErrorEvent(error, this)); - } - - function onOpen() { - listener.call(this, new OpenEvent(this)); + addEventListener(type, handler, options = {}) { + for (const listener of this.listeners(type)) { + if ( + !options[kForOnEventAttribute] && + listener[kListener] === handler && + !listener[kForOnEventAttribute] + ) { + return; + } } - const method = options && options.once ? 'once' : 'on'; + let wrapper; if (type === 'message') { - onMessage._listener = listener; - this[method](type, onMessage); + wrapper = function onMessage(data, isBinary) { + const event = new MessageEvent('message', { + data: isBinary ? data : data.toString() + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'close') { - onClose._listener = listener; - this[method](type, onClose); + wrapper = function onClose(code, message) { + const event = new CloseEvent('close', { + code, + reason: message.toString(), + wasClean: this._closeFrameReceived && this._closeFrameSent + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'error') { - onError._listener = listener; - this[method](type, onError); + wrapper = function onError(error) { + const event = new ErrorEvent('error', { + error, + message: error.message + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else if (type === 'open') { - onOpen._listener = listener; - this[method](type, onOpen); + wrapper = function onOpen() { + const event = new Event('open'); + + event[kTarget] = this; + callListener(handler, this, event); + }; } else { - this[method](type, listener); + return; + } + + wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; + wrapper[kListener] = handler; + + if (options.once) { + this.once(type, wrapper); + } else { + this.on(type, wrapper); } }, @@ -2191,44 +2495,44 @@ const EventTarget = { * Remove an event listener. * * @param {String} type A string representing the event type to remove - * @param {Function} listener The listener to remove + * @param {(Function|Object)} handler The listener to remove * @public */ - removeEventListener(type, listener) { - const listeners = this.listeners(type); - - for (let i = 0; i < listeners.length; i++) { - if (listeners[i] === listener || listeners[i]._listener === listener) { - this.removeListener(type, listeners[i]); + removeEventListener(type, handler) { + for (const listener of this.listeners(type)) { + if (listener[kListener] === handler && !listener[kForOnEventAttribute]) { + this.removeListener(type, listener); + break; } } } }; -var eventTarget = EventTarget; +var eventTarget = { + CloseEvent, + ErrorEvent, + Event, + EventTarget, + MessageEvent +}; + +/** + * Call an event listener + * + * @param {(Function|Object)} listener The listener to call + * @param {*} thisArg The value to use as `this`` when calling the listener + * @param {Event} event The event to pass to the listener + * @private + */ +function callListener(listener, thisArg, event) { + if (typeof listener === 'object' && listener.handleEvent) { + listener.handleEvent.call(listener, event); + } else { + listener.call(thisArg, event); + } +} -// -// Allowed token characters: -// -// '!', '#', '$', '%', '&', ''', '*', '+', '-', -// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' -// -// tokenChars[32] === 0 // ' ' -// tokenChars[33] === 1 // '!' -// tokenChars[34] === 0 // '"' -// ... -// -// prettier-ignore -const tokenChars = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 - 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 -]; +const { tokenChars } = validation; /** * Adds an offer to the map of extension offers or a parameter to the map of @@ -2254,24 +2558,25 @@ function push(dest, name, elem) { */ function parse(header) { const offers = Object.create(null); - - if (header === undefined || header === '') return offers; - let params = Object.create(null); let inQuotes = false; let extensionName; let paramName; let start = -1; + let code = -1; let end = -1; let i = 0; for (; i < header.length; i++) { - const code = header.charCodeAt(i); + code = header.charCodeAt(i); if (extensionName === undefined) { if (end === -1 && tokenChars[code] === 1) { if (start === -1) start = i; - } else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) { + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { if (end === -1 && start !== -1) end = i; } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { if (start === -1) { @@ -2359,7 +2664,7 @@ function parse(header) { } } - if (start === -1 || inQuotes) { + if (start === -1 || inQuotes || code === 0x20 || code === 0x09) { throw new SyntaxError('Unexpected end of input'); } @@ -2412,8 +2717,8 @@ function format(extensions) { var extension = { format, parse }; -const { randomBytes, createHash } = require$$0$1; -const { URL } = require$$1; +const { randomBytes, createHash } = require$$0$2; +const { URL } = require$$2; @@ -2422,17 +2727,23 @@ const { BINARY_TYPES: BINARY_TYPES$1, EMPTY_BUFFER: EMPTY_BUFFER$2, GUID, + kForOnEventAttribute: kForOnEventAttribute$1, + kListener: kListener$1, kStatusCode: kStatusCode$2, kWebSocket: kWebSocket$1, - NOOP: NOOP$1 + NOOP } = constants; -const { addEventListener, removeEventListener } = eventTarget; +const { + EventTarget: { addEventListener, removeEventListener } +} = eventTarget; const { format: format$1, parse: parse$1 } = extension; const { toBuffer: toBuffer$1 } = bufferUtil; -const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; -const protocolVersions = [8, 13]; const closeTimeout = 30 * 1000; +const kAborted = Symbol('kAborted'); +const protocolVersions = [8, 13]; +const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; +const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/; /** * Class representing a WebSocket. @@ -2443,7 +2754,7 @@ class WebSocket extends EventEmitter { /** * Create a new `WebSocket`. * - * @param {(String|url.URL)} address The URL to which to connect + * @param {(String|URL)} address The URL to which to connect * @param {(String|String[])} [protocols] The subprotocols * @param {Object} [options] Connection options */ @@ -2454,9 +2765,10 @@ class WebSocket extends EventEmitter { this._closeCode = 1006; this._closeFrameReceived = false; this._closeFrameSent = false; - this._closeMessage = ''; + this._closeMessage = EMPTY_BUFFER$2; this._closeTimer = null; this._extensions = {}; + this._paused = false; this._protocol = ''; this._readyState = WebSocket.CONNECTING; this._receiver = null; @@ -2468,11 +2780,15 @@ class WebSocket extends EventEmitter { this._isServer = false; this._redirects = 0; - if (Array.isArray(protocols)) { - protocols = protocols.join(', '); - } else if (typeof protocols === 'object' && protocols !== null) { - options = protocols; - protocols = undefined; + if (protocols === undefined) { + protocols = []; + } else if (!Array.isArray(protocols)) { + if (typeof protocols === 'object' && protocols !== null) { + options = protocols; + protocols = []; + } else { + protocols = [protocols]; + } } initAsClient(this, address, protocols, options); @@ -2519,6 +2835,45 @@ class WebSocket extends EventEmitter { return Object.keys(this._extensions).join(); } + /** + * @type {Boolean} + */ + get isPaused() { + return this._paused; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onclose() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onerror() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onopen() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onmessage() { + return null; + } + /** * @type {String} */ @@ -2543,20 +2898,27 @@ class WebSocket extends EventEmitter { /** * Set up the socket and the internal resources. * - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream - * @param {Number} [maxPayload=0] The maximum allowed message size + * @param {Object} options Options object + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Number} [options.maxPayload=0] The maximum allowed message size + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ - setSocket(socket, head, maxPayload) { - const receiver$1 = new receiver( - this.binaryType, - this._extensions, - this._isServer, - maxPayload - ); + setSocket(socket, head, options) { + const receiver$1 = new receiver({ + binaryType: this.binaryType, + extensions: this._extensions, + isServer: this._isServer, + maxPayload: options.maxPayload, + skipUTF8Validation: options.skipUTF8Validation + }); - this._sender = new sender(socket, this._extensions); + this._sender = new sender(socket, this._extensions, options.generateMask); this._receiver = receiver$1; this._socket = socket; @@ -2621,18 +2983,26 @@ class WebSocket extends EventEmitter { * +---+ * * @param {Number} [code] Status code explaining why the connection is closing - * @param {String} [data] A string explaining why the connection is closing + * @param {(String|Buffer)} [data] The reason why the connection is + * closing * @public */ close(code, data) { if (this.readyState === WebSocket.CLOSED) return; if (this.readyState === WebSocket.CONNECTING) { const msg = 'WebSocket was closed before the connection was established'; - return abortHandshake(this, this._req, msg); + abortHandshake(this, this._req, msg); + return; } if (this.readyState === WebSocket.CLOSING) { - if (this._closeFrameSent && this._closeFrameReceived) this._socket.end(); + if ( + this._closeFrameSent && + (this._closeFrameReceived || this._receiver._writableState.errorEmitted) + ) { + this._socket.end(); + } + return; } @@ -2645,7 +3015,13 @@ class WebSocket extends EventEmitter { if (err) return; this._closeFrameSent = true; - if (this._closeFrameReceived) this._socket.end(); + + if ( + this._closeFrameReceived || + this._receiver._writableState.errorEmitted + ) { + this._socket.end(); + } }); // @@ -2657,6 +3033,23 @@ class WebSocket extends EventEmitter { ); } + /** + * Pause the socket. + * + * @public + */ + pause() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = true; + this._socket.pause(); + } + /** * Send a ping. * @@ -2721,15 +3114,32 @@ class WebSocket extends EventEmitter { this._sender.pong(data || EMPTY_BUFFER$2, mask, cb); } + /** + * Resume the socket. + * + * @public + */ + resume() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = false; + if (!this._receiver._writableState.needDrain) this._socket.resume(); + } + /** * Send a data message. * * @param {*} data The message to send * @param {Object} [options] Options object - * @param {Boolean} [options.compress] Specifies whether or not to compress - * `data` * @param {Boolean} [options.binary] Specifies whether `data` is binary or * text + * @param {Boolean} [options.compress] Specifies whether or not to compress + * `data` * @param {Boolean} [options.fin=true] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask] Specifies whether or not to mask `data` @@ -2777,7 +3187,8 @@ class WebSocket extends EventEmitter { if (this.readyState === WebSocket.CLOSED) return; if (this.readyState === WebSocket.CONNECTING) { const msg = 'WebSocket was closed before the connection was established'; - return abortHandshake(this, this._req, msg); + abortHandshake(this, this._req, msg); + return; } if (this._socket) { @@ -2787,17 +3198,83 @@ class WebSocket extends EventEmitter { } } -readyStates.forEach((readyState, i) => { - const descriptor = { enumerable: true, value: i }; +/** + * @constant {Number} CONNECTING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} CONNECTING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); - Object.defineProperty(WebSocket.prototype, readyState, descriptor); - Object.defineProperty(WebSocket, readyState, descriptor); +/** + * @constant {Number} CLOSED + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') }); [ 'binaryType', 'bufferedAmount', 'extensions', + 'isPaused', 'protocol', 'readyState', 'url' @@ -2811,37 +3288,27 @@ readyStates.forEach((readyState, i) => { // ['open', 'error', 'close', 'message'].forEach((method) => { Object.defineProperty(WebSocket.prototype, `on${method}`, { - configurable: true, enumerable: true, - /** - * Return the listener of the event. - * - * @return {(Function|undefined)} The event listener or `undefined` - * @public - */ get() { - const listeners = this.listeners(method); - for (let i = 0; i < listeners.length; i++) { - if (listeners[i]._listener) return listeners[i]._listener; + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute$1]) return listener[kListener$1]; } - return undefined; + return null; }, - /** - * Add a listener for the event. - * - * @param {Function} listener The listener to add - * @public - */ - set(listener) { - const listeners = this.listeners(method); - for (let i = 0; i < listeners.length; i++) { - // - // Remove only the listeners added via `addEventListener`. - // - if (listeners[i]._listener) this.removeListener(method, listeners[i]); + set(handler) { + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute$1]) { + this.removeListener(method, listener); + break; + } } - this.addEventListener(method, listener); + + if (typeof handler !== 'function') return; + + this.addEventListener(method, handler, { + [kForOnEventAttribute$1]: true + }); } }); }); @@ -2855,29 +3322,34 @@ var websocket = WebSocket; * Initialize a WebSocket client. * * @param {WebSocket} websocket The client to initialize - * @param {(String|url.URL)} address The URL to which to connect - * @param {String} [protocols] The subprotocols + * @param {(String|URL)} address The URL to which to connect + * @param {Array} protocols The subprotocols * @param {Object} [options] Connection options - * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable - * permessage-deflate + * @param {Boolean} [options.followRedirects=false] Whether or not to follow + * redirects + * @param {Function} [options.generateMask] The function used to generate the + * masking key * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the * handshake request - * @param {Number} [options.protocolVersion=13] Value of the - * `Sec-WebSocket-Version` header - * @param {String} [options.origin] Value of the `Origin` or - * `Sec-WebSocket-Origin` header * @param {Number} [options.maxPayload=104857600] The maximum allowed message * size - * @param {Boolean} [options.followRedirects=false] Whether or not to follow - * redirects * @param {Number} [options.maxRedirects=10] The maximum number of redirects * allowed + * @param {String} [options.origin] Value of the `Origin` or + * `Sec-WebSocket-Origin` header + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable + * permessage-deflate + * @param {Number} [options.protocolVersion=13] Value of the + * `Sec-WebSocket-Version` header + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @private */ function initAsClient(websocket, address, protocols, options) { const opts = { protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: true, followRedirects: false, maxRedirects: 10, @@ -2887,7 +3359,7 @@ function initAsClient(websocket, address, protocols, options) { hostname: undefined, protocol: undefined, timeout: undefined, - method: undefined, + method: 'GET', host: undefined, path: undefined, port: undefined @@ -2906,21 +3378,43 @@ function initAsClient(websocket, address, protocols, options) { parsedUrl = address; websocket._url = address.href; } else { - parsedUrl = new URL(address); + try { + parsedUrl = new URL(address); + } catch (e) { + throw new SyntaxError(`Invalid URL: ${address}`); + } + websocket._url = address; } - const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; + const isSecure = parsedUrl.protocol === 'wss:'; + const isIpcUrl = parsedUrl.protocol === 'ws+unix:'; + let invalidUrlMessage; - if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) { - throw new Error(`Invalid URL: ${websocket.url}`); + if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { + invalidUrlMessage = + 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; + } else if (isIpcUrl && !parsedUrl.pathname) { + invalidUrlMessage = "The URL's pathname is empty"; + } else if (parsedUrl.hash) { + invalidUrlMessage = 'The URL contains a fragment identifier'; + } + + if (invalidUrlMessage) { + const err = new SyntaxError(invalidUrlMessage); + + if (websocket._redirects === 0) { + throw err; + } else { + emitErrorAndClose(websocket, err); + return; + } } - const isSecure = - parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:'; const defaultPort = isSecure ? 443 : 80; const key = randomBytes(16).toString('base64'); - const get = isSecure ? https.get : http.get; + const request = isSecure ? https.request : http.request; + const protocolSet = new Set(); let perMessageDeflate; opts.createConnection = isSecure ? tlsConnect : netConnect; @@ -2930,11 +3424,11 @@ function initAsClient(websocket, address, protocols, options) { ? parsedUrl.hostname.slice(1, -1) : parsedUrl.hostname; opts.headers = { + ...opts.headers, 'Sec-WebSocket-Version': opts.protocolVersion, 'Sec-WebSocket-Key': key, Connection: 'Upgrade', - Upgrade: 'websocket', - ...opts.headers + Upgrade: 'websocket' }; opts.path = parsedUrl.pathname + parsedUrl.search; opts.timeout = opts.handshakeTimeout; @@ -2949,8 +3443,22 @@ function initAsClient(websocket, address, protocols, options) { [permessageDeflate.extensionName]: perMessageDeflate.offer() }); } - if (protocols) { - opts.headers['Sec-WebSocket-Protocol'] = protocols; + if (protocols.length) { + for (const protocol of protocols) { + if ( + typeof protocol !== 'string' || + !subprotocolRegex.test(protocol) || + protocolSet.has(protocol) + ) { + throw new SyntaxError( + 'An invalid or duplicated subprotocol was specified' + ); + } + + protocolSet.add(protocol); + } + + opts.headers['Sec-WebSocket-Protocol'] = protocols.join(','); } if (opts.origin) { if (opts.protocolVersion < 13) { @@ -2963,14 +3471,86 @@ function initAsClient(websocket, address, protocols, options) { opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; } - if (isUnixSocket) { + if (isIpcUrl) { const parts = opts.path.split(':'); opts.socketPath = parts[0]; opts.path = parts[1]; } - let req = (websocket._req = get(opts)); + let req; + + if (opts.followRedirects) { + if (websocket._redirects === 0) { + websocket._originalIpc = isIpcUrl; + websocket._originalSecure = isSecure; + websocket._originalHostOrSocketPath = isIpcUrl + ? opts.socketPath + : parsedUrl.host; + + const headers = options && options.headers; + + // + // Shallow copy the user provided options so that headers can be changed + // without mutating the original object. + // + options = { ...options, headers: {} }; + + if (headers) { + for (const [key, value] of Object.entries(headers)) { + options.headers[key.toLowerCase()] = value; + } + } + } else if (websocket.listenerCount('redirect') === 0) { + const isSameHost = isIpcUrl + ? websocket._originalIpc + ? opts.socketPath === websocket._originalHostOrSocketPath + : false + : websocket._originalIpc + ? false + : parsedUrl.host === websocket._originalHostOrSocketPath; + + if (!isSameHost || (websocket._originalSecure && !isSecure)) { + // + // Match curl 7.77.0 behavior and drop the following headers. These + // headers are also dropped when following a redirect to a subdomain. + // + delete opts.headers.authorization; + delete opts.headers.cookie; + + if (!isSameHost) delete opts.headers.host; + + opts.auth = undefined; + } + } + + // + // Match curl 7.77.0 behavior and make the first `Authorization` header win. + // If the `Authorization` header is set, then there is nothing to do as it + // will take precedence. + // + if (opts.auth && !options.headers.authorization) { + options.headers.authorization = + 'Basic ' + Buffer.from(opts.auth).toString('base64'); + } + + req = websocket._req = request(opts); + + if (websocket._redirects) { + // + // Unlike what is done for the `'upgrade'` event, no early exit is + // triggered here if the user calls `websocket.close()` or + // `websocket.terminate()` from a listener of the `'redirect'` event. This + // is because the user can also call `request.destroy()` with an error + // before calling `websocket.close()` or `websocket.terminate()` and this + // would result in an error being emitted on the `request` object with no + // `'error'` event listeners attached. + // + websocket.emit('redirect', websocket.url, req); + } + } else { + req = websocket._req = request(opts); + } if (opts.timeout) { req.on('timeout', () => { @@ -2979,12 +3559,10 @@ function initAsClient(websocket, address, protocols, options) { } req.on('error', (err) => { - if (req === null || req.aborted) return; + if (req === null || req[kAborted]) return; req = websocket._req = null; - websocket._readyState = WebSocket.CLOSING; - websocket.emit('error', err); - websocket.emitClose(); + emitErrorAndClose(websocket, err); }); req.on('response', (res) => { @@ -3004,7 +3582,15 @@ function initAsClient(websocket, address, protocols, options) { req.abort(); - const addr = new URL(location, address); + let addr; + + try { + addr = new URL(location, address); + } catch (e) { + const err = new SyntaxError(`Invalid URL: ${location}`); + emitErrorAndClose(websocket, err); + return; + } initAsClient(websocket, addr, protocols, options); } else if (!websocket.emit('unexpected-response', req, res)) { @@ -3020,13 +3606,18 @@ function initAsClient(websocket, address, protocols, options) { websocket.emit('upgrade', res); // - // The user may have closed the connection from a listener of the `upgrade` - // event. + // The user may have closed the connection from a listener of the + // `'upgrade'` event. // if (websocket.readyState !== WebSocket.CONNECTING) return; req = websocket._req = null; + if (res.headers.upgrade.toLowerCase() !== 'websocket') { + abortHandshake(websocket, socket, 'Invalid Upgrade header'); + return; + } + const digest = createHash('sha1') .update(key + GUID) .digest('base64'); @@ -3037,15 +3628,16 @@ function initAsClient(websocket, address, protocols, options) { } const serverProt = res.headers['sec-websocket-protocol']; - const protList = (protocols || '').split(/, */); let protError; - if (!protocols && serverProt) { - protError = 'Server sent a subprotocol but none was requested'; - } else if (protocols && !serverProt) { + if (serverProt !== undefined) { + if (!protocolSet.size) { + protError = 'Server sent a subprotocol but none was requested'; + } else if (!protocolSet.has(serverProt)) { + protError = 'Server sent an invalid subprotocol'; + } + } else if (protocolSet.size) { protError = 'Server sent no subprotocol'; - } else if (serverProt && !protList.includes(serverProt)) { - protError = 'Server sent an invalid subprotocol'; } if (protError) { @@ -3055,28 +3647,75 @@ function initAsClient(websocket, address, protocols, options) { if (serverProt) websocket._protocol = serverProt; - if (perMessageDeflate) { + const secWebSocketExtensions = res.headers['sec-websocket-extensions']; + + if (secWebSocketExtensions !== undefined) { + if (!perMessageDeflate) { + const message = + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested'; + abortHandshake(websocket, socket, message); + return; + } + + let extensions; + try { - const extensions = parse$1(res.headers['sec-websocket-extensions']); + extensions = parse$1(secWebSocketExtensions); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } - if (extensions[permessageDeflate.extensionName]) { - perMessageDeflate.accept(extensions[permessageDeflate.extensionName]); - websocket._extensions[ - permessageDeflate.extensionName - ] = perMessageDeflate; - } + const extensionNames = Object.keys(extensions); + + if ( + extensionNames.length !== 1 || + extensionNames[0] !== permessageDeflate.extensionName + ) { + const message = 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } + + try { + perMessageDeflate.accept(extensions[permessageDeflate.extensionName]); } catch (err) { - abortHandshake( - websocket, - socket, - 'Invalid Sec-WebSocket-Extensions header' - ); + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); return; } + + websocket._extensions[permessageDeflate.extensionName] = + perMessageDeflate; } - websocket.setSocket(socket, head, opts.maxPayload); + websocket.setSocket(socket, head, { + generateMask: opts.generateMask, + maxPayload: opts.maxPayload, + skipUTF8Validation: opts.skipUTF8Validation + }); }); + + if (opts.finishRequest) { + opts.finishRequest(req, websocket); + } else { + req.end(); + } +} + +/** + * Emit the `'error'` and `'close'` events. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {Error} The error to emit + * @private + */ +function emitErrorAndClose(websocket, err) { + websocket._readyState = WebSocket.CLOSING; + websocket.emit('error', err); + websocket.emitClose(); } /** @@ -3112,8 +3751,8 @@ function tlsConnect(options) { * Abort the handshake and emit an error. * * @param {WebSocket} websocket The WebSocket instance - * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the - * socket to destroy + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to + * abort or the socket to destroy * @param {String} message The error message * @private */ @@ -3124,6 +3763,7 @@ function abortHandshake(websocket, stream, message) { Error.captureStackTrace(err, abortHandshake); if (stream.setHeader) { + stream[kAborted] = true; stream.abort(); if (stream.socket && !stream.socket.destroyed) { @@ -3135,8 +3775,7 @@ function abortHandshake(websocket, stream, message) { stream.socket.destroy(); } - stream.once('abort', websocket.emitClose.bind(websocket)); - websocket.emit('error', err); + process.nextTick(emitErrorAndClose, websocket, err); } else { stream.destroy(err); stream.once('error', websocket.emit.bind(websocket, 'error')); @@ -3172,7 +3811,7 @@ function sendAfterClose(websocket, data, cb) { `WebSocket is not open: readyState ${websocket.readyState} ` + `(${readyStates[websocket.readyState]})` ); - cb(err); + process.nextTick(cb, err); } } @@ -3180,19 +3819,21 @@ function sendAfterClose(websocket, data, cb) { * The listener of the `Receiver` `'conclude'` event. * * @param {Number} code The status code - * @param {String} reason The reason for closing + * @param {Buffer} reason The reason for closing * @private */ function receiverOnConclude(code, reason) { const websocket = this[kWebSocket$1]; - websocket._socket.removeListener('data', socketOnData); - websocket._socket.resume(); - websocket._closeFrameReceived = true; websocket._closeMessage = reason; websocket._closeCode = code; + if (websocket._socket[kWebSocket$1] === undefined) return; + + websocket._socket.removeListener('data', socketOnData); + process.nextTick(resume, websocket._socket); + if (code === 1005) websocket.close(); else websocket.close(code, reason); } @@ -3203,7 +3844,9 @@ function receiverOnConclude(code, reason) { * @private */ function receiverOnDrain() { - this[kWebSocket$1]._socket.resume(); + const websocket = this[kWebSocket$1]; + + if (!websocket.isPaused) websocket._socket.resume(); } /** @@ -3215,12 +3858,19 @@ function receiverOnDrain() { function receiverOnError(err) { const websocket = this[kWebSocket$1]; - websocket._socket.removeListener('data', socketOnData); + if (websocket._socket[kWebSocket$1] !== undefined) { + websocket._socket.removeListener('data', socketOnData); + + // + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See + // https://github.com/websockets/ws/issues/1940. + // + process.nextTick(resume, websocket._socket); + + websocket.close(err[kStatusCode$2]); + } - websocket._readyState = WebSocket.CLOSING; - websocket._closeCode = err[kStatusCode$2]; websocket.emit('error', err); - websocket._socket.destroy(); } /** @@ -3235,11 +3885,12 @@ function receiverOnFinish() { /** * The listener of the `Receiver` `'message'` event. * - * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Boolean} isBinary Specifies whether the message is binary or not * @private */ -function receiverOnMessage(data) { - this[kWebSocket$1].emit('message', data); +function receiverOnMessage(data, isBinary) { + this[kWebSocket$1].emit('message', data, isBinary); } /** @@ -3251,7 +3902,7 @@ function receiverOnMessage(data) { function receiverOnPing(data) { const websocket = this[kWebSocket$1]; - websocket.pong(data, !websocket._isServer, NOOP$1); + websocket.pong(data, !websocket._isServer, NOOP); websocket.emit('ping', data); } @@ -3265,6 +3916,16 @@ function receiverOnPong(data) { this[kWebSocket$1].emit('pong', data); } +/** + * Resume a readable stream + * + * @param {Readable} stream The readable stream + * @private + */ +function resume(stream) { + stream.resume(); +} + /** * The listener of the `net.Socket` `'close'` event. * @@ -3274,10 +3935,13 @@ function socketOnClose() { const websocket = this[kWebSocket$1]; this.removeListener('close', socketOnClose); + this.removeListener('data', socketOnData); this.removeListener('end', socketOnEnd); websocket._readyState = WebSocket.CLOSING; + let chunk; + // // The close frame might not have been received or the `'end'` event emitted, // for example, if the socket was destroyed due to an error. Ensure that the @@ -3285,13 +3949,19 @@ function socketOnClose() { // it. If the readable side of the socket is in flowing mode then there is no // buffered data as everything has been already written and `readable.read()` // will return `null`. If instead, the socket is paused, any possible buffered - // data will be read as a single chunk and emitted synchronously in a single - // `'data'` event. + // data will be read as a single chunk. // - websocket._socket.read(); + if ( + !this._readableState.endEmitted && + !websocket._closeFrameReceived && + !websocket._receiver._writableState.errorEmitted && + (chunk = websocket._socket.read()) !== null + ) { + websocket._receiver.write(chunk); + } + websocket._receiver.end(); - this.removeListener('data', socketOnData); this[kWebSocket$1] = undefined; clearTimeout(websocket._closeTimer); @@ -3341,7 +4011,7 @@ function socketOnError() { const websocket = this[kWebSocket$1]; this.removeListener('error', socketOnError); - this.on('error', NOOP$1); + this.on('error', NOOP); if (websocket) { websocket._readyState = WebSocket.CLOSING; @@ -3349,12 +4019,12 @@ function socketOnError() { } } -const { Duplex } = require$$0; +const { Duplex } = require$$0$1; /** * Emits the `'close'` event on a stream. * - * @param {stream.Duplex} The stream. + * @param {Duplex} stream The stream. * @private */ function emitClose(stream) { @@ -3392,25 +4062,11 @@ function duplexOnError(err) { * * @param {WebSocket} ws The `WebSocket` to wrap * @param {Object} [options] The options for the `Duplex` constructor - * @return {stream.Duplex} The duplex stream + * @return {Duplex} The duplex stream * @public */ function createWebSocketStream(ws, options) { - let resumeOnReceiverDrain = true; - - function receiverOnDrain() { - if (resumeOnReceiverDrain) ws._socket.resume(); - } - - if (ws.readyState === ws.CONNECTING) { - ws.once('open', function open() { - ws._receiver.removeAllListeners('drain'); - ws._receiver.on('drain', receiverOnDrain); - }); - } else { - ws._receiver.removeAllListeners('drain'); - ws._receiver.on('drain', receiverOnDrain); - } + let terminateOnDestroy = true; const duplex = new Duplex({ ...options, @@ -3420,16 +4076,26 @@ function createWebSocketStream(ws, options) { writableObjectMode: false }); - ws.on('message', function message(msg) { - if (!duplex.push(msg)) { - resumeOnReceiverDrain = false; - ws._socket.pause(); - } + ws.on('message', function message(msg, isBinary) { + const data = + !isBinary && duplex._readableState.objectMode ? msg.toString() : msg; + + if (!duplex.push(data)) ws.pause(); }); ws.once('error', function error(err) { if (duplex.destroyed) return; + // Prevent `ws.terminate()` from being called by `duplex._destroy()`. + // + // - If the `'error'` event is emitted before the `'open'` event, then + // `ws.terminate()` is a noop as no socket is assigned. + // - Otherwise, the error is re-emitted by the listener of the `'error'` + // event of the `Receiver` object. The listener already closes the + // connection by calling `ws.close()`. This allows a close frame to be + // sent to the other peer. If `ws.terminate()` is called right after this, + // then the close frame might not be sent. + terminateOnDestroy = false; duplex.destroy(err); }); @@ -3457,7 +4123,8 @@ function createWebSocketStream(ws, options) { if (!called) callback(err); process.nextTick(emitClose, duplex); }); - ws.terminate(); + + if (terminateOnDestroy) ws.terminate(); }; duplex._final = function (callback) { @@ -3489,10 +4156,7 @@ function createWebSocketStream(ws, options) { }; duplex._read = function () { - if (ws.readyState === ws.OPEN && !resumeOnReceiverDrain) { - resumeOnReceiverDrain = true; - if (!ws._receiver._writableState.needDrain) ws._socket.resume(); - } + if (ws.isPaused) ws.resume(); }; duplex._write = function (chunk, encoding, callback) { @@ -3513,16 +4177,81 @@ function createWebSocketStream(ws, options) { var stream = createWebSocketStream; -const { createHash: createHash$1 } = require$$0$1; -const { createServer, STATUS_CODES } = http; +const { tokenChars: tokenChars$1 } = validation; + +/** + * Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names. + * + * @param {String} header The field value of the header + * @return {Set} The subprotocol names + * @public + */ +function parse$2(header) { + const protocols = new Set(); + let start = -1; + let end = -1; + let i = 0; + + for (i; i < header.length; i++) { + const code = header.charCodeAt(i); + + if (end === -1 && tokenChars$1[code] === 1) { + if (start === -1) start = i; + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x2c /* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + + const protocol = header.slice(start, end); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + + if (start === -1 || end !== -1) { + throw new SyntaxError('Unexpected end of input'); + } + + const protocol = header.slice(start, i); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + return protocols; +} + +var subprotocol = { parse: parse$2 }; + +const { createHash: createHash$1 } = require$$0$2; + + -const { format: format$2, parse: parse$2 } = extension; const { GUID: GUID$1, kWebSocket: kWebSocket$2 } = constants; const keyRegex = /^[+/0-9A-Za-z]{22}==$/; +const RUNNING = 0; +const CLOSING = 1; +const CLOSED = 2; + /** * Class representing a WebSocket server. * @@ -3546,8 +4275,13 @@ class WebSocketServer extends EventEmitter { * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable * permessage-deflate * @param {Number} [options.port] The port where to bind the server - * @param {http.Server} [options.server] A pre-created HTTP/S server to use + * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S + * server to use + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages * @param {Function} [options.verifyClient] A hook to reject connections + * @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket` + * class to use. It must be the `WebSocket` class or class that extends it * @param {Function} [callback] A listener for the `listening` event */ constructor(options, callback) { @@ -3555,6 +4289,7 @@ class WebSocketServer extends EventEmitter { options = { maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, perMessageDeflate: false, handleProtocols: null, clientTracking: true, @@ -3565,18 +4300,24 @@ class WebSocketServer extends EventEmitter { host: null, path: null, port: null, + WebSocket: websocket, ...options }; - if (options.port == null && !options.server && !options.noServer) { + if ( + (options.port == null && !options.server && !options.noServer) || + (options.port != null && (options.server || options.noServer)) || + (options.server && options.noServer) + ) { throw new TypeError( - 'One of the "port", "server", or "noServer" options must be specified' + 'One and only one of the "port", "server", or "noServer" options ' + + 'must be specified' ); } if (options.port != null) { - this._server = createServer((req, res) => { - const body = STATUS_CODES[426]; + this._server = http.createServer((req, res) => { + const body = http.STATUS_CODES[426]; res.writeHead(426, { 'Content-Length': body.length, @@ -3607,8 +4348,13 @@ class WebSocketServer extends EventEmitter { } if (options.perMessageDeflate === true) options.perMessageDeflate = {}; - if (options.clientTracking) this.clients = new Set(); + if (options.clientTracking) { + this.clients = new Set(); + this._shouldEmitClose = false; + } + this.options = options; + this._state = RUNNING; } /** @@ -3630,37 +4376,58 @@ class WebSocketServer extends EventEmitter { } /** - * Close the server. + * Stop the server from accepting new connections and emit the `'close'` event + * when all existing connections are closed. * - * @param {Function} [cb] Callback + * @param {Function} [cb] A one-time listener for the `'close'` event * @public */ close(cb) { - if (cb) this.once('close', cb); + if (this._state === CLOSED) { + if (cb) { + this.once('close', () => { + cb(new Error('The server is not running')); + }); + } - // - // Terminate all associated clients. - // - if (this.clients) { - for (const client of this.clients) client.terminate(); + process.nextTick(emitClose$1, this); + return; } - const server = this._server; + if (cb) this.once('close', cb); + + if (this._state === CLOSING) return; + this._state = CLOSING; + + if (this.options.noServer || this.options.server) { + if (this._server) { + this._removeListeners(); + this._removeListeners = this._server = null; + } + + if (this.clients) { + if (!this.clients.size) { + process.nextTick(emitClose$1, this); + } else { + this._shouldEmitClose = true; + } + } else { + process.nextTick(emitClose$1, this); + } + } else { + const server = this._server; - if (server) { this._removeListeners(); this._removeListeners = this._server = null; // - // Close the http server if it was internally created. + // The HTTP/S server was created internally. Close it, and rely on its + // `'close'` event. // - if (this.options.port != null) { - server.close(() => this.emit('close')); - return; - } + server.close(() => { + emitClose$1(this); + }); } - - process.nextTick(emitClose$1, this); } /** @@ -3685,7 +4452,8 @@ class WebSocketServer extends EventEmitter { * Handle a HTTP Upgrade request. * * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @public @@ -3693,25 +4461,58 @@ class WebSocketServer extends EventEmitter { handleUpgrade(req, socket, head, cb) { socket.on('error', socketOnError$1); - const key = - req.headers['sec-websocket-key'] !== undefined - ? req.headers['sec-websocket-key'].trim() - : false; + const key = req.headers['sec-websocket-key']; const version = +req.headers['sec-websocket-version']; + + if (req.method !== 'GET') { + const message = 'Invalid HTTP method'; + abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); + return; + } + + if (req.headers.upgrade.toLowerCase() !== 'websocket') { + const message = 'Invalid Upgrade header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (!key || !keyRegex.test(key)) { + const message = 'Missing or invalid Sec-WebSocket-Key header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (version !== 8 && version !== 13) { + const message = 'Missing or invalid Sec-WebSocket-Version header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (!this.shouldHandle(req)) { + abortHandshake$1(socket, 400); + return; + } + + const secWebSocketProtocol = req.headers['sec-websocket-protocol']; + let protocols = new Set(); + + if (secWebSocketProtocol !== undefined) { + try { + protocols = subprotocol.parse(secWebSocketProtocol); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Protocol header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + } + + const secWebSocketExtensions = req.headers['sec-websocket-extensions']; const extensions = {}; if ( - req.method !== 'GET' || - req.headers.upgrade.toLowerCase() !== 'websocket' || - !key || - !keyRegex.test(key) || - (version !== 8 && version !== 13) || - !this.shouldHandle(req) + this.options.perMessageDeflate && + secWebSocketExtensions !== undefined ) { - return abortHandshake$1(socket, 400); - } - - if (this.options.perMessageDeflate) { const perMessageDeflate = new permessageDeflate( this.options.perMessageDeflate, true, @@ -3719,14 +4520,17 @@ class WebSocketServer extends EventEmitter { ); try { - const offers = parse$2(req.headers['sec-websocket-extensions']); + const offers = extension.parse(secWebSocketExtensions); if (offers[permessageDeflate.extensionName]) { perMessageDeflate.accept(offers[permessageDeflate.extensionName]); extensions[permessageDeflate.extensionName] = perMessageDeflate; } } catch (err) { - return abortHandshake$1(socket, 400); + const message = + 'Invalid or unacceptable Sec-WebSocket-Extensions header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; } } @@ -3747,7 +4551,15 @@ class WebSocketServer extends EventEmitter { return abortHandshake$1(socket, code || 401, message, headers); } - this.completeUpgrade(key, extensions, req, socket, head, cb); + this.completeUpgrade( + extensions, + key, + protocols, + req, + socket, + head, + cb + ); }); return; } @@ -3755,22 +4567,24 @@ class WebSocketServer extends EventEmitter { if (!this.options.verifyClient(info)) return abortHandshake$1(socket, 401); } - this.completeUpgrade(key, extensions, req, socket, head, cb); + this.completeUpgrade(extensions, key, protocols, req, socket, head, cb); } /** * Upgrade the connection to WebSocket. * - * @param {String} key The value of the `Sec-WebSocket-Key` header * @param {Object} extensions The accepted extensions + * @param {String} key The value of the `Sec-WebSocket-Key` header + * @param {Set} protocols The subprotocols * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @throws {Error} If called more than once with the same socket * @private */ - completeUpgrade(key, extensions, req, socket, head, cb) { + completeUpgrade(extensions, key, protocols, req, socket, head, cb) { // // Destroy the socket if the client has already sent a FIN packet. // @@ -3783,6 +4597,8 @@ class WebSocketServer extends EventEmitter { ); } + if (this._state > RUNNING) return abortHandshake$1(socket, 503); + const digest = createHash$1('sha1') .update(key + GUID$1) .digest('base64'); @@ -3794,20 +4610,15 @@ class WebSocketServer extends EventEmitter { `Sec-WebSocket-Accept: ${digest}` ]; - const ws = new websocket(null); - let protocol = req.headers['sec-websocket-protocol']; - - if (protocol) { - protocol = protocol.trim().split(/ *, */); + const ws = new this.options.WebSocket(null); + if (protocols.size) { // // Optionally call external protocol selection handler. // - if (this.options.handleProtocols) { - protocol = this.options.handleProtocols(protocol, req); - } else { - protocol = protocol[0]; - } + const protocol = this.options.handleProtocols + ? this.options.handleProtocols(protocols, req) + : protocols.values().next().value; if (protocol) { headers.push(`Sec-WebSocket-Protocol: ${protocol}`); @@ -3817,7 +4628,7 @@ class WebSocketServer extends EventEmitter { if (extensions[permessageDeflate.extensionName]) { const params = extensions[permessageDeflate.extensionName].params; - const value = format$2({ + const value = extension.format({ [permessageDeflate.extensionName]: [params] }); headers.push(`Sec-WebSocket-Extensions: ${value}`); @@ -3832,11 +4643,20 @@ class WebSocketServer extends EventEmitter { socket.write(headers.concat('\r\n').join('\r\n')); socket.removeListener('error', socketOnError$1); - ws.setSocket(socket, head, this.options.maxPayload); + ws.setSocket(socket, head, { + maxPayload: this.options.maxPayload, + skipUTF8Validation: this.options.skipUTF8Validation + }); if (this.clients) { this.clients.add(ws); - ws.on('close', () => this.clients.delete(ws)); + ws.on('close', () => { + this.clients.delete(ws); + + if (this._shouldEmitClose && !this.clients.size) { + process.nextTick(emitClose$1, this); + } + }); } cb(ws, req); @@ -3872,11 +4692,12 @@ function addListeners(server, map) { * @private */ function emitClose$1(server) { + server._state = CLOSED; server.emit('close'); } /** - * Handle premature socket errors. + * Handle socket errors. * * @private */ @@ -3887,34 +4708,61 @@ function socketOnError$1() { /** * Close the connection when preconditions are not fulfilled. * - * @param {net.Socket} socket The socket of the upgrade request + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} [message] The HTTP response body * @param {Object} [headers] Additional HTTP response headers * @private */ function abortHandshake$1(socket, code, message, headers) { - if (socket.writable) { - message = message || STATUS_CODES[code]; - headers = { - Connection: 'close', - 'Content-Type': 'text/html', - 'Content-Length': Buffer.byteLength(message), - ...headers - }; + // + // The socket is writable unless the user destroyed or ended it before calling + // `server.handleUpgrade()` or in the `verifyClient` function, which is a user + // error. Handling this does not make much sense as the worst that can happen + // is that some of the data written by the user might be discarded due to the + // call to `socket.end()` below, which triggers an `'error'` event that in + // turn causes the socket to be destroyed. + // + message = message || http.STATUS_CODES[code]; + headers = { + Connection: 'close', + 'Content-Type': 'text/html', + 'Content-Length': Buffer.byteLength(message), + ...headers + }; - socket.write( - `HTTP/1.1 ${code} ${STATUS_CODES[code]}\r\n` + - Object.keys(headers) - .map((h) => `${h}: ${headers[h]}`) - .join('\r\n') + - '\r\n\r\n' + - message - ); - } + socket.once('finish', socket.destroy); + + socket.end( + `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + + Object.keys(headers) + .map((h) => `${h}: ${headers[h]}`) + .join('\r\n') + + '\r\n\r\n' + + message + ); +} + +/** + * Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least + * one listener for it, otherwise call `abortHandshake()`. + * + * @param {WebSocketServer} server The WebSocket server + * @param {http.IncomingMessage} req The request object + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} message The HTTP response body + * @private + */ +function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) { + if (server.listenerCount('wsClientError')) { + const err = new Error(message); + Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); - socket.removeListener('error', socketOnError$1); - socket.destroy(); + server.emit('wsClientError', err, socket, req); + } else { + abortHandshake$1(socket, code, message); + } } websocket.createWebSocketStream = stream; @@ -3922,6 +4770,9 @@ websocket.Server = websocketServer; websocket.Receiver = receiver; websocket.Sender = sender; +websocket.WebSocket = websocket; +websocket.WebSocketServer = websocket.Server; + var ws = websocket; function noop() {} @@ -6428,7 +7279,7 @@ nacl.setPRNG = function(fn) { }); } else if (typeof commonjsRequire !== 'undefined') { // Node.js. - crypto = require$$0$1; + crypto = require$$0$2; if (crypto && crypto.randomBytes) { nacl.setPRNG(function(x, n) { var i, v = crypto.randomBytes(n); @@ -7013,20 +7864,24 @@ naclFast.util = naclUtil; function send({ message, channel }) { const { log } = channel; + // logger.red(log, `Sending over channel ${channel.ident} ws id ${channel.ws.__id}`); + // logger.red(log, message); + if (isObject(message)) { message = JSON.stringify(message); } + const prefix = `Channel #${channel.ident} ${channel.remoteAddress() || ''} ${ + channel.remotePubkeyHex() ? `to ${channel.remotePubkeyHex()}` : '' + }`; + const nonce = new Uint8Array(integerToByteArray(2 * channel.sentCount + 1, 24)); if (channel.verbose) { if (channel.sharedSecret) { - logger.write( - log, - `Channel ${channel.remoteAddress()} → Sending encrypted message #${channel.sentCount}:` - ); + logger.cyan(log, `${prefix} → Sending encrypted message #${channel.sentCount}:`); } else { - logger.write(log, `Channel ${channel.remoteAddress()} → Sending message #${channel.sentCount}:`); + logger.green(log, `${prefix} → Sending message #${channel.sentCount}:`); } logger.write(log, message); @@ -7105,12 +7960,16 @@ function handleMessage(channel, message) { function messageReceived({ message, channel }) { const { log } = channel; + const prefix = `Channel #${channel.ident} ${channel.remoteAddress() || ''} ${ + channel.remotePubkeyHex() ? `to ${channel.remotePubkeyHex()}` : '' + }`; + channel.lastMessageAt = Date.now(); const nonce = new Uint8Array(integerToByteArray(2 * channel.receivedCount, 24)); if (channel.verbose) { - logger.write(log, `Channel ${channel.remoteAddress()} → Received message #${channel.receivedCount} ↴`); + logger.yellow(log, `${prefix} → Received message #${channel.receivedCount} ↴`); } //if (channel.sharedSecret) { @@ -7125,6 +7984,10 @@ function messageReceived({ message, channel }) { try { // handshake phase if (!channel.sharedSecret) { + if (channel.verbose) { + logger.write(log, `${prefix} handshake message: ${message}`); + } + //const jsonData = JSON.parse(message); handleMessage(channel, message); return; @@ -7814,6 +8677,8 @@ class Channel$1 extends Eev { super(); this.ws = ws; + this.ident = Math.round(10 ** 5 * Math.random()).toString(); + this.log = log; this.verbose = verbose; @@ -7983,16 +8848,12 @@ class WsServer extends Eev { super(); process.nextTick(() => { - // const handleProtocols = (protocols, request) => { - // return protocols[0]; - // }; - if (server) { - this.webSocketServer = new ws.Server({ server }); - //this.webSocketServer = new WebSocket.Server({ server, handleProtocols }); + //this.webSocketServer = new WebSocket.Server({ server }); + this.webSocketServer = new ws.WebSocketServer({ server }); } else { - this.webSocketServer = new ws.Server({ port }); - //this.webSocketServer = new WebSocket.Server({ port, handleProtocols }); + //this.webSocketServer = new WebSocket.Server({ port }); + this.webSocketServer = new ws.WebSocketServer({ port }); } this.continueSetup({ log, verbose }); @@ -8001,8 +8862,25 @@ class WsServer extends Eev { continueSetup({ log, verbose }) { this.webSocketServer.on('connection', (ws, req) => { + // https://github.com/websockets/ws/issues/1354 + // Websocket RangeError: Invalid WebSocket frame: RSV2 and RSV3 must be clear + // https://stackoverflow.com/questions/45303733/error-rsv2-and-rsv3-must-be-clear-in-ws + // this should possibly help, not yet confirmed! + ws.on('error', e => { + const log2 = log.yellow || log; + log2('Handled Websocket issue (probably a malformed websocket connection):'); + log2(e); + // log.red => assume dmt logger + // log => assume console.log + }); + const channel = new Channel$1(ws, { log, verbose }); + // const wsId = Math.round(10 ** 5 * Math.random()).toString(); + // ws.__id = wsId; + // const log3 = log.red || log; + // log3(`Created new channel ${channel.ident}, ws id: ${wsId}`); + channel._remoteIp = getRemoteIp(req); channel._remoteAddress = getRemoteHost(req); @@ -8248,6 +9126,10 @@ function orderBy(key, key2, order = 'asc') { //import ProtocolStore from '../../stores/back/protocolStore.js'; +//⚠️ when not going directly to ws port but instead for example through ligthttpd websocket-upgrade +// this channelList will behave strangely... probably just a lag between connections actually disappearing +// so to count active connections through proxy it is not accurate, hopefully just a time lag but test... + class ChannelList extends Eev { constructor({ protocol }) { super(); @@ -8311,13 +9193,11 @@ class ChannelList extends Eev { reportStatus() { const connList = this.channels.map(channel => { - const result = { + return { ip: channel.remoteIp(), address: channel.remoteAddress(), remotePubkeyHex: channel.remotePubkeyHex() }; - - return result; }); this.emit('status', { connList }); diff --git a/core/node/connectome/src/client/connect/establishAndMaintainConnection.js b/core/node/connectome/src/client/connect/establishAndMaintainConnection.js index 91bcbe301..468cd5d8f 100644 --- a/core/node/connectome/src/client/connect/establishAndMaintainConnection.js +++ b/core/node/connectome/src/client/connect/establishAndMaintainConnection.js @@ -13,6 +13,22 @@ import determineEndpoint from './determineEndpoint.js'; import logger from '../../utils/logger/logger.js'; +function addListener(event, callback, ws) { + if (browser) { + ws.addEventListener(event, callback); + } else { + ws.on(event, callback); + } +} + +function removeListener(event, callback, ws) { + if (browser) { + ws.removeEventListener(event, callback); + } else { + ws.off(event, callback); + } +} + function establishAndMaintainConnection( { endpoint, host, port, protocol, keypair, remotePubkey, rpcRequestTimeout, autoDecommission, log, verbose, tag, dummy }, { WebSocket } @@ -38,10 +54,14 @@ function establishAndMaintainConnection( connector.connection = { terminate() { this.websocket._removeAllCallbacks(); + this.websocket.__closed = true; this.websocket.close(); connector.connectStatus(false); reconnect(); }, + isOpen() { + return this.websocket.readyState == wsOPEN && !this.websocket.__closed; + }, endpoint, checkTicker: 0 }; @@ -65,12 +85,12 @@ function checkConnection({ connector, reconnect, log }) { if (connectionIdle(conn) || connector.decommissioned) { if (connector.decommissioned) { - logger.yellow(log, `${connector.endpoint} Connection decommisioned, closing websocket ${conn.websocket.__id}, will not retry again `); + logger.yellow(log, `${connector.endpoint} Connection decommisioned, closing websocket #${conn.websocket.__id}, will not retry again `); decommission(connector); } else { connector.emit('inactive_connection'); - logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection`); + logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection #${conn.websocket.__id}`); } conn.terminate(); @@ -103,6 +123,8 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb return; } + const wsId = Math.round(10 ** 5 * Math.random()).toString(); + if (conn.currentlyTryingWS && conn.currentlyTryingWS.readyState == wsCONNECTING) { if (conn.currentlyTryingWS._waitForConnectCounter < WAIT_FOR_NEW_CONN_TICKS) { conn.currentlyTryingWS._waitForConnectCounter += 1; @@ -114,13 +136,14 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); } else if (verbose || browser) { - logger.write(log, `${endpoint} Created new websocket`); + logger.write(log, `${endpoint} Created new websocket #${wsId}`); } const ws = new WebSocket(endpoint); - ws.__id = Math.random(); + ws.__id = wsId; conn.currentlyTryingWS = ws; conn.currentlyTryingWS._waitForConnectCounter = 0; @@ -139,7 +162,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } if (verbose || browser) { - logger.write(log, `${endpoint} Websocket open`); + logger.write(log, `${endpoint} Websocket #${wsId} open`); } conn.currentlyTryingWS = null; @@ -152,14 +175,10 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb }; ws._removeAllCallbacks = () => { - ws.removeEventListener('open', openCallback); + removeListener('open', openCallback, ws); }; - if (browser) { - ws.addEventListener('open', openCallback); - } else { - ws.on('open', openCallback); - } + addListener('open', openCallback, ws); } function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, verbose }) { @@ -172,7 +191,9 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; const closeCallback = () => { - logger.write(log, `${connector.endpoint} ✖ Connection closed`); + ws.__closed = true; + + logger.blue(log, `${connector.endpoint} ✖ Connection #${ws.__id} [ ${connector.protocol} ] closed`); if (connector.decommissioned) { connector.connectStatus(false); @@ -180,6 +201,7 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v } connector.connectStatus(undefined); + reconnect(); }; @@ -192,6 +214,10 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v const msg = browser ? _msg.data : _msg; + if (ws.__closed) { + return; + } + if (msg == 'pong') { connector.emit('pong'); return; @@ -212,22 +238,15 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; ws._removeAllCallbacks = () => { - ws.removeEventListener('error', errorCallback); - ws.removeEventListener('close', closeCallback); - ws.removeEventListener('message', messageCallback); - - ws.removeEventListener('open', openCallback); + removeListener('error', errorCallback, ws); + removeListener('close', closeCallback, ws); + removeListener('message', messageCallback, ws); + removeListener('open', openCallback, ws); }; - if (browser) { - ws.addEventListener('error', errorCallback); - ws.addEventListener('close', closeCallback); - ws.addEventListener('message', messageCallback); - } else { - ws.on('error', errorCallback); - ws.on('close', closeCallback); - ws.on('message', messageCallback); - } + addListener('error', errorCallback, ws); + addListener('close', closeCallback, ws); + addListener('message', messageCallback, ws); } function decommission(connector) { @@ -235,21 +254,23 @@ function decommission(connector) { if (conn.currentlyTryingWS) { conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); conn.currentlyTryingWS = null; } - if (conn.ws) { - conn.ws._removeAllCallbacks(); - conn.ws.close(); - conn.ws = null; + if (conn.websocket) { + conn.websocket._removeAllCallbacks(); + conn.websocket.__closed = true; + conn.websocket.close(); + conn.websocket = null; } connector.connectStatus(false); } function socketConnected(conn) { - return conn.websocket && conn.websocket.readyState == wsOPEN; + return conn.websocket && conn.websocket.readyState == wsOPEN && !conn.websocket.__closed; } function connectionIdle(conn) { diff --git a/core/node/connectome/src/client/connector/connector.js b/core/node/connectome/src/client/connector/connector.js index 51fd16bc0..bf151d939 100644 --- a/core/node/connectome/src/client/connector/connector.js +++ b/core/node/connectome/src/client/connector/connector.js @@ -152,7 +152,7 @@ class Connector extends EventEmitter { this.successfulConnectsCount += 1; if (this.verbose) { - logger.green(this.log, `✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`); + logger.white(this.log, `✓ Connector ${this.endpoint} connected (${this.successfulConnectsCount} total reconnects)`); } const websocketId = this.connection.websocket.__id; diff --git a/core/node/connectome/src/client/connector/handshake.js b/core/node/connectome/src/client/connector/handshake.js index dc847da78..f68f5be0e 100644 --- a/core/node/connectome/src/client/connector/handshake.js +++ b/core/node/connectome/src/client/connector/handshake.js @@ -2,6 +2,8 @@ import nacl from 'tweetnacl'; import naclutil from 'tweetnacl-util'; nacl.util = naclutil; +const wsOPEN = 1; + import { hexToBuffer } from '../../utils/index.js'; import logger from '../../utils/logger/logger.js'; @@ -22,20 +24,25 @@ export default function diffieHellman({ connector, afterFirstStep = () => {} }) logger.write(connector.log, `Connector ${endpoint} established shared secret through diffie-hellman exchange.`); } - connector - .remoteObject('Auth') - .call('finalizeHandshake', { protocol }) - .then(res => { - if (res && res.error) { - console.log(res.error); - } else { - success(); - - const _tag = tag ? ` (${tag})` : ''; - logger.cyan(connector.log, `${endpoint}${_tag} ✓ Connection [ ${protocol || '"no-name"'} ] ready`); - } - }) - .catch(reject); + if (connector.connection.websocket.readyState == wsOPEN) { + connector + .remoteObject('Auth') + .call('finalizeHandshake', { protocol }) + .then(res => { + if (res && res.error) { + console.log(res.error); + } else { + success(); + + const _tag = tag ? ` (${tag})` : ''; + logger.cyan(connector.log, `☑️ ${endpoint}${_tag} ✓ Connection #${connector.connection.websocket.__id} [ ${protocol || '"no-name"'} ] ready`); + } + }) + .catch(reject); + } else { + const _tag = tag ? ` (${tag})` : ''; + logger.yellow(connector.log, `${endpoint}${_tag} ✖ Connection [ ${protocol || '"no-name"'} ] closed just before finalizeHandshake step`); + } }) .catch(reject); }); diff --git a/core/node/connectome/src/client/connector/receive.js b/core/node/connectome/src/client/connector/receive.js index 2820507d7..9985c2e2b 100644 --- a/core/node/connectome/src/client/connector/receive.js +++ b/core/node/connectome/src/client/connector/receive.js @@ -41,8 +41,13 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec // encryptedJson data!! if (connector.verbose == 'extra') { logger.magenta(log, `Connector ${connector.endpoint} received bytes ↴`); - logger.gray(log, encryptedData); - logger.magenta(log, `Connector ${connector.endpoint} decrypting with shared secret ${connector.sharedSecret}...`); + logger.cyan(log, encryptedData); + logger.green(log, JSON.stringify(encryptedData)); + logger.gray(log, `Connector ${connector.endpoint} decrypting with shared secret ${connector.sharedSecret}...`); + } + + if (!connector.sharedSecret) { + logger.red(log, `Connector ${connector.endpoint} missing sharedSecret - should not happen...`); } const _decryptedMessage = nacl.secretbox.open(encryptedData, nonce, connector.sharedSecret); @@ -54,7 +59,7 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec const decodedMessage = nacl.util.encodeUTF8(decryptedMessage); if (connector.verbose) { - logger.write(log, `Received message: ${decodedMessage}`); + logger.yellow(log, `Connector ${connector.endpoint} received message: ${decodedMessage}`); } try { @@ -83,6 +88,10 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec throw e; } } else { + if (connector.verbose) { + logger.yellow(log, `Connector ${connector.endpoint} received binary data`); + } + connector.emit('receive_binary', decryptedMessage); } } diff --git a/core/node/connectome/src/client/connector/send.js b/core/node/connectome/src/client/connector/send.js index 9951ec8ce..12745c588 100644 --- a/core/node/connectome/src/client/connector/send.js +++ b/core/node/connectome/src/client/connector/send.js @@ -38,7 +38,7 @@ function send({ data, connector }) { } else { if (connector.verbose) { logger.green(log, `Connector ${connector.endpoint} → Sending message #${connector.sentCount} ↴`); - logger.gray(log, data); + logger.cyan(log, data); } connector.connection.websocket.send(data); diff --git a/core/node/connectome/src/server/channel/channel.js b/core/node/connectome/src/server/channel/channel.js index 6e47b324d..7df3cef91 100644 --- a/core/node/connectome/src/server/channel/channel.js +++ b/core/node/connectome/src/server/channel/channel.js @@ -14,6 +14,8 @@ class Channel extends EventEmitter { super(); this.ws = ws; + this.ident = Math.round(10 ** 5 * Math.random()).toString(); + this.log = log; this.verbose = verbose; diff --git a/core/node/connectome/src/server/channel/channelList.js b/core/node/connectome/src/server/channel/channelList.js index 9c3ac971e..359af3db1 100644 --- a/core/node/connectome/src/server/channel/channelList.js +++ b/core/node/connectome/src/server/channel/channelList.js @@ -56,13 +56,11 @@ class ChannelList extends EventEmitter { reportStatus() { const connList = this.channels.map(channel => { - const result = { + return { ip: channel.remoteIp(), address: channel.remoteAddress(), remotePubkeyHex: channel.remotePubkeyHex() }; - - return result; }); this.emit('status', { connList }); diff --git a/core/node/connectome/src/server/channel/receive.js b/core/node/connectome/src/server/channel/receive.js index 808447778..ff0d1879f 100644 --- a/core/node/connectome/src/server/channel/receive.js +++ b/core/node/connectome/src/server/channel/receive.js @@ -36,18 +36,24 @@ function handleMessage(channel, message) { function messageReceived({ message, channel }) { const { log } = channel; + const prefix = `Channel #${channel.ident} ${channel.remoteAddress() || ''} ${channel.remotePubkeyHex() ? `to ${channel.remotePubkeyHex()}` : ''}`; + channel.lastMessageAt = Date.now(); const nonce = new Uint8Array(integerToByteArray(2 * channel.receivedCount, 24)); if (channel.verbose) { - logger.write(log, `Channel ${channel.remoteAddress()} → Received message #${channel.receivedCount} ↴`); + logger.yellow(log, `${prefix} → Received message #${channel.receivedCount} ↴`); } let decodedMessage; try { if (!channel.sharedSecret) { + if (channel.verbose) { + logger.write(log, `${prefix} handshake message: ${message}`); + } + handleMessage(channel, message); return; } diff --git a/core/node/connectome/src/server/channel/send.js b/core/node/connectome/src/server/channel/send.js index 3c49d06b6..088205da6 100644 --- a/core/node/connectome/src/server/channel/send.js +++ b/core/node/connectome/src/server/channel/send.js @@ -14,13 +14,15 @@ function send({ message, channel }) { message = JSON.stringify(message); } + const prefix = `Channel #${channel.ident} ${channel.remoteAddress() || ''} ${channel.remotePubkeyHex() ? `to ${channel.remotePubkeyHex()}` : ''}`; + const nonce = new Uint8Array(integerToByteArray(2 * channel.sentCount + 1, 24)); if (channel.verbose) { if (channel.sharedSecret) { - logger.write(log, `Channel ${channel.remoteAddress()} → Sending encrypted message #${channel.sentCount}:`); + logger.cyan(log, `${prefix} → Sending encrypted message #${channel.sentCount}:`); } else { - logger.write(log, `Channel ${channel.remoteAddress()} → Sending message #${channel.sentCount}:`); + logger.green(log, `${prefix} → Sending message #${channel.sentCount}:`); } logger.write(log, message); diff --git a/core/node/connectome/src/server/connectome/wsServer.js b/core/node/connectome/src/server/connectome/wsServer.js index 48ea85e81..e31ba94a7 100644 --- a/core/node/connectome/src/server/connectome/wsServer.js +++ b/core/node/connectome/src/server/connectome/wsServer.js @@ -1,5 +1,4 @@ -import WebSocket from 'ws'; - +import { WebSocketServer } from 'ws'; import { EventEmitter } from '../../utils/index.js'; import getRemoteHost from '../channel/getRemoteHost.js'; @@ -18,9 +17,9 @@ class WsServer extends EventEmitter { process.nextTick(() => { if (server) { - this.webSocketServer = new WebSocket.Server({ server }); + this.webSocketServer = new WebSocketServer({ server }); } else { - this.webSocketServer = new WebSocket.Server({ port }); + this.webSocketServer = new WebSocketServer({ port }); } this.continueSetup({ log, verbose }); @@ -29,6 +28,12 @@ class WsServer extends EventEmitter { continueSetup({ log, verbose }) { this.webSocketServer.on('connection', (ws, req) => { + ws.on('error', e => { + const log2 = log.yellow || log; + log2('Handled Websocket issue (probably a malformed websocket connection):'); + log2(e); + }); + const channel = new Channel(ws, { log, verbose }); channel._remoteIp = getRemoteIp(req); diff --git a/core/node/connectome/src/stores-node/index.js b/core/node/connectome/src/stores-node/index.js index a0d2353d1..4c93283b3 100644 --- a/core/node/connectome/src/stores-node/index.js +++ b/core/node/connectome/src/stores-node/index.js @@ -2,4 +2,8 @@ import SyncStore from './syncStore.js'; import MultiConnectedStore from '../stores/lib/multiConnectedStore/multiConnectedStore.js'; -export { SyncStore, MultiConnectedStore }; +function isEmptyObject(obj) { + return typeof obj === 'object' && Object.keys(obj).length === 0; +} + +export { SyncStore, MultiConnectedStore, isEmptyObject }; diff --git a/core/node/connectome/src/stores-node/syncStore.js b/core/node/connectome/src/stores-node/syncStore.js index 3e01cc61c..a915e49b5 100644 --- a/core/node/connectome/src/stores-node/syncStore.js +++ b/core/node/connectome/src/stores-node/syncStore.js @@ -79,6 +79,11 @@ export default class SyncStore extends EventEmitter { return this.kvStore.state; } + set(state) { + this.kvStore.set(state); + this.announceStateChange(); + } + get(key) { return key ? this.state()[key] : this.state(); } diff --git a/core/node/connectome/src/stores-node/twoLevelMergeKVStore.js b/core/node/connectome/src/stores-node/twoLevelMergeKVStore.js index faafa0f0a..936290067 100644 --- a/core/node/connectome/src/stores-node/twoLevelMergeKVStore.js +++ b/core/node/connectome/src/stores-node/twoLevelMergeKVStore.js @@ -5,6 +5,10 @@ export default class KeyValueStore { this.state = {}; } + set(state) { + this.state = state; + } + update(patch) { this.state = mergeState(this.state, patch); } diff --git a/core/node/connectome/src/stores/index.js b/core/node/connectome/src/stores/index.js index 10694ceeb..0ded41cd7 100644 --- a/core/node/connectome/src/stores/index.js +++ b/core/node/connectome/src/stores/index.js @@ -1,3 +1,7 @@ import MultiConnectedStore from './lib/multiConnectedStore/multiConnectedStore.js'; -export { MultiConnectedStore }; +function isEmptyObject(obj) { + return typeof obj === 'object' && Object.keys(obj).length === 0; +} + +export { MultiConnectedStore, isEmptyObject }; diff --git a/core/node/connectome/stores/index.js b/core/node/connectome/stores/index.js index 63d79b19c..406e1a214 100644 --- a/core/node/connectome/stores/index.js +++ b/core/node/connectome/stores/index.js @@ -3252,7 +3252,7 @@ function send({ data, connector }) { log, `Connector ${connector.endpoint} → Sending message #${connector.sentCount} ↴` ); - logger.gray(log, data); + logger.cyan(log, data); } connector.connection.websocket.send(data); @@ -3318,11 +3318,25 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec // 💡 encryptedJson data!! if (connector.verbose == 'extra') { logger.magenta(log, `Connector ${connector.endpoint} received bytes ↴`); - logger.gray(log, encryptedData); - logger.magenta( + logger.cyan(log, encryptedData); + logger.green(log, JSON.stringify(encryptedData)); + logger.gray( log, `Connector ${connector.endpoint} decrypting with shared secret ${connector.sharedSecret}...` ); + //logger.cyan(log, JSON.stringify(connector.sharedSecret)); + } + + if (!connector.sharedSecret) { + // we had this problem before -- zurich wifi -- when terminating inactive websocket + // it didn't actually close in time .. we set connector to disconnected and deleted sharedSecret + // but then a stray message json rpc return from hadshake arrived after that and couldn't be decrypted + // because it shouldn't have arrived in the first place after websocket was supposedly closed + // solution: __closed flag on all websockets.. it is set to true at the same time as calling close() + // and then any messages still coming over the wire on such closed websockets are dropped + // we hope websocket is eventually closed though (?) + // see messageCallback in establishAndMaintainConnection, this was fixed there + logger.red(log, `Connector ${connector.endpoint} missing sharedSecret - should not happen...`); } const _decryptedMessage = naclFast.secretbox.open(encryptedData, nonce, connector.sharedSecret); @@ -3334,7 +3348,7 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec const decodedMessage = naclFast.util.encodeUTF8(decryptedMessage); if (connector.verbose) { - logger.write(log, `Received message: ${decodedMessage}`); + logger.yellow(log, `Connector ${connector.endpoint} received message: ${decodedMessage}`); } try { @@ -3380,6 +3394,10 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec throw e; } } else { + if (connector.verbose) { + logger.yellow(log, `Connector ${connector.endpoint} received binary data`); + } + //const binaryData = decryptedMessage; // const sessionId = Buffer.from(binaryData.buffer, binaryData.byteOffset, 64).toString(); // const binaryPayload = Buffer.from(binaryData.buffer, binaryData.byteOffset + 64); @@ -3391,20 +3409,17 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec naclFast.util = naclUtil; +const wsOPEN = 1; + function diffieHellman({ connector, afterFirstStep = () => {} }) { - const { - clientPrivateKey, - clientPublicKey, - clientPublicKeyHex, - protocol, - tag, - endpoint, - verbose - } = connector; + const { clientPrivateKey, clientPublicKey, clientPublicKeyHex, protocol, tag, endpoint, verbose } = + connector; return new Promise((success, reject) => { - connector.remoteObject('Auth') + connector + .remoteObject('Auth') .call('exchangePubkeys', { pubkey: clientPublicKeyHex }) + //.call('exchangePubkeys', { pubkey: clientPublicKeyHex, clientWsId: connector.connection.websocket.__id }) .then(remotePubkeyHex => { const sharedSecret = naclFast.box.before(hexToBuffer(remotePubkeyHex), clientPrivateKey); @@ -3417,33 +3432,50 @@ function diffieHellman({ connector, afterFirstStep = () => {} }) { ); } - connector.remoteObject('Auth') - .call('finalizeHandshake', { protocol }) - .then(res => { - // finalizeHandshake rpc endpoint on server can cleanly retorn {error} as a result - // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) - if (res && res.error) { - console.log(res.error); - // this connection will keep hangling and no reconnect tries will be made - // since we keep websocket open just that nothing is happening - - // when we enable the protocol on the endpoint we have to restart the process - // frontend connector will get disconnected at this point, websocket will close - // and from then on it tries reconnecting again so when ws first connects - // and protocol is present , it will be a success - - // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging - } else { - success(); - - const _tag = tag ? ` (${tag})` : ''; - logger.cyan( - connector.log, - `${endpoint}${_tag} ✓ Connection [ ${protocol || '"no-name"'} ] ready` - ); - } - }) - .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + // if connection has closed at this point we don't try to send into closed + // connection, it would still work but error would be logged + if (connector.connection.websocket.readyState == wsOPEN) { + connector + .remoteObject('Auth') + .call('finalizeHandshake', { protocol }) + .then(res => { + // finalizeHandshake rpc endpoint on server can cleanly return {error} as a result + // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) + if (res && res.error) { + console.log(res.error); + // this connection will keep hangling and no reconnect tries will be made + // since we keep websocket open just that nothing is happening + + // when we enable the protocol on the endpoint we have to restart the process + // frontend connector will get disconnected at this point, websocket will close + // and from then on it tries reconnecting again so when ws first connects + // and protocol is present , it will be a success + + // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging + } else { + success(); + + const _tag = tag ? ` (${tag})` : ''; + logger.cyan( + connector.log, + `✓✓✓ ${endpoint}${_tag} ✓ Connection #${connector.connection.websocket.__id} [ ${ + protocol || '"no-name"' + } ] ready` + ); + } + }) + .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + } else { + const _tag = tag ? ` (${tag})` : ''; + logger.yellow( + connector.log, + `${endpoint}${_tag} ✖ Connection [ ${ + protocol || '"no-name"' + } ] closed just before finalizeHandshake step` + ); + // don't reject here -- because it will show some wring log message in connector + // on:ready error "will not try to reconnect" .. which is not the case here + } }) .catch(reject); }); @@ -4875,7 +4907,7 @@ const DECOMMISSION_INACTIVITY = 60000; // 1min //const DECOMMISSION_INACTIVITY = 120000; // 2min //const DECOMMISSION_INACTIVITY = 10000; // 2min -const wsOPEN = 1; +const wsOPEN$1 = 1; class Connector extends Eev { constructor({ @@ -5031,7 +5063,7 @@ class Connector extends Eev { this.successfulConnectsCount += 1; if (this.verbose) { - logger.green(this.log, `✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`); + logger.white(this.log, `✓ Connector ${this.endpoint} connected (${this.successfulConnectsCount} total reconnects)`); } const websocketId = this.connection.websocket.__id; @@ -5063,7 +5095,7 @@ class Connector extends Eev { // but sometimes we also get an open websocket after rpc timeout (not sure but this code handles it anyway, should be no problem, only better for all cases) if ( this.connection.websocket.__id == websocketId && - this.connection.websocket.readyState == wsOPEN + this.connection.websocket.readyState == wsOPEN$1 ) { //⚠️ we only show if it seems still relevant, special case // previously we had this first log output above this if statement @@ -5247,7 +5279,7 @@ function determineEndpoint({ endpoint, host, port }) { const browser$1 = typeof window !== 'undefined'; const wsCONNECTING = 0; -const wsOPEN$1 = 1; +const wsOPEN$2 = 1; //const wsCLOSING = 2; //const wsCLOSED = 3; @@ -5261,6 +5293,22 @@ const CONN_IDLE_TICKS = 3; // how long to wait for a new websocket to connect... after this we cancel it const WAIT_FOR_NEW_CONN_TICKS = 5; // 5000 ms ( = (5) * CONN_CHECK_INTERVAL ) +function addListener(name, callback, ws) { + if (browser$1) { + ws.addEventListener(name, callback); + } else { + ws.on(name, callback); + } +} + +function removeListener(name, callback, ws) { + if (browser$1) { + ws.removeEventListener(name, callback); + } else { + ws.off(name, callback); + } +} + //todo: remove 'dummy' argument once legacyLib with old MCS is history function establishAndMaintainConnection( { @@ -5300,7 +5348,8 @@ function establishAndMaintainConnection( connector.connection = { terminate() { this.websocket._removeAllCallbacks(); - this.websocket.close(); + this.websocket.__closed = true; + this.websocket.close(); // might take some time to actually close, we can get stray messages through that websocket //connector.connectStatus(undefined); connector.connectStatus(false); reconnect(); @@ -5334,14 +5383,14 @@ function checkConnection({ connector, reconnect, log }) { // decommissioned logger.yellow( log, - `${connector.endpoint} Connection decommisioned, closing websocket ${conn.websocket.__id}, will not retry again ` + `${connector.endpoint} Connection decommisioned, closing websocket #${conn.websocket.__id}, will not retry again ` ); decommission(connector); } else { // idle connection connector.emit('inactive_connection'); - logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection`); + logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection #${conn.websocket.__id}`); } conn.terminate(); @@ -5383,6 +5432,8 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb return; } + const wsId = Math.round(10 ** 5 * Math.random()).toString(); + //logger.write(log, `${endpoint} CONN_TICK`); //logger.write(log, `${endpoint} wsReadyState ${conn.currentlyTryingWS?.readyState}`); @@ -5398,9 +5449,10 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); } else if (verbose || browser$1) { - logger.write(log, `${endpoint} Created new websocket`); + logger.write(log, `${endpoint} Created new websocket #${wsId}`); } // so in case when device is online but websocket server is not running we usually @@ -5411,7 +5463,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb // (see above)... and we try with a new websocket every 4800ms again instead on every tick (800ms) const ws = new WebSocket(endpoint); - ws.__id = Math.random(); + ws.__id = wsId; conn.currentlyTryingWS = ws; conn.currentlyTryingWS._waitForConnectCounter = 0; @@ -5431,7 +5483,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } if (verbose || browser$1) { - logger.write(log, `${endpoint} Websocket open`); + logger.write(log, `${endpoint} Websocket #${wsId} open`); } conn.currentlyTryingWS = null; @@ -5444,14 +5496,14 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb }; ws._removeAllCallbacks = () => { - ws.removeEventListener('open', openCallback); + // logger.red( + // log, + // `${connector.endpoint} removing 1 callback (open) on ws #${ws.__id} [ ${connector.protocol} ]` + // ); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('open', openCallback); - } else { - ws.on('open', openCallback); - } + addListener('open', openCallback, ws); } function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, verbose }) { @@ -5468,7 +5520,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; const closeCallback = () => { - logger.write(log, `${connector.endpoint} ✖ Connection closed`); + //❗❗❗❗ -- can get stray messages even here!! after close callback ws implementation lets a few (one) messages through!! + // this only happened on LAN ... + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+167ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 ✖ Connection #28485 [ dmt ] closed' + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+01ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 Created new websocket #17068' + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+338ms) ∞ 1.0.0.1 consecutiveUnresolvedTimeout after 2x unresolved promise + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+43ms) ∞ lanServerConn — "ws://192.168.0.10:7780 connection #28485 [ dmt ] received msg '��\x19X���9�߈�V^L�#�b��)\x02�\r��n\x06^?U�v�\x00�ͻ>����k~�A(^�\t�İP�=���X*���'" + // maybe not needed anymore after listeners issue was fixed ..... + ws.__closed = true; + + logger.blue(log, `${connector.endpoint} ✖ Connection #${ws.__id} [ ${connector.protocol} ] closed`); if (connector.decommissioned) { connector.connectStatus(false); @@ -5481,6 +5542,7 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v // flip side is that there is such small delay between when we stop some process and when red x appears... but it's quite ok! // we do however disable all commands immediately ... so: show red X when connect status is FALSE excusively and disable all gui actions when it's NOT TRUE (false or undefined) connector.connectStatus(undefined); + reconnect(); //setTimeout(reconnect, MAX_RECONNECT_DELAY_AFTER_WS_CLOSE * Math.random()); // turns out we don't really need to do these delays, works fine without }; @@ -5494,11 +5556,26 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v const msg = browser$1 ? _msg.data : _msg; + if (ws.__closed) { + // if (msg != 'pong') { + // logger.red( + // log, + // `${connector.endpoint} Already closed connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + // } + return; + } + if (msg == 'pong') { connector.emit('pong'); return; } + // logger.red( + // log, + // `${connector.endpoint} connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + let jsonData; try { @@ -5514,22 +5591,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; ws._removeAllCallbacks = () => { - ws.removeEventListener('error', errorCallback); - ws.removeEventListener('close', closeCallback); - ws.removeEventListener('message', messageCallback); - - ws.removeEventListener('open', openCallback); + // logger.red(log, `${connector.endpoint} removing 4 callbacks on ws #${ws.__id} [ ${connector.protocol} ]`); + removeListener('error', errorCallback, ws); + removeListener('close', closeCallback, ws); + removeListener('message', messageCallback, ws); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('error', errorCallback); - ws.addEventListener('close', closeCallback); - ws.addEventListener('message', messageCallback); - } else { - ws.on('error', errorCallback); - ws.on('close', closeCallback); - ws.on('message', messageCallback); - } + addListener('error', errorCallback, ws); + addListener('close', closeCallback, ws); + addListener('message', messageCallback, ws); } function decommission(connector) { @@ -5537,21 +5608,23 @@ function decommission(connector) { if (conn.currentlyTryingWS) { conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); conn.currentlyTryingWS = null; } - if (conn.ws) { - conn.ws._removeAllCallbacks(); - conn.ws.close(); - conn.ws = null; + if (conn.websocket) { + conn.websocket._removeAllCallbacks(); + conn.websocket.__closed = true; + conn.websocket.close(); + conn.websocket = null; } connector.connectStatus(false); } function socketConnected(conn) { - return conn.websocket && conn.websocket.readyState == wsOPEN$1; + return conn.websocket && conn.websocket.readyState == wsOPEN$2 && !conn.websocket.__closed; // when terminating connection, might be useful -- check } function connectionIdle(conn) { @@ -5960,4 +6033,9 @@ class MultiConnectedStore extends MergeStore { } } +function isEmptyObject(obj) { + return typeof obj === 'object' && Object.keys(obj).length === 0; +} + exports.MultiConnectedStore = MultiConnectedStore; +exports.isEmptyObject = isEmptyObject; diff --git a/core/node/connectome/stores/index.mjs b/core/node/connectome/stores/index.mjs index 7ef40ae62..d0fd63009 100644 --- a/core/node/connectome/stores/index.mjs +++ b/core/node/connectome/stores/index.mjs @@ -3248,7 +3248,7 @@ function send({ data, connector }) { log, `Connector ${connector.endpoint} → Sending message #${connector.sentCount} ↴` ); - logger.gray(log, data); + logger.cyan(log, data); } connector.connection.websocket.send(data); @@ -3314,11 +3314,25 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec // 💡 encryptedJson data!! if (connector.verbose == 'extra') { logger.magenta(log, `Connector ${connector.endpoint} received bytes ↴`); - logger.gray(log, encryptedData); - logger.magenta( + logger.cyan(log, encryptedData); + logger.green(log, JSON.stringify(encryptedData)); + logger.gray( log, `Connector ${connector.endpoint} decrypting with shared secret ${connector.sharedSecret}...` ); + //logger.cyan(log, JSON.stringify(connector.sharedSecret)); + } + + if (!connector.sharedSecret) { + // we had this problem before -- zurich wifi -- when terminating inactive websocket + // it didn't actually close in time .. we set connector to disconnected and deleted sharedSecret + // but then a stray message json rpc return from hadshake arrived after that and couldn't be decrypted + // because it shouldn't have arrived in the first place after websocket was supposedly closed + // solution: __closed flag on all websockets.. it is set to true at the same time as calling close() + // and then any messages still coming over the wire on such closed websockets are dropped + // we hope websocket is eventually closed though (?) + // see messageCallback in establishAndMaintainConnection, this was fixed there + logger.red(log, `Connector ${connector.endpoint} missing sharedSecret - should not happen...`); } const _decryptedMessage = naclFast.secretbox.open(encryptedData, nonce, connector.sharedSecret); @@ -3330,7 +3344,7 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec const decodedMessage = naclFast.util.encodeUTF8(decryptedMessage); if (connector.verbose) { - logger.write(log, `Received message: ${decodedMessage}`); + logger.yellow(log, `Connector ${connector.endpoint} received message: ${decodedMessage}`); } try { @@ -3376,6 +3390,10 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec throw e; } } else { + if (connector.verbose) { + logger.yellow(log, `Connector ${connector.endpoint} received binary data`); + } + //const binaryData = decryptedMessage; // const sessionId = Buffer.from(binaryData.buffer, binaryData.byteOffset, 64).toString(); // const binaryPayload = Buffer.from(binaryData.buffer, binaryData.byteOffset + 64); @@ -3387,20 +3405,17 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec naclFast.util = naclUtil; +const wsOPEN = 1; + function diffieHellman({ connector, afterFirstStep = () => {} }) { - const { - clientPrivateKey, - clientPublicKey, - clientPublicKeyHex, - protocol, - tag, - endpoint, - verbose - } = connector; + const { clientPrivateKey, clientPublicKey, clientPublicKeyHex, protocol, tag, endpoint, verbose } = + connector; return new Promise((success, reject) => { - connector.remoteObject('Auth') + connector + .remoteObject('Auth') .call('exchangePubkeys', { pubkey: clientPublicKeyHex }) + //.call('exchangePubkeys', { pubkey: clientPublicKeyHex, clientWsId: connector.connection.websocket.__id }) .then(remotePubkeyHex => { const sharedSecret = naclFast.box.before(hexToBuffer(remotePubkeyHex), clientPrivateKey); @@ -3413,33 +3428,50 @@ function diffieHellman({ connector, afterFirstStep = () => {} }) { ); } - connector.remoteObject('Auth') - .call('finalizeHandshake', { protocol }) - .then(res => { - // finalizeHandshake rpc endpoint on server can cleanly retorn {error} as a result - // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) - if (res && res.error) { - console.log(res.error); - // this connection will keep hangling and no reconnect tries will be made - // since we keep websocket open just that nothing is happening - - // when we enable the protocol on the endpoint we have to restart the process - // frontend connector will get disconnected at this point, websocket will close - // and from then on it tries reconnecting again so when ws first connects - // and protocol is present , it will be a success - - // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging - } else { - success(); - - const _tag = tag ? ` (${tag})` : ''; - logger.cyan( - connector.log, - `${endpoint}${_tag} ✓ Connection [ ${protocol || '"no-name"'} ] ready` - ); - } - }) - .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + // if connection has closed at this point we don't try to send into closed + // connection, it would still work but error would be logged + if (connector.connection.websocket.readyState == wsOPEN) { + connector + .remoteObject('Auth') + .call('finalizeHandshake', { protocol }) + .then(res => { + // finalizeHandshake rpc endpoint on server can cleanly return {error} as a result + // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) + if (res && res.error) { + console.log(res.error); + // this connection will keep hangling and no reconnect tries will be made + // since we keep websocket open just that nothing is happening + + // when we enable the protocol on the endpoint we have to restart the process + // frontend connector will get disconnected at this point, websocket will close + // and from then on it tries reconnecting again so when ws first connects + // and protocol is present , it will be a success + + // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging + } else { + success(); + + const _tag = tag ? ` (${tag})` : ''; + logger.cyan( + connector.log, + `✓✓✓ ${endpoint}${_tag} ✓ Connection #${connector.connection.websocket.__id} [ ${ + protocol || '"no-name"' + } ] ready` + ); + } + }) + .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + } else { + const _tag = tag ? ` (${tag})` : ''; + logger.yellow( + connector.log, + `${endpoint}${_tag} ✖ Connection [ ${ + protocol || '"no-name"' + } ] closed just before finalizeHandshake step` + ); + // don't reject here -- because it will show some wring log message in connector + // on:ready error "will not try to reconnect" .. which is not the case here + } }) .catch(reject); }); @@ -4871,7 +4903,7 @@ const DECOMMISSION_INACTIVITY = 60000; // 1min //const DECOMMISSION_INACTIVITY = 120000; // 2min //const DECOMMISSION_INACTIVITY = 10000; // 2min -const wsOPEN = 1; +const wsOPEN$1 = 1; class Connector extends Eev { constructor({ @@ -5027,7 +5059,7 @@ class Connector extends Eev { this.successfulConnectsCount += 1; if (this.verbose) { - logger.green(this.log, `✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`); + logger.white(this.log, `✓ Connector ${this.endpoint} connected (${this.successfulConnectsCount} total reconnects)`); } const websocketId = this.connection.websocket.__id; @@ -5059,7 +5091,7 @@ class Connector extends Eev { // but sometimes we also get an open websocket after rpc timeout (not sure but this code handles it anyway, should be no problem, only better for all cases) if ( this.connection.websocket.__id == websocketId && - this.connection.websocket.readyState == wsOPEN + this.connection.websocket.readyState == wsOPEN$1 ) { //⚠️ we only show if it seems still relevant, special case // previously we had this first log output above this if statement @@ -5243,7 +5275,7 @@ function determineEndpoint({ endpoint, host, port }) { const browser$1 = typeof window !== 'undefined'; const wsCONNECTING = 0; -const wsOPEN$1 = 1; +const wsOPEN$2 = 1; //const wsCLOSING = 2; //const wsCLOSED = 3; @@ -5257,6 +5289,22 @@ const CONN_IDLE_TICKS = 3; // how long to wait for a new websocket to connect... after this we cancel it const WAIT_FOR_NEW_CONN_TICKS = 5; // 5000 ms ( = (5) * CONN_CHECK_INTERVAL ) +function addListener(name, callback, ws) { + if (browser$1) { + ws.addEventListener(name, callback); + } else { + ws.on(name, callback); + } +} + +function removeListener(name, callback, ws) { + if (browser$1) { + ws.removeEventListener(name, callback); + } else { + ws.off(name, callback); + } +} + //todo: remove 'dummy' argument once legacyLib with old MCS is history function establishAndMaintainConnection( { @@ -5296,7 +5344,8 @@ function establishAndMaintainConnection( connector.connection = { terminate() { this.websocket._removeAllCallbacks(); - this.websocket.close(); + this.websocket.__closed = true; + this.websocket.close(); // might take some time to actually close, we can get stray messages through that websocket //connector.connectStatus(undefined); connector.connectStatus(false); reconnect(); @@ -5330,14 +5379,14 @@ function checkConnection({ connector, reconnect, log }) { // decommissioned logger.yellow( log, - `${connector.endpoint} Connection decommisioned, closing websocket ${conn.websocket.__id}, will not retry again ` + `${connector.endpoint} Connection decommisioned, closing websocket #${conn.websocket.__id}, will not retry again ` ); decommission(connector); } else { // idle connection connector.emit('inactive_connection'); - logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection`); + logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection #${conn.websocket.__id}`); } conn.terminate(); @@ -5379,6 +5428,8 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb return; } + const wsId = Math.round(10 ** 5 * Math.random()).toString(); + //logger.write(log, `${endpoint} CONN_TICK`); //logger.write(log, `${endpoint} wsReadyState ${conn.currentlyTryingWS?.readyState}`); @@ -5394,9 +5445,10 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); } else if (verbose || browser$1) { - logger.write(log, `${endpoint} Created new websocket`); + logger.write(log, `${endpoint} Created new websocket #${wsId}`); } // so in case when device is online but websocket server is not running we usually @@ -5407,7 +5459,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb // (see above)... and we try with a new websocket every 4800ms again instead on every tick (800ms) const ws = new WebSocket(endpoint); - ws.__id = Math.random(); + ws.__id = wsId; conn.currentlyTryingWS = ws; conn.currentlyTryingWS._waitForConnectCounter = 0; @@ -5427,7 +5479,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } if (verbose || browser$1) { - logger.write(log, `${endpoint} Websocket open`); + logger.write(log, `${endpoint} Websocket #${wsId} open`); } conn.currentlyTryingWS = null; @@ -5440,14 +5492,14 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb }; ws._removeAllCallbacks = () => { - ws.removeEventListener('open', openCallback); + // logger.red( + // log, + // `${connector.endpoint} removing 1 callback (open) on ws #${ws.__id} [ ${connector.protocol} ]` + // ); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('open', openCallback); - } else { - ws.on('open', openCallback); - } + addListener('open', openCallback, ws); } function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, verbose }) { @@ -5464,7 +5516,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; const closeCallback = () => { - logger.write(log, `${connector.endpoint} ✖ Connection closed`); + //❗❗❗❗ -- can get stray messages even here!! after close callback ws implementation lets a few (one) messages through!! + // this only happened on LAN ... + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+167ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 ✖ Connection #28485 [ dmt ] closed' + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+01ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 Created new websocket #17068' + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+338ms) ∞ 1.0.0.1 consecutiveUnresolvedTimeout after 2x unresolved promise + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+43ms) ∞ lanServerConn — "ws://192.168.0.10:7780 connection #28485 [ dmt ] received msg '��\x19X���9�߈�V^L�#�b��)\x02�\r��n\x06^?U�v�\x00�ͻ>����k~�A(^�\t�İP�=���X*���'" + // maybe not needed anymore after listeners issue was fixed ..... + ws.__closed = true; + + logger.blue(log, `${connector.endpoint} ✖ Connection #${ws.__id} [ ${connector.protocol} ] closed`); if (connector.decommissioned) { connector.connectStatus(false); @@ -5477,6 +5538,7 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v // flip side is that there is such small delay between when we stop some process and when red x appears... but it's quite ok! // we do however disable all commands immediately ... so: show red X when connect status is FALSE excusively and disable all gui actions when it's NOT TRUE (false or undefined) connector.connectStatus(undefined); + reconnect(); //setTimeout(reconnect, MAX_RECONNECT_DELAY_AFTER_WS_CLOSE * Math.random()); // turns out we don't really need to do these delays, works fine without }; @@ -5490,11 +5552,26 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v const msg = browser$1 ? _msg.data : _msg; + if (ws.__closed) { + // if (msg != 'pong') { + // logger.red( + // log, + // `${connector.endpoint} Already closed connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + // } + return; + } + if (msg == 'pong') { connector.emit('pong'); return; } + // logger.red( + // log, + // `${connector.endpoint} connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + let jsonData; try { @@ -5510,22 +5587,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; ws._removeAllCallbacks = () => { - ws.removeEventListener('error', errorCallback); - ws.removeEventListener('close', closeCallback); - ws.removeEventListener('message', messageCallback); - - ws.removeEventListener('open', openCallback); + // logger.red(log, `${connector.endpoint} removing 4 callbacks on ws #${ws.__id} [ ${connector.protocol} ]`); + removeListener('error', errorCallback, ws); + removeListener('close', closeCallback, ws); + removeListener('message', messageCallback, ws); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('error', errorCallback); - ws.addEventListener('close', closeCallback); - ws.addEventListener('message', messageCallback); - } else { - ws.on('error', errorCallback); - ws.on('close', closeCallback); - ws.on('message', messageCallback); - } + addListener('error', errorCallback, ws); + addListener('close', closeCallback, ws); + addListener('message', messageCallback, ws); } function decommission(connector) { @@ -5533,21 +5604,23 @@ function decommission(connector) { if (conn.currentlyTryingWS) { conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); conn.currentlyTryingWS = null; } - if (conn.ws) { - conn.ws._removeAllCallbacks(); - conn.ws.close(); - conn.ws = null; + if (conn.websocket) { + conn.websocket._removeAllCallbacks(); + conn.websocket.__closed = true; + conn.websocket.close(); + conn.websocket = null; } connector.connectStatus(false); } function socketConnected(conn) { - return conn.websocket && conn.websocket.readyState == wsOPEN$1; + return conn.websocket && conn.websocket.readyState == wsOPEN$2 && !conn.websocket.__closed; // when terminating connection, might be useful -- check } function connectionIdle(conn) { @@ -5956,4 +6029,8 @@ class MultiConnectedStore extends MergeStore { } } -export { MultiConnectedStore }; +function isEmptyObject(obj) { + return typeof obj === 'object' && Object.keys(obj).length === 0; +} + +export { MultiConnectedStore, isEmptyObject }; diff --git a/core/node/connectome/stores/node/index.js b/core/node/connectome/stores/node/index.js index b5b7dd9a1..f2370c64d 100644 --- a/core/node/connectome/stores/node/index.js +++ b/core/node/connectome/stores/node/index.js @@ -5384,7 +5384,7 @@ function send({ data, connector }) { log, `Connector ${connector.endpoint} → Sending message #${connector.sentCount} ↴` ); - logger.gray(log, data); + logger.cyan(log, data); } connector.connection.websocket.send(data); @@ -5450,11 +5450,25 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec // 💡 encryptedJson data!! if (connector.verbose == 'extra') { logger.magenta(log, `Connector ${connector.endpoint} received bytes ↴`); - logger.gray(log, encryptedData); - logger.magenta( + logger.cyan(log, encryptedData); + logger.green(log, JSON.stringify(encryptedData)); + logger.gray( log, `Connector ${connector.endpoint} decrypting with shared secret ${connector.sharedSecret}...` ); + //logger.cyan(log, JSON.stringify(connector.sharedSecret)); + } + + if (!connector.sharedSecret) { + // we had this problem before -- zurich wifi -- when terminating inactive websocket + // it didn't actually close in time .. we set connector to disconnected and deleted sharedSecret + // but then a stray message json rpc return from hadshake arrived after that and couldn't be decrypted + // because it shouldn't have arrived in the first place after websocket was supposedly closed + // solution: __closed flag on all websockets.. it is set to true at the same time as calling close() + // and then any messages still coming over the wire on such closed websockets are dropped + // we hope websocket is eventually closed though (?) + // see messageCallback in establishAndMaintainConnection, this was fixed there + logger.red(log, `Connector ${connector.endpoint} missing sharedSecret - should not happen...`); } const _decryptedMessage = naclFast.secretbox.open(encryptedData, nonce, connector.sharedSecret); @@ -5466,7 +5480,7 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec const decodedMessage = naclFast.util.encodeUTF8(decryptedMessage); if (connector.verbose) { - logger.write(log, `Received message: ${decodedMessage}`); + logger.yellow(log, `Connector ${connector.endpoint} received message: ${decodedMessage}`); } try { @@ -5512,6 +5526,10 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec throw e; } } else { + if (connector.verbose) { + logger.yellow(log, `Connector ${connector.endpoint} received binary data`); + } + //const binaryData = decryptedMessage; // const sessionId = Buffer.from(binaryData.buffer, binaryData.byteOffset, 64).toString(); // const binaryPayload = Buffer.from(binaryData.buffer, binaryData.byteOffset + 64); @@ -5523,20 +5541,17 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec naclFast.util = naclUtil; +const wsOPEN = 1; + function diffieHellman({ connector, afterFirstStep = () => {} }) { - const { - clientPrivateKey, - clientPublicKey, - clientPublicKeyHex, - protocol, - tag, - endpoint, - verbose - } = connector; + const { clientPrivateKey, clientPublicKey, clientPublicKeyHex, protocol, tag, endpoint, verbose } = + connector; return new Promise((success, reject) => { - connector.remoteObject('Auth') + connector + .remoteObject('Auth') .call('exchangePubkeys', { pubkey: clientPublicKeyHex }) + //.call('exchangePubkeys', { pubkey: clientPublicKeyHex, clientWsId: connector.connection.websocket.__id }) .then(remotePubkeyHex => { const sharedSecret = naclFast.box.before(hexToBuffer(remotePubkeyHex), clientPrivateKey); @@ -5549,33 +5564,50 @@ function diffieHellman({ connector, afterFirstStep = () => {} }) { ); } - connector.remoteObject('Auth') - .call('finalizeHandshake', { protocol }) - .then(res => { - // finalizeHandshake rpc endpoint on server can cleanly retorn {error} as a result - // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) - if (res && res.error) { - console.log(res.error); - // this connection will keep hangling and no reconnect tries will be made - // since we keep websocket open just that nothing is happening - - // when we enable the protocol on the endpoint we have to restart the process - // frontend connector will get disconnected at this point, websocket will close - // and from then on it tries reconnecting again so when ws first connects - // and protocol is present , it will be a success - - // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging - } else { - success(); - - const _tag = tag ? ` (${tag})` : ''; - logger.cyan( - connector.log, - `${endpoint}${_tag} ✓ Connection [ ${protocol || '"no-name"'} ] ready` - ); - } - }) - .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + // if connection has closed at this point we don't try to send into closed + // connection, it would still work but error would be logged + if (connector.connection.websocket.readyState == wsOPEN) { + connector + .remoteObject('Auth') + .call('finalizeHandshake', { protocol }) + .then(res => { + // finalizeHandshake rpc endpoint on server can cleanly return {error} as a result + // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) + if (res && res.error) { + console.log(res.error); + // this connection will keep hangling and no reconnect tries will be made + // since we keep websocket open just that nothing is happening + + // when we enable the protocol on the endpoint we have to restart the process + // frontend connector will get disconnected at this point, websocket will close + // and from then on it tries reconnecting again so when ws first connects + // and protocol is present , it will be a success + + // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging + } else { + success(); + + const _tag = tag ? ` (${tag})` : ''; + logger.cyan( + connector.log, + `✓✓✓ ${endpoint}${_tag} ✓ Connection #${connector.connection.websocket.__id} [ ${ + protocol || '"no-name"' + } ] ready` + ); + } + }) + .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + } else { + const _tag = tag ? ` (${tag})` : ''; + logger.yellow( + connector.log, + `${endpoint}${_tag} ✖ Connection [ ${ + protocol || '"no-name"' + } ] closed just before finalizeHandshake step` + ); + // don't reject here -- because it will show some wring log message in connector + // on:ready error "will not try to reconnect" .. which is not the case here + } }) .catch(reject); }); @@ -6239,7 +6271,7 @@ const DECOMMISSION_INACTIVITY = 60000; // 1min //const DECOMMISSION_INACTIVITY = 120000; // 2min //const DECOMMISSION_INACTIVITY = 10000; // 2min -const wsOPEN = 1; +const wsOPEN$1 = 1; class Connector extends Eev { constructor({ @@ -6395,7 +6427,7 @@ class Connector extends Eev { this.successfulConnectsCount += 1; if (this.verbose) { - logger.green(this.log, `✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`); + logger.white(this.log, `✓ Connector ${this.endpoint} connected (${this.successfulConnectsCount} total reconnects)`); } const websocketId = this.connection.websocket.__id; @@ -6427,7 +6459,7 @@ class Connector extends Eev { // but sometimes we also get an open websocket after rpc timeout (not sure but this code handles it anyway, should be no problem, only better for all cases) if ( this.connection.websocket.__id == websocketId && - this.connection.websocket.readyState == wsOPEN + this.connection.websocket.readyState == wsOPEN$1 ) { //⚠️ we only show if it seems still relevant, special case // previously we had this first log output above this if statement @@ -6611,7 +6643,7 @@ function determineEndpoint({ endpoint, host, port }) { const browser$1 = typeof window !== 'undefined'; const wsCONNECTING = 0; -const wsOPEN$1 = 1; +const wsOPEN$2 = 1; //const wsCLOSING = 2; //const wsCLOSED = 3; @@ -6625,6 +6657,22 @@ const CONN_IDLE_TICKS = 3; // how long to wait for a new websocket to connect... after this we cancel it const WAIT_FOR_NEW_CONN_TICKS = 5; // 5000 ms ( = (5) * CONN_CHECK_INTERVAL ) +function addListener(name, callback, ws) { + if (browser$1) { + ws.addEventListener(name, callback); + } else { + ws.on(name, callback); + } +} + +function removeListener(name, callback, ws) { + if (browser$1) { + ws.removeEventListener(name, callback); + } else { + ws.off(name, callback); + } +} + //todo: remove 'dummy' argument once legacyLib with old MCS is history function establishAndMaintainConnection( { @@ -6664,7 +6712,8 @@ function establishAndMaintainConnection( connector.connection = { terminate() { this.websocket._removeAllCallbacks(); - this.websocket.close(); + this.websocket.__closed = true; + this.websocket.close(); // might take some time to actually close, we can get stray messages through that websocket //connector.connectStatus(undefined); connector.connectStatus(false); reconnect(); @@ -6698,14 +6747,14 @@ function checkConnection({ connector, reconnect, log }) { // decommissioned logger.yellow( log, - `${connector.endpoint} Connection decommisioned, closing websocket ${conn.websocket.__id}, will not retry again ` + `${connector.endpoint} Connection decommisioned, closing websocket #${conn.websocket.__id}, will not retry again ` ); decommission(connector); } else { // idle connection connector.emit('inactive_connection'); - logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection`); + logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection #${conn.websocket.__id}`); } conn.terminate(); @@ -6747,6 +6796,8 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb return; } + const wsId = Math.round(10 ** 5 * Math.random()).toString(); + //logger.write(log, `${endpoint} CONN_TICK`); //logger.write(log, `${endpoint} wsReadyState ${conn.currentlyTryingWS?.readyState}`); @@ -6762,9 +6813,10 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); } else if (verbose || browser$1) { - logger.write(log, `${endpoint} Created new websocket`); + logger.write(log, `${endpoint} Created new websocket #${wsId}`); } // so in case when device is online but websocket server is not running we usually @@ -6775,7 +6827,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb // (see above)... and we try with a new websocket every 4800ms again instead on every tick (800ms) const ws = new WebSocket(endpoint); - ws.__id = Math.random(); + ws.__id = wsId; conn.currentlyTryingWS = ws; conn.currentlyTryingWS._waitForConnectCounter = 0; @@ -6795,7 +6847,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } if (verbose || browser$1) { - logger.write(log, `${endpoint} Websocket open`); + logger.write(log, `${endpoint} Websocket #${wsId} open`); } conn.currentlyTryingWS = null; @@ -6808,14 +6860,14 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb }; ws._removeAllCallbacks = () => { - ws.removeEventListener('open', openCallback); + // logger.red( + // log, + // `${connector.endpoint} removing 1 callback (open) on ws #${ws.__id} [ ${connector.protocol} ]` + // ); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('open', openCallback); - } else { - ws.on('open', openCallback); - } + addListener('open', openCallback, ws); } function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, verbose }) { @@ -6832,7 +6884,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; const closeCallback = () => { - logger.write(log, `${connector.endpoint} ✖ Connection closed`); + //❗❗❗❗ -- can get stray messages even here!! after close callback ws implementation lets a few (one) messages through!! + // this only happened on LAN ... + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+167ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 ✖ Connection #28485 [ dmt ] closed' + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+01ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 Created new websocket #17068' + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+338ms) ∞ 1.0.0.1 consecutiveUnresolvedTimeout after 2x unresolved promise + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+43ms) ∞ lanServerConn — "ws://192.168.0.10:7780 connection #28485 [ dmt ] received msg '��\x19X���9�߈�V^L�#�b��)\x02�\r��n\x06^?U�v�\x00�ͻ>����k~�A(^�\t�İP�=���X*���'" + // maybe not needed anymore after listeners issue was fixed ..... + ws.__closed = true; + + logger.blue(log, `${connector.endpoint} ✖ Connection #${ws.__id} [ ${connector.protocol} ] closed`); if (connector.decommissioned) { connector.connectStatus(false); @@ -6845,6 +6906,7 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v // flip side is that there is such small delay between when we stop some process and when red x appears... but it's quite ok! // we do however disable all commands immediately ... so: show red X when connect status is FALSE excusively and disable all gui actions when it's NOT TRUE (false or undefined) connector.connectStatus(undefined); + reconnect(); //setTimeout(reconnect, MAX_RECONNECT_DELAY_AFTER_WS_CLOSE * Math.random()); // turns out we don't really need to do these delays, works fine without }; @@ -6858,11 +6920,26 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v const msg = browser$1 ? _msg.data : _msg; + if (ws.__closed) { + // if (msg != 'pong') { + // logger.red( + // log, + // `${connector.endpoint} Already closed connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + // } + return; + } + if (msg == 'pong') { connector.emit('pong'); return; } + // logger.red( + // log, + // `${connector.endpoint} connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + let jsonData; try { @@ -6878,22 +6955,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; ws._removeAllCallbacks = () => { - ws.removeEventListener('error', errorCallback); - ws.removeEventListener('close', closeCallback); - ws.removeEventListener('message', messageCallback); - - ws.removeEventListener('open', openCallback); + // logger.red(log, `${connector.endpoint} removing 4 callbacks on ws #${ws.__id} [ ${connector.protocol} ]`); + removeListener('error', errorCallback, ws); + removeListener('close', closeCallback, ws); + removeListener('message', messageCallback, ws); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('error', errorCallback); - ws.addEventListener('close', closeCallback); - ws.addEventListener('message', messageCallback); - } else { - ws.on('error', errorCallback); - ws.on('close', closeCallback); - ws.on('message', messageCallback); - } + addListener('error', errorCallback, ws); + addListener('close', closeCallback, ws); + addListener('message', messageCallback, ws); } function decommission(connector) { @@ -6901,21 +6972,23 @@ function decommission(connector) { if (conn.currentlyTryingWS) { conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); conn.currentlyTryingWS = null; } - if (conn.ws) { - conn.ws._removeAllCallbacks(); - conn.ws.close(); - conn.ws = null; + if (conn.websocket) { + conn.websocket._removeAllCallbacks(); + conn.websocket.__closed = true; + conn.websocket.close(); + conn.websocket = null; } connector.connectStatus(false); } function socketConnected(conn) { - return conn.websocket && conn.websocket.readyState == wsOPEN$1; + return conn.websocket && conn.websocket.readyState == wsOPEN$2 && !conn.websocket.__closed; // when terminating connection, might be useful -- check } function connectionIdle(conn) { @@ -7324,5 +7397,10 @@ class MultiConnectedStore extends MergeStore { } } +function isEmptyObject(obj) { + return typeof obj === 'object' && Object.keys(obj).length === 0; +} + exports.MultiConnectedStore = MultiConnectedStore; exports.SyncStore = SyncStore; +exports.isEmptyObject = isEmptyObject; diff --git a/core/node/connectome/stores/node/index.mjs b/core/node/connectome/stores/node/index.mjs index 360d18388..863d22db8 100644 --- a/core/node/connectome/stores/node/index.mjs +++ b/core/node/connectome/stores/node/index.mjs @@ -5370,7 +5370,7 @@ function send({ data, connector }) { log, `Connector ${connector.endpoint} → Sending message #${connector.sentCount} ↴` ); - logger.gray(log, data); + logger.cyan(log, data); } connector.connection.websocket.send(data); @@ -5436,11 +5436,25 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec // 💡 encryptedJson data!! if (connector.verbose == 'extra') { logger.magenta(log, `Connector ${connector.endpoint} received bytes ↴`); - logger.gray(log, encryptedData); - logger.magenta( + logger.cyan(log, encryptedData); + logger.green(log, JSON.stringify(encryptedData)); + logger.gray( log, `Connector ${connector.endpoint} decrypting with shared secret ${connector.sharedSecret}...` ); + //logger.cyan(log, JSON.stringify(connector.sharedSecret)); + } + + if (!connector.sharedSecret) { + // we had this problem before -- zurich wifi -- when terminating inactive websocket + // it didn't actually close in time .. we set connector to disconnected and deleted sharedSecret + // but then a stray message json rpc return from hadshake arrived after that and couldn't be decrypted + // because it shouldn't have arrived in the first place after websocket was supposedly closed + // solution: __closed flag on all websockets.. it is set to true at the same time as calling close() + // and then any messages still coming over the wire on such closed websockets are dropped + // we hope websocket is eventually closed though (?) + // see messageCallback in establishAndMaintainConnection, this was fixed there + logger.red(log, `Connector ${connector.endpoint} missing sharedSecret - should not happen...`); } const _decryptedMessage = naclFast.secretbox.open(encryptedData, nonce, connector.sharedSecret); @@ -5452,7 +5466,7 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec const decodedMessage = naclFast.util.encodeUTF8(decryptedMessage); if (connector.verbose) { - logger.write(log, `Received message: ${decodedMessage}`); + logger.yellow(log, `Connector ${connector.endpoint} received message: ${decodedMessage}`); } try { @@ -5498,6 +5512,10 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec throw e; } } else { + if (connector.verbose) { + logger.yellow(log, `Connector ${connector.endpoint} received binary data`); + } + //const binaryData = decryptedMessage; // const sessionId = Buffer.from(binaryData.buffer, binaryData.byteOffset, 64).toString(); // const binaryPayload = Buffer.from(binaryData.buffer, binaryData.byteOffset + 64); @@ -5509,20 +5527,17 @@ function wireReceive({ jsonData, encryptedData, rawMessage, wasEncrypted, connec naclFast.util = naclUtil; +const wsOPEN = 1; + function diffieHellman({ connector, afterFirstStep = () => {} }) { - const { - clientPrivateKey, - clientPublicKey, - clientPublicKeyHex, - protocol, - tag, - endpoint, - verbose - } = connector; + const { clientPrivateKey, clientPublicKey, clientPublicKeyHex, protocol, tag, endpoint, verbose } = + connector; return new Promise((success, reject) => { - connector.remoteObject('Auth') + connector + .remoteObject('Auth') .call('exchangePubkeys', { pubkey: clientPublicKeyHex }) + //.call('exchangePubkeys', { pubkey: clientPublicKeyHex, clientWsId: connector.connection.websocket.__id }) .then(remotePubkeyHex => { const sharedSecret = naclFast.box.before(hexToBuffer(remotePubkeyHex), clientPrivateKey); @@ -5535,33 +5550,50 @@ function diffieHellman({ connector, afterFirstStep = () => {} }) { ); } - connector.remoteObject('Auth') - .call('finalizeHandshake', { protocol }) - .then(res => { - // finalizeHandshake rpc endpoint on server can cleanly retorn {error} as a result - // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) - if (res && res.error) { - console.log(res.error); - // this connection will keep hangling and no reconnect tries will be made - // since we keep websocket open just that nothing is happening - - // when we enable the protocol on the endpoint we have to restart the process - // frontend connector will get disconnected at this point, websocket will close - // and from then on it tries reconnecting again so when ws first connects - // and protocol is present , it will be a success - - // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging - } else { - success(); - - const _tag = tag ? ` (${tag})` : ''; - logger.cyan( - connector.log, - `${endpoint}${_tag} ✓ Connection [ ${protocol || '"no-name"'} ] ready` - ); - } - }) - .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + // if connection has closed at this point we don't try to send into closed + // connection, it would still work but error would be logged + if (connector.connection.websocket.readyState == wsOPEN) { + connector + .remoteObject('Auth') + .call('finalizeHandshake', { protocol }) + .then(res => { + // finalizeHandshake rpc endpoint on server can cleanly return {error} as a result + // in case the protocol we are trying to connect to is not registered (does not exist at the endpoint) + if (res && res.error) { + console.log(res.error); + // this connection will keep hangling and no reconnect tries will be made + // since we keep websocket open just that nothing is happening + + // when we enable the protocol on the endpoint we have to restart the process + // frontend connector will get disconnected at this point, websocket will close + // and from then on it tries reconnecting again so when ws first connects + // and protocol is present , it will be a success + + // DONT'T REJECT here! reject(res.error); -- we need to keep this websocket hanging + } else { + success(); + + const _tag = tag ? ` (${tag})` : ''; + logger.cyan( + connector.log, + `✓✓✓ ${endpoint}${_tag} ✓ Connection #${connector.connection.websocket.__id} [ ${ + protocol || '"no-name"' + } ] ready` + ); + } + }) + .catch(reject); // for example Timeout ... delayed! we have to be careful with closing any connections because new websocket might have already be created, we should not close that one + } else { + const _tag = tag ? ` (${tag})` : ''; + logger.yellow( + connector.log, + `${endpoint}${_tag} ✖ Connection [ ${ + protocol || '"no-name"' + } ] closed just before finalizeHandshake step` + ); + // don't reject here -- because it will show some wring log message in connector + // on:ready error "will not try to reconnect" .. which is not the case here + } }) .catch(reject); }); @@ -6225,7 +6257,7 @@ const DECOMMISSION_INACTIVITY = 60000; // 1min //const DECOMMISSION_INACTIVITY = 120000; // 2min //const DECOMMISSION_INACTIVITY = 10000; // 2min -const wsOPEN = 1; +const wsOPEN$1 = 1; class Connector extends Eev { constructor({ @@ -6381,7 +6413,7 @@ class Connector extends Eev { this.successfulConnectsCount += 1; if (this.verbose) { - logger.green(this.log, `✓ Connector ${this.endpoint} connected #${this.successfulConnectsCount}`); + logger.white(this.log, `✓ Connector ${this.endpoint} connected (${this.successfulConnectsCount} total reconnects)`); } const websocketId = this.connection.websocket.__id; @@ -6413,7 +6445,7 @@ class Connector extends Eev { // but sometimes we also get an open websocket after rpc timeout (not sure but this code handles it anyway, should be no problem, only better for all cases) if ( this.connection.websocket.__id == websocketId && - this.connection.websocket.readyState == wsOPEN + this.connection.websocket.readyState == wsOPEN$1 ) { //⚠️ we only show if it seems still relevant, special case // previously we had this first log output above this if statement @@ -6597,7 +6629,7 @@ function determineEndpoint({ endpoint, host, port }) { const browser$1 = typeof window !== 'undefined'; const wsCONNECTING = 0; -const wsOPEN$1 = 1; +const wsOPEN$2 = 1; //const wsCLOSING = 2; //const wsCLOSED = 3; @@ -6611,6 +6643,22 @@ const CONN_IDLE_TICKS = 3; // how long to wait for a new websocket to connect... after this we cancel it const WAIT_FOR_NEW_CONN_TICKS = 5; // 5000 ms ( = (5) * CONN_CHECK_INTERVAL ) +function addListener(name, callback, ws) { + if (browser$1) { + ws.addEventListener(name, callback); + } else { + ws.on(name, callback); + } +} + +function removeListener(name, callback, ws) { + if (browser$1) { + ws.removeEventListener(name, callback); + } else { + ws.off(name, callback); + } +} + //todo: remove 'dummy' argument once legacyLib with old MCS is history function establishAndMaintainConnection( { @@ -6650,7 +6698,8 @@ function establishAndMaintainConnection( connector.connection = { terminate() { this.websocket._removeAllCallbacks(); - this.websocket.close(); + this.websocket.__closed = true; + this.websocket.close(); // might take some time to actually close, we can get stray messages through that websocket //connector.connectStatus(undefined); connector.connectStatus(false); reconnect(); @@ -6684,14 +6733,14 @@ function checkConnection({ connector, reconnect, log }) { // decommissioned logger.yellow( log, - `${connector.endpoint} Connection decommisioned, closing websocket ${conn.websocket.__id}, will not retry again ` + `${connector.endpoint} Connection decommisioned, closing websocket #${conn.websocket.__id}, will not retry again ` ); decommission(connector); } else { // idle connection connector.emit('inactive_connection'); - logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection`); + logger.yellow(log, `${connector.endpoint} ✖ Terminated inactive connection #${conn.websocket.__id}`); } conn.terminate(); @@ -6733,6 +6782,8 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb return; } + const wsId = Math.round(10 ** 5 * Math.random()).toString(); + //logger.write(log, `${endpoint} CONN_TICK`); //logger.write(log, `${endpoint} wsReadyState ${conn.currentlyTryingWS?.readyState}`); @@ -6748,9 +6799,10 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); } else if (verbose || browser$1) { - logger.write(log, `${endpoint} Created new websocket`); + logger.write(log, `${endpoint} Created new websocket #${wsId}`); } // so in case when device is online but websocket server is not running we usually @@ -6761,7 +6813,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb // (see above)... and we try with a new websocket every 4800ms again instead on every tick (800ms) const ws = new WebSocket(endpoint); - ws.__id = Math.random(); + ws.__id = wsId; conn.currentlyTryingWS = ws; conn.currentlyTryingWS._waitForConnectCounter = 0; @@ -6781,7 +6833,7 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb } if (verbose || browser$1) { - logger.write(log, `${endpoint} Websocket open`); + logger.write(log, `${endpoint} Websocket #${wsId} open`); } conn.currentlyTryingWS = null; @@ -6794,14 +6846,14 @@ function tryReconnect({ connector, endpoint }, { WebSocket, reconnect, log, verb }; ws._removeAllCallbacks = () => { - ws.removeEventListener('open', openCallback); + // logger.red( + // log, + // `${connector.endpoint} removing 1 callback (open) on ws #${ws.__id} [ ${connector.protocol} ]` + // ); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('open', openCallback); - } else { - ws.on('open', openCallback); - } + addListener('open', openCallback, ws); } function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, verbose }) { @@ -6818,7 +6870,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; const closeCallback = () => { - logger.write(log, `${connector.endpoint} ✖ Connection closed`); + //❗❗❗❗ -- can get stray messages even here!! after close callback ws implementation lets a few (one) messages through!! + // this only happened on LAN ... + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+167ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 ✖ Connection #28485 [ dmt ] closed' + // [run] turbine 82106 4/17/2023, 11:27:25 AM (+01ms) ∞ lanServerConn — 'ws://192.168.0.10:7780 Created new websocket #17068' + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+338ms) ∞ 1.0.0.1 consecutiveUnresolvedTimeout after 2x unresolved promise + // [run] turbine 82106 4/17/2023, 11:27:26 AM (+43ms) ∞ lanServerConn — "ws://192.168.0.10:7780 connection #28485 [ dmt ] received msg '��\x19X���9�߈�V^L�#�b��)\x02�\r��n\x06^?U�v�\x00�ͻ>����k~�A(^�\t�İP�=���X*���'" + // maybe not needed anymore after listeners issue was fixed ..... + ws.__closed = true; + + logger.blue(log, `${connector.endpoint} ✖ Connection #${ws.__id} [ ${connector.protocol} ] closed`); if (connector.decommissioned) { connector.connectStatus(false); @@ -6831,6 +6892,7 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v // flip side is that there is such small delay between when we stop some process and when red x appears... but it's quite ok! // we do however disable all commands immediately ... so: show red X when connect status is FALSE excusively and disable all gui actions when it's NOT TRUE (false or undefined) connector.connectStatus(undefined); + reconnect(); //setTimeout(reconnect, MAX_RECONNECT_DELAY_AFTER_WS_CLOSE * Math.random()); // turns out we don't really need to do these delays, works fine without }; @@ -6844,11 +6906,26 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v const msg = browser$1 ? _msg.data : _msg; + if (ws.__closed) { + // if (msg != 'pong') { + // logger.red( + // log, + // `${connector.endpoint} Already closed connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + // } + return; + } + if (msg == 'pong') { connector.emit('pong'); return; } + // logger.red( + // log, + // `${connector.endpoint} connection #${ws.__id} [ ${connector.protocol} ] received msg '${msg}'` + // ); + let jsonData; try { @@ -6864,22 +6941,16 @@ function addSocketListeners({ ws, connector, openCallback, reconnect }, { log, v }; ws._removeAllCallbacks = () => { - ws.removeEventListener('error', errorCallback); - ws.removeEventListener('close', closeCallback); - ws.removeEventListener('message', messageCallback); - - ws.removeEventListener('open', openCallback); + // logger.red(log, `${connector.endpoint} removing 4 callbacks on ws #${ws.__id} [ ${connector.protocol} ]`); + removeListener('error', errorCallback, ws); + removeListener('close', closeCallback, ws); + removeListener('message', messageCallback, ws); + removeListener('open', openCallback, ws); }; - if (browser$1) { - ws.addEventListener('error', errorCallback); - ws.addEventListener('close', closeCallback); - ws.addEventListener('message', messageCallback); - } else { - ws.on('error', errorCallback); - ws.on('close', closeCallback); - ws.on('message', messageCallback); - } + addListener('error', errorCallback, ws); + addListener('close', closeCallback, ws); + addListener('message', messageCallback, ws); } function decommission(connector) { @@ -6887,21 +6958,23 @@ function decommission(connector) { if (conn.currentlyTryingWS) { conn.currentlyTryingWS._removeAllCallbacks(); + conn.currentlyTryingWS.__closed = true; conn.currentlyTryingWS.close(); conn.currentlyTryingWS = null; } - if (conn.ws) { - conn.ws._removeAllCallbacks(); - conn.ws.close(); - conn.ws = null; + if (conn.websocket) { + conn.websocket._removeAllCallbacks(); + conn.websocket.__closed = true; + conn.websocket.close(); + conn.websocket = null; } connector.connectStatus(false); } function socketConnected(conn) { - return conn.websocket && conn.websocket.readyState == wsOPEN$1; + return conn.websocket && conn.websocket.readyState == wsOPEN$2 && !conn.websocket.__closed; // when terminating connection, might be useful -- check } function connectionIdle(conn) { @@ -7310,4 +7383,8 @@ class MultiConnectedStore extends MergeStore { } } -export { MultiConnectedStore, SyncStore }; +function isEmptyObject(obj) { + return typeof obj === 'object' && Object.keys(obj).length === 0; +} + +export { MultiConnectedStore, SyncStore, isEmptyObject }; diff --git a/core/node/connectome/yarn.lock b/core/node/connectome/yarn.lock index b09bb3ae8..4e8d1e686 100644 --- a/core/node/connectome/yarn.lock +++ b/core/node/connectome/yarn.lock @@ -3,276 +3,276 @@ "@rollup/plugin-commonjs@^16.0.0": - "integrity" "sha512-LuNyypCP3msCGVQJ7ki8PqYdpjfEkE/xtFa5DqlF+7IBD0JsfMZ87C58heSwIMint58sAUZbt3ITqOmdQv/dXw==" - "resolved" "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-16.0.0.tgz" - "version" "16.0.0" + version "16.0.0" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-16.0.0.tgz" + integrity sha512-LuNyypCP3msCGVQJ7ki8PqYdpjfEkE/xtFa5DqlF+7IBD0JsfMZ87C58heSwIMint58sAUZbt3ITqOmdQv/dXw== dependencies: "@rollup/pluginutils" "^3.1.0" - "commondir" "^1.0.1" - "estree-walker" "^2.0.1" - "glob" "^7.1.6" - "is-reference" "^1.2.1" - "magic-string" "^0.25.7" - "resolve" "^1.17.0" + commondir "^1.0.1" + estree-walker "^2.0.1" + glob "^7.1.6" + is-reference "^1.2.1" + magic-string "^0.25.7" + resolve "^1.17.0" "@rollup/plugin-node-resolve@^10.0.0": - "integrity" "sha512-sNijGta8fqzwA1VwUEtTvWCx2E7qC70NMsDh4ZG13byAXYigBNZMxALhKUSycBks5gupJdq0lFrKumFrRZ8H3A==" - "resolved" "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-10.0.0.tgz" - "version" "10.0.0" + version "10.0.0" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-10.0.0.tgz" + integrity sha512-sNijGta8fqzwA1VwUEtTvWCx2E7qC70NMsDh4ZG13byAXYigBNZMxALhKUSycBks5gupJdq0lFrKumFrRZ8H3A== dependencies: "@rollup/pluginutils" "^3.1.0" "@types/resolve" "1.17.1" - "builtin-modules" "^3.1.0" - "deepmerge" "^4.2.2" - "is-module" "^1.0.0" - "resolve" "^1.17.0" + builtin-modules "^3.1.0" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.17.0" "@rollup/pluginutils@^3.1.0": - "integrity" "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==" - "resolved" "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz" - "version" "3.1.0" + version "3.1.0" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== dependencies: "@types/estree" "0.0.39" - "estree-walker" "^1.0.1" - "picomatch" "^2.2.2" + estree-walker "^1.0.1" + picomatch "^2.2.2" "@types/estree@*", "@types/estree@0.0.39": - "integrity" "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" - "resolved" "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" - "version" "0.0.39" + version "0.0.39" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== "@types/node@*": - "integrity" "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw==" - "resolved" "https://registry.npmjs.org/@types/node/-/node-14.14.9.tgz" - "version" "14.14.9" + version "14.14.9" + resolved "https://registry.npmjs.org/@types/node/-/node-14.14.9.tgz" + integrity sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw== "@types/resolve@1.17.1": - "integrity" "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==" - "resolved" "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz" - "version" "1.17.1" + version "1.17.1" + resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== dependencies: "@types/node" "*" -"balanced-match@^1.0.0": - "integrity" "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - "resolved" "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz" - "version" "1.0.0" +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -"brace-expansion@^1.1.7": - "integrity" "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==" - "resolved" "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - "version" "1.1.11" +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: - "balanced-match" "^1.0.0" - "concat-map" "0.0.1" - -"browser-util-inspect@^0.2.0": - "integrity" "sha512-R7WvAj0p9FtwS2Jbtc1HUd1+YZdeb5EEqjBSbbOK3owJtW1viWyJDeTPy43QZ7bZ8POtb1yMv++h844486jMsQ==" - "resolved" "https://registry.npmjs.org/browser-util-inspect/-/browser-util-inspect-0.2.0.tgz" - "version" "0.2.0" - -"bufferutil@^4.0.2": - "integrity" "sha512-AtnG3W6M8B2n4xDQ5R+70EXvOpnXsFYg/AK2yTZd+HQ/oxAdz+GI+DvjmhBw3L0ole+LJ0ngqY4JMbDzkfNzhA==" - "resolved" "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.2.tgz" - "version" "4.0.2" + balanced-match "^1.0.0" + concat-map "0.0.1" + +browser-util-inspect@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/browser-util-inspect/-/browser-util-inspect-0.2.0.tgz" + integrity sha512-R7WvAj0p9FtwS2Jbtc1HUd1+YZdeb5EEqjBSbbOK3owJtW1viWyJDeTPy43QZ7bZ8POtb1yMv++h844486jMsQ== + +bufferutil@^4.0.1, bufferutil@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.2.tgz" + integrity sha512-AtnG3W6M8B2n4xDQ5R+70EXvOpnXsFYg/AK2yTZd+HQ/oxAdz+GI+DvjmhBw3L0ole+LJ0ngqY4JMbDzkfNzhA== dependencies: - "node-gyp-build" "^4.2.0" - -"builtin-modules@^3.1.0": - "integrity" "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==" - "resolved" "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz" - "version" "3.1.0" - -"commondir@^1.0.1": - "integrity" "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" - "resolved" "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" - "version" "1.0.1" - -"concat-map@0.0.1": - "integrity" "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - "resolved" "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - "version" "0.0.1" - -"deepmerge@^4.2.2": - "integrity" "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" - "resolved" "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" - "version" "4.2.2" - -"estree-walker@^1.0.1": - "integrity" "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" - "resolved" "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz" - "version" "1.0.1" - -"estree-walker@^2.0.1": - "integrity" "sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg==" - "resolved" "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.1.tgz" - "version" "2.0.1" - -"fast-json-patch@^3.0.0-1": - "integrity" "sha512-6pdFb07cknxvPzCeLsFHStEy+MysPJPgZQ9LbQ/2O67unQF93SNqfdSqnPPl71YMHX+AD8gbl7iuoGFzHEdDuw==" - "resolved" "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz" - "version" "3.0.0-1" - -"fs.realpath@^1.0.0": - "integrity" "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - "version" "1.0.0" - -"fsevents@~2.1.2": - "integrity" "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==" - "resolved" "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz" - "version" "2.1.3" - -"function-bind@^1.1.1": - "integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - "resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" - "version" "1.1.1" - -"glob@^7.1.6": - "integrity" "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==" - "resolved" "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" - "version" "7.1.6" + node-gyp-build "^4.2.0" + +builtin-modules@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz" + integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +estree-walker@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.1.tgz" + integrity sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg== + +fast-json-patch@^3.0.0-1: + version "3.0.0-1" + resolved "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz" + integrity sha512-6pdFb07cknxvPzCeLsFHStEy+MysPJPgZQ9LbQ/2O67unQF93SNqfdSqnPPl71YMHX+AD8gbl7iuoGFzHEdDuw== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +glob@^7.1.6: + version "7.1.6" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: - "fs.realpath" "^1.0.0" - "inflight" "^1.0.4" - "inherits" "2" - "minimatch" "^3.0.4" - "once" "^1.3.0" - "path-is-absolute" "^1.0.0" - -"has@^1.0.3": - "integrity" "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==" - "resolved" "https://registry.npmjs.org/has/-/has-1.0.3.tgz" - "version" "1.0.3" + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== dependencies: - "function-bind" "^1.1.1" + function-bind "^1.1.1" -"inflight@^1.0.4": - "integrity" "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" - "resolved" "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - "version" "1.0.6" +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: - "once" "^1.3.0" - "wrappy" "1" - -"inherits@2": - "integrity" "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - "resolved" "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - "version" "2.0.4" - -"is-core-module@^2.1.0": - "integrity" "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==" - "resolved" "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz" - "version" "2.1.0" + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-core-module@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz" + integrity sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA== dependencies: - "has" "^1.0.3" + has "^1.0.3" -"is-module@^1.0.0": - "integrity" "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=" - "resolved" "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" - "version" "1.0.0" +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" + integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= -"is-reference@^1.2.1": - "integrity" "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==" - "resolved" "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz" - "version" "1.2.1" +is-reference@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== dependencies: "@types/estree" "*" -"kleur@^4.1.5": - "integrity" "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" - "resolved" "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz" - "version" "4.1.5" +kleur@^4.1.5: + version "4.1.5" + resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz" + integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== -"magic-string@^0.25.7": - "integrity" "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==" - "resolved" "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz" - "version" "0.25.7" +magic-string@^0.25.7: + version "0.25.7" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== dependencies: - "sourcemap-codec" "^1.4.4" + sourcemap-codec "^1.4.4" -"minimatch@^3.0.4": - "integrity" "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==" - "resolved" "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" - "version" "3.0.4" +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: - "brace-expansion" "^1.1.7" + brace-expansion "^1.1.7" -"node-gyp-build@^4.2.0": - "integrity" "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==" - "resolved" "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz" - "version" "4.2.3" +node-gyp-build@^4.2.0: + version "4.2.3" + resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz" + integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg== -"once@^1.3.0": - "integrity" "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" - "resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - "version" "1.4.0" +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: - "wrappy" "1" - -"path-is-absolute@^1.0.0": - "integrity" "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - "resolved" "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - "version" "1.0.1" - -"path-parse@^1.0.6": - "integrity" "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - "resolved" "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz" - "version" "1.0.6" - -"picomatch@^2.2.2": - "integrity" "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" - "resolved" "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz" - "version" "2.2.2" - -"quantum-generator@^1.9.1": - "integrity" "sha512-dw+qdGde5Fn8PMn6ARmPT5rhT8ro0fdILDUmBVHPsfsyMW9i5GqCnZy4C7DWzNzFegPlRsEcQjH95ZIsyWxvGg==" - "resolved" "https://registry.npmjs.org/quantum-generator/-/quantum-generator-1.9.3.tgz" - "version" "1.9.3" - -"resolve@^1.17.0": - "integrity" "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==" - "resolved" "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz" - "version" "1.19.0" + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +picomatch@^2.2.2: + version "2.2.2" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +quantum-generator@^1.9.1: + version "1.9.3" + resolved "https://registry.npmjs.org/quantum-generator/-/quantum-generator-1.9.3.tgz" + integrity sha512-dw+qdGde5Fn8PMn6ARmPT5rhT8ro0fdILDUmBVHPsfsyMW9i5GqCnZy4C7DWzNzFegPlRsEcQjH95ZIsyWxvGg== + +resolve@^1.17.0: + version "1.19.0" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== dependencies: - "is-core-module" "^2.1.0" - "path-parse" "^1.0.6" + is-core-module "^2.1.0" + path-parse "^1.0.6" -"rollup@^2.33.3": - "integrity" "sha512-RpayhPTe4Gu/uFGCmk7Gp5Z9Qic2VsqZ040G+KZZvsZYdcuWaJg678JeDJJvJeEQXminu24a2au+y92CUWVd+w==" - "resolved" "https://registry.npmjs.org/rollup/-/rollup-2.33.3.tgz" - "version" "2.33.3" +rollup@^2.33.3: + version "2.33.3" + resolved "https://registry.npmjs.org/rollup/-/rollup-2.33.3.tgz" + integrity sha512-RpayhPTe4Gu/uFGCmk7Gp5Z9Qic2VsqZ040G+KZZvsZYdcuWaJg678JeDJJvJeEQXminu24a2au+y92CUWVd+w== optionalDependencies: - "fsevents" "~2.1.2" - -"sourcemap-codec@^1.4.4": - "integrity" "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" - "resolved" "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" - "version" "1.4.8" - -"tweetnacl-util@^0.15.1": - "integrity" "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" - "resolved" "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz" - "version" "0.15.1" - -"tweetnacl@^1.0.3": - "integrity" "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" - "resolved" "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz" - "version" "1.0.3" - -"utf-8-validate@^5.0.3": - "integrity" "sha512-jtJM6fpGv8C1SoH4PtG22pGto6x+Y8uPprW0tw3//gGFhDDTiuksgradgFN6yRayDP4SyZZa6ZMGHLIa17+M8A==" - "resolved" "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.3.tgz" - "version" "5.0.3" + fsevents "~2.1.2" + +sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +tweetnacl-util@^0.15.1: + version "0.15.1" + resolved "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz" + integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw== + +tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + +utf-8-validate@^5.0.3, utf-8-validate@>=5.0.2: + version "5.0.3" + resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.3.tgz" + integrity sha512-jtJM6fpGv8C1SoH4PtG22pGto6x+Y8uPprW0tw3//gGFhDDTiuksgradgFN6yRayDP4SyZZa6ZMGHLIa17+M8A== dependencies: - "node-gyp-build" "^4.2.0" + node-gyp-build "^4.2.0" -"wrappy@1": - "integrity" "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - "resolved" "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - "version" "1.0.2" +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -"ws@^7.4.5": - "integrity" "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" - "resolved" "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz" - "version" "7.4.5" +ws@^8.13.0: + version "8.13.0" + resolved "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== diff --git a/core/node/controller/processes/abc/proc.js b/core/node/controller/processes/abc/proc.js index d38d4d4f4..cf6a8f1a6 100644 --- a/core/node/controller/processes/abc/proc.js +++ b/core/node/controller/processes/abc/proc.js @@ -19,6 +19,10 @@ const STATS_INTERVAL = 700; const ONE_MINUTE = 60 * 1000; +const CRASH_LOOP_WINDOW_SECONDS = 60; +const MAX_CRASHES = 3; +const crashTimestamps = []; + let startedAt = Date.now(); function correctAbcBootTime() { @@ -46,9 +50,6 @@ let dmtProcCrashedInForegroundAt; let dmtForeground; -const MAX_CRASHES = 3; -const crashTimestamps = []; - function collectStat() { return new Promise((success, reject) => { getStats().then(stats => { @@ -70,11 +71,11 @@ function isCrashLoop(MAX_CRASHES) { crashTimestamps.shift(); } - return crashTimestamps.length == MAX_CRASHES && Date.now() - crashTimestamps[0] < 60 * 1000; + return crashTimestamps.length == MAX_CRASHES && Date.now() - crashTimestamps[0] < CRASH_LOOP_WINDOW_SECONDS * 1000; } function isSecondCrash() { - return crashTimestamps.length > 1 && Date.now() - crashTimestamps[crashTimestamps.length - 2] < 60 * 1000; + return crashTimestamps.length > 1 && Date.now() - crashTimestamps[crashTimestamps.length - 2] < CRASH_LOOP_WINDOW_SECONDS * 1000; } function crashNotify(crashMsg, msg, { highPriority = true } = {}) { @@ -127,7 +128,7 @@ export default function init() { if (dmtProcCrashedInForegroundAt && dmtProcCrashedInForegroundAt < Date.now() - 3 * 60 * 1000) { if (!isMainDevice()) { const msg = `✨ Spawning a new dmt-proc after crash ${prettyTimeAge(dmtProcCrashedInForegroundAt)} while running in terminal foreground …`; - log.cyan(msg); + log.magenta(msg); push.notify(msg); startDMT(); @@ -225,7 +226,7 @@ export default function init() { crashTimestamps.push(Date.now()); if (isCrashLoop(MAX_CRASHES)) { - crashMsg = '⚠️😵‍💫💀 dmt-proc crash loop'; + crashMsg = `⚠️💀 dmt-proc Crash Loop 😵‍💫 (${MAX_CRASHES} crashes in ${CRASH_LOOP_WINDOW_SECONDS}s)`; log.red(crashMsg); const msg = '🤷‍♂️ Giving up on restarting dmt-proc, needs a bugfix and manual restart.'; log.cyan(msg); @@ -241,7 +242,7 @@ export default function init() { log.red(crashMsg); const msg = '✨ Spawning a new dmt-proc …'; - log.cyan(msg); + log.magenta(msg); setTimeout(() => { startDMT(); diff --git a/core/node/controller/processes/abc/startDMT.js b/core/node/controller/processes/abc/startDMT.js index 206c2f156..bdf4647e7 100644 --- a/core/node/controller/processes/abc/startDMT.js +++ b/core/node/controller/processes/abc/startDMT.js @@ -15,7 +15,7 @@ export default function startDMT(counter = 0) { return; } - const child = spawn(process.execPath, nodeFlags.concat([dmtProcManagerPath, 'start', 'dmt-proc.js']), { + const child = spawn(process.execPath, nodeFlags.concat([dmtProcManagerPath, 'start', 'dmt-proc.js', '--from_abc']), { cwd: daemonsPath, detached: true, stdio: 'ignore' diff --git a/core/node/controller/processes/dmt-proc.js b/core/node/controller/processes/dmt-proc.js index ed40d3a76..c964b1162 100644 --- a/core/node/controller/processes/dmt-proc.js +++ b/core/node/controller/processes/dmt-proc.js @@ -1,4 +1,4 @@ -import { log, device, isDevUser, dmtHerePath } from 'dmt/common'; +import { log, device, isDevUser } from 'dmt/common'; import program from '../program/program.js'; import exceptionNotify from '../program/exceptionNotify.js'; @@ -7,15 +7,22 @@ import getExitMsg from '../program/getExitMsg.js'; let foreground; let profiling; +let fromABC; -if (process.argv.length > 2 && process.argv[2] == '--fg') { +const args = process.argv.slice(2); + +if (args.length > 0 && args[0] == '--fg') { foreground = true; - if (process.argv.length > 3 && process.argv[3] == '--profile') { + if (args.length > 1 && args[1] == '--profile') { profiling = true; } } +if (args.length > 0 && args[0] == '--from_abc') { + fromABC = true; +} + const deviceName = device({ onlyBasicParsing: true }).id; const logfile = 'dmt.log'; @@ -51,7 +58,7 @@ mids.push('webindex'); mids.push('webscan'); try { - program({ mids }); + program({ mids, fromABC }); } catch (e) { const title = '⚠️ DMT BOOT ERROR ⚠️'; diff --git a/core/node/controller/processes/manager.js b/core/node/controller/processes/manager.js index 3f9bf4ba5..ddfa626a5 100644 --- a/core/node/controller/processes/manager.js +++ b/core/node/controller/processes/manager.js @@ -25,6 +25,11 @@ if (args.length < 2) { const proc = args[1]; const procName = proc.replace(new RegExp(/\.js$/, ''), ''); +let argsForDmtProc; +if (args.length > 2 && args[2] == '--from_abc') { + argsForDmtProc = args[2]; +} + if (!fs.existsSync(`${proc}`)) { console.log(`Missing ${proc} file`); usage(); @@ -44,6 +49,7 @@ const daemon = daemonize({ main: `${proc}`, name: `${procName}`, pidfile: pidFilePath, + argv: [argsForDmtProc], nodeFlags }); diff --git a/core/node/controller/program/connectionsAcceptor.js b/core/node/controller/program/connectionsAcceptor.js index 79d12ebbc..ce0bf15b0 100644 --- a/core/node/controller/program/connectionsAcceptor.js +++ b/core/node/controller/program/connectionsAcceptor.js @@ -1,6 +1,8 @@ import { Connectome } from 'dmt/connectome-server'; -import { log, colors, keypair, isDevMachine, isDevUser } from 'dmt/common'; +import { log, colors, keypair, isDevMachine } from 'dmt/common'; + +import connectomeLogging from './connectomeLogging.js'; class ProgramConnectionsAcceptor { constructor(program) { @@ -13,10 +15,13 @@ class ProgramConnectionsAcceptor { if (this.keypair) { log.write(`Initializing ProgramConnectionsAcceptor with public key ${colors.gray(this.keypair.publicKeyHex)}`); + const { verbose } = connectomeLogging().server; + this.connectome = new Connectome({ port, keypair: this.keypair, - log + log, + verbose }); this.connectome.subscribe(({ connectionList }) => { diff --git a/core/node/controller/program/connectomeLogging.js b/core/node/controller/program/connectomeLogging.js new file mode 100644 index 000000000..8560b9620 --- /dev/null +++ b/core/node/controller/program/connectomeLogging.js @@ -0,0 +1,12 @@ +import { isDevMachine, device, log } from 'dmt/common'; + +export default function connectomeLogging() { + const moreClientLogging = false; + const moreServerLogging = false; + + const fiberPoolLog = moreClientLogging ? log : console.log; + const verboseClient = moreClientLogging ? 'extra' : null; + const verboseServer = moreServerLogging ? 'extra' : null; + + return { client: { verbose: verboseClient, fiberPoolLog }, server: { verbose: verboseServer } }; +} diff --git a/core/node/controller/program/interval/onProgramTick.js b/core/node/controller/program/interval/onProgramTick.js index d9a0da1f2..f4cf441b6 100644 --- a/core/node/controller/program/interval/onProgramTick.js +++ b/core/node/controller/program/interval/onProgramTick.js @@ -1,4 +1,4 @@ -import { log, isDevMachine, isDevUser, isDevPanel, apMode, colors } from 'dmt/common'; +import { log, isDevMachine, isDevUser, isDevPanel, apMode, colors, keypair } from 'dmt/common'; import util from 'util'; @@ -6,7 +6,7 @@ import { connect } from 'dmt/connectome'; import determineIP from './determineIP.js'; -import determineWifiAP from './determineWifiAP.js'; +import connectomeLogging from '../connectomeLogging.js'; let lanConnector; @@ -52,7 +52,8 @@ export default function onTick(program) { } if (!lanConnector) { - lanConnector = connect({ host: primaryLanServer.ip, port, protocol: 'dmt', verbose: false, log: logger }); + const { verbose } = connectomeLogging().client; + lanConnector = connect({ host: primaryLanServer.ip, port, protocol: 'dmt', keypair: keypair(), log: logger, verbose }); lanConnector.on('inactive_connection', () => { log.cyan(`${statusTxt} Inactive connection ${lanConnector.remoteAddress()}`); diff --git a/core/node/controller/program/peerlist/createFiberPool.js b/core/node/controller/program/peerlist/createFiberPool.js index 050390e5d..95de7920d 100644 --- a/core/node/controller/program/peerlist/createFiberPool.js +++ b/core/node/controller/program/peerlist/createFiberPool.js @@ -2,15 +2,21 @@ import { log, keypair } from 'dmt/common'; import { ConnectorPool } from 'dmt/connectome'; -import { isDevUser, isDevMachine } from 'dmt/common'; +import { isDevUser, isDevMachine, program } from 'dmt/common'; + +import connectomeLogging from '../connectomeLogging.js'; const SEARCH_TIMEOUT = 50000; export default function createFiberPool({ port, protocol }) { + const { fiberPoolLog, verbose } = connectomeLogging().client; + return new ConnectorPool({ protocol, port, keypair: keypair(), - rpcRequestTimeout: SEARCH_TIMEOUT + rpcRequestTimeout: SEARCH_TIMEOUT, + log: fiberPoolLog, + verbose }); } diff --git a/core/node/controller/program/program.js b/core/node/controller/program/program.js index 07573cb7f..687c2897e 100644 --- a/core/node/controller/program/program.js +++ b/core/node/controller/program/program.js @@ -40,7 +40,7 @@ import ipcServerLegacy from './ipcServer/ipcServerLegacy.js'; import load from './load.js'; class Program extends EventEmitter { - constructor({ mids }) { + constructor({ mids, fromABC }) { super(); this.mqttHandlers = []; @@ -68,6 +68,14 @@ class Program extends EventEmitter { if (!dmt.isMainDevice()) { this.nearbyNotification({ msg: 'dmt-proc started', ttl: 10, color: '#50887E', dev: true }); } + + if (fromABC) { + setTimeout(() => { + log.yellow(`🛑 ${colors.cyan('dmt-proc')} started by ABC after crash`); + push.notify('✅ dmt-proc resumed but the cause for crash still has to be fixed'); + }, 2000); + } + this.sendCachedNearbyNotifications(); this.sendCachedMainDeviceNotifications(); }); diff --git a/core/node/gui/protocol/dmtGUI/index.js b/core/node/gui/protocol/dmtGUI/index.js index aaeea4b27..1cfb15d0c 100644 --- a/core/node/gui/protocol/dmtGUI/index.js +++ b/core/node/gui/protocol/dmtGUI/index.js @@ -69,6 +69,8 @@ export default function initProtocol({ program }) { if (action == 'reload') { loadGuiViewsDef(program); + + program.emit('gui:reload'); } channels.signalAll('frontend_action', { action, payload }); diff --git a/core/node/node_modules/.bin/node-gyp-build b/core/node/node_modules/.bin/node-gyp-build new file mode 120000 index 000000000..671c6ebce --- /dev/null +++ b/core/node/node_modules/.bin/node-gyp-build @@ -0,0 +1 @@ +../node-gyp-build/bin.js \ No newline at end of file diff --git a/core/node/node_modules/.bin/node-gyp-build-optional b/core/node/node_modules/.bin/node-gyp-build-optional new file mode 120000 index 000000000..46d347e6b --- /dev/null +++ b/core/node/node_modules/.bin/node-gyp-build-optional @@ -0,0 +1 @@ +../node-gyp-build/optional.js \ No newline at end of file diff --git a/core/node/node_modules/.bin/node-gyp-build-test b/core/node/node_modules/.bin/node-gyp-build-test new file mode 120000 index 000000000..d11de1bec --- /dev/null +++ b/core/node/node_modules/.bin/node-gyp-build-test @@ -0,0 +1 @@ +../node-gyp-build/build-test.js \ No newline at end of file diff --git a/core/node/node_modules/.package-lock.json b/core/node/node_modules/.package-lock.json index 354898ec0..fd6cc43bb 100644 --- a/core/node/node_modules/.package-lock.json +++ b/core/node/node_modules/.package-lock.json @@ -1,9 +1,34 @@ { "name": "dmt", "version": "0.0.1", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { + "connectome": { + "version": "0.2.9", + "extraneous": true, + "license": "ISC", + "dependencies": { + "browser-util-inspect": "^0.2.0", + "bufferutil": "^4.0.2", + "fast-json-patch": "^3.0.0-1", + "kleur": "^4.1.5", + "quantum-generator": "^1.9.1", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1", + "utf-8-validate": "^5.0.3", + "ws": "^8.13.0" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^16.0.0", + "@rollup/plugin-node-resolve": "^10.0.0", + "builtin-modules": "^3.1.0", + "rollup": "^2.33.3" + } + }, + "connectome-next": { + "extraneous": true + }, "node_modules/@types/http-proxy": { "version": "1.17.9", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", @@ -299,6 +324,20 @@ "node": ">=0.2.0" } }, + "node_modules/bufferutil": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", + "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -1734,14 +1773,6 @@ "node": ">= 6" } }, - "node_modules/mqtt/node_modules/ws": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", - "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==", - "engines": { - "node": ">=8.3.0" - } - }, "node_modules/ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -1826,6 +1857,18 @@ "node": "4.x || >=6.0.0" } }, + "node_modules/node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "optional": true, + "peer": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-ipc": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.1.1.tgz", @@ -2776,6 +2819,20 @@ "iconv-lite": "~0.4.11" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2885,6 +2942,26 @@ "typedarray-to-buffer": "^3.1.5" } }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wtfnode": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.9.1.tgz", diff --git a/core/node/node_modules/bufferutil/LICENSE b/core/node/node_modules/bufferutil/LICENSE new file mode 100644 index 000000000..541fa4134 --- /dev/null +++ b/core/node/node_modules/bufferutil/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011 Einar Otto Stangvik (http://2x.io) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/core/node/node_modules/bufferutil/README.md b/core/node/node_modules/bufferutil/README.md new file mode 100644 index 000000000..36a450339 --- /dev/null +++ b/core/node/node_modules/bufferutil/README.md @@ -0,0 +1,78 @@ +# bufferutil + +[![Version npm](https://img.shields.io/npm/v/bufferutil.svg?logo=npm)](https://www.npmjs.com/package/bufferutil) +[![Linux/macOS/Windows Build](https://img.shields.io/github/workflow/status/websockets/bufferutil/CI/master?label=build&logo=github)](https://github.com/websockets/bufferutil/actions?query=workflow%3ACI+branch%3Amaster) + +`bufferutil` is what makes `ws` fast. It provides some utilities to efficiently +perform some operations such as masking and unmasking the data payload of +WebSocket frames. + +## Installation + +``` +npm install bufferutil --save-optional +``` + +The `--save-optional` flag tells npm to save the package in your package.json +under the +[`optionalDependencies`](https://docs.npmjs.com/files/package.json#optionaldependencies) +key. + +## API + +The module exports two functions. + +### `bufferUtil.mask(source, mask, output, offset, length)` + +Masks a buffer using the given masking-key as specified by the WebSocket +protocol. + +#### Arguments + +- `source` - The buffer to mask. +- `mask` - A buffer representing the masking-key. +- `output` - The buffer where to store the result. +- `offset` - The offset at which to start writing. +- `length` - The number of bytes to mask. + +#### Example + +```js +'use strict'; + +const bufferUtil = require('bufferutil'); +const crypto = require('crypto'); + +const source = crypto.randomBytes(10); +const mask = crypto.randomBytes(4); + +bufferUtil.mask(source, mask, source, 0, source.length); +``` + +### `bufferUtil.unmask(buffer, mask)` + +Unmasks a buffer using the given masking-key as specified by the WebSocket +protocol. + +#### Arguments + +- `buffer` - The buffer to unmask. +- `mask` - A buffer representing the masking-key. + +#### Example + +```js +'use strict'; + +const bufferUtil = require('bufferutil'); +const crypto = require('crypto'); + +const buffer = crypto.randomBytes(10); +const mask = crypto.randomBytes(4); + +bufferUtil.unmask(buffer, mask); +``` + +## License + +[MIT](LICENSE) diff --git a/core/node/node_modules/bufferutil/binding.gyp b/core/node/node_modules/bufferutil/binding.gyp new file mode 100644 index 000000000..1e97c0cf9 --- /dev/null +++ b/core/node/node_modules/bufferutil/binding.gyp @@ -0,0 +1,18 @@ +{ + 'targets': [ + { + 'target_name': 'bufferutil', + 'sources': ['src/bufferutil.c'], + 'cflags': ['-std=c99'], + 'conditions': [ + ["OS=='mac'", { + 'xcode_settings': { + 'MACOSX_DEPLOYMENT_TARGET': '10.7', + 'OTHER_CFLAGS': ['-arch x86_64', '-arch arm64'], + 'OTHER_LDFLAGS': ['-arch x86_64', '-arch arm64'] + } + }] + ] + } + ] +} diff --git a/core/node/node_modules/bufferutil/fallback.js b/core/node/node_modules/bufferutil/fallback.js new file mode 100644 index 000000000..d28b9e306 --- /dev/null +++ b/core/node/node_modules/bufferutil/fallback.js @@ -0,0 +1,34 @@ +'use strict'; + +/** + * Masks a buffer using the given mask. + * + * @param {Buffer} source The buffer to mask + * @param {Buffer} mask The mask to use + * @param {Buffer} output The buffer where to store the result + * @param {Number} offset The offset at which to start writing + * @param {Number} length The number of bytes to mask. + * @public + */ +const mask = (source, mask, output, offset, length) => { + for (var i = 0; i < length; i++) { + output[offset + i] = source[i] ^ mask[i & 3]; + } +}; + +/** + * Unmasks a buffer using the given mask. + * + * @param {Buffer} buffer The buffer to unmask + * @param {Buffer} mask The mask to use + * @public + */ +const unmask = (buffer, mask) => { + // Required until https://github.com/nodejs/node/issues/9006 is resolved. + const length = buffer.length; + for (var i = 0; i < length; i++) { + buffer[i] ^= mask[i & 3]; + } +}; + +module.exports = { mask, unmask }; diff --git a/core/node/node_modules/bufferutil/index.js b/core/node/node_modules/bufferutil/index.js new file mode 100644 index 000000000..8c30561ae --- /dev/null +++ b/core/node/node_modules/bufferutil/index.js @@ -0,0 +1,7 @@ +'use strict'; + +try { + module.exports = require('node-gyp-build')(__dirname); +} catch (e) { + module.exports = require('./fallback'); +} diff --git a/core/node/node_modules/bufferutil/package.json b/core/node/node_modules/bufferutil/package.json new file mode 100644 index 000000000..4c33e67c4 --- /dev/null +++ b/core/node/node_modules/bufferutil/package.json @@ -0,0 +1,36 @@ +{ + "name": "bufferutil", + "version": "4.0.7", + "description": "WebSocket buffer utils", + "main": "index.js", + "engines": { + "node": ">=6.14.2" + }, + "scripts": { + "install": "node-gyp-build", + "prebuild": "prebuildify --napi --strip --target=14.0.0", + "prebuild-darwin-x64+arm64": "prebuildify --arch x64+arm64 --napi --strip --target=14.0.0", + "test": "mocha" + }, + "repository": { + "type": "git", + "url": "https://github.com/websockets/bufferutil" + }, + "keywords": [ + "bufferutil" + ], + "author": "Einar Otto Stangvik (http://2x.io)", + "license": "MIT", + "bugs": { + "url": "https://github.com/websockets/bufferutil/issues" + }, + "homepage": "https://github.com/websockets/bufferutil", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "devDependencies": { + "mocha": "^10.0.0", + "node-gyp": "^9.1.0", + "prebuildify": "^5.0.0" + } +} diff --git a/core/node/node_modules/bufferutil/prebuilds/darwin-x64+arm64/node.napi.node b/core/node/node_modules/bufferutil/prebuilds/darwin-x64+arm64/node.napi.node new file mode 100644 index 0000000000000000000000000000000000000000..824132dd587bd2465496935f5c4c8a152d0dc03b GIT binary patch literal 116128 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 bSJt{DVn0N=Zjy6(*R8HzV}Yh@$KwA0L}eH@ literal 0 HcmV?d00001 diff --git a/core/node/node_modules/bufferutil/prebuilds/win32-ia32/node.napi.node b/core/node/node_modules/bufferutil/prebuilds/win32-ia32/node.napi.node new file mode 100644 index 0000000000000000000000000000000000000000..aea0bf30acf4fcf3ce3d00b2ad249ae09b5b0784 GIT binary patch literal 122368 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 F{{`Kd=}`ax literal 0 HcmV?d00001 diff --git a/core/node/node_modules/bufferutil/prebuilds/win32-x64/node.napi.node b/core/node/node_modules/bufferutil/prebuilds/win32-x64/node.napi.node new file mode 100644 index 0000000000000000000000000000000000000000..dd47faa460f45a4b557073f61496a44b5881aeb3 GIT binary patch literal 151552 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 q+qhT +#include + +napi_value Mask(napi_env env, napi_callback_info info) { + napi_status status; + size_t argc = 5; + napi_value argv[5]; + + status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + assert(status == napi_ok); + + uint8_t *source; + uint8_t *mask; + uint8_t *destination; + uint32_t offset; + uint32_t length; + + status = napi_get_buffer_info(env, argv[0], (void **)&source, NULL); + assert(status == napi_ok); + + status = napi_get_buffer_info(env, argv[1], (void **)&mask, NULL); + assert(status == napi_ok); + + status = napi_get_buffer_info(env, argv[2], (void **)&destination, NULL); + assert(status == napi_ok); + + status = napi_get_value_uint32(env, argv[3], &offset); + assert(status == napi_ok); + + status = napi_get_value_uint32(env, argv[4], &length); + assert(status == napi_ok); + + destination += offset; + uint32_t index = 0; + + // + // Alignment preamble. + // + while (index < length && ((size_t)source % 8)) { + *destination++ = *source++ ^ mask[index % 4]; + index++; + } + + length -= index; + if (!length) + return NULL; + + // + // Realign mask and convert to 64 bit. + // + uint8_t maskAlignedArray[8]; + + for (uint8_t i = 0; i < 8; i++, index++) { + maskAlignedArray[i] = mask[index % 4]; + } + + // + // Apply 64 bit mask in 8 byte chunks. + // + uint32_t loop = length / 8; + uint64_t *pMask8 = (uint64_t *)maskAlignedArray; + + while (loop--) { + uint64_t *pFrom8 = (uint64_t *)source; + uint64_t *pTo8 = (uint64_t *)destination; + *pTo8 = *pFrom8 ^ *pMask8; + source += 8; + destination += 8; + } + + // + // Apply mask to remaining data. + // + uint8_t *pmaskAlignedArray = maskAlignedArray; + + length %= 8; + while (length--) { + *destination++ = *source++ ^ *pmaskAlignedArray++; + } + + return NULL; +} + +napi_value Unmask(napi_env env, napi_callback_info info) { + napi_status status; + size_t argc = 2; + napi_value argv[2]; + + status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + assert(status == napi_ok); + + uint8_t *source; + size_t length; + uint8_t *mask; + + status = napi_get_buffer_info(env, argv[0], (void **)&source, &length); + assert(status == napi_ok); + + status = napi_get_buffer_info(env, argv[1], (void **)&mask, NULL); + assert(status == napi_ok); + + uint32_t index = 0; + + // + // Alignment preamble. + // + while (index < length && ((size_t)source % 8)) { + *source++ ^= mask[index % 4]; + index++; + } + + length -= index; + if (!length) + return NULL; + + // + // Realign mask and convert to 64 bit. + // + uint8_t maskAlignedArray[8]; + + for (uint8_t i = 0; i < 8; i++, index++) { + maskAlignedArray[i] = mask[index % 4]; + } + + // + // Apply 64 bit mask in 8 byte chunks. + // + uint32_t loop = length / 8; + uint64_t *pMask8 = (uint64_t *)maskAlignedArray; + + while (loop--) { + uint64_t *pSource8 = (uint64_t *)source; + *pSource8 ^= *pMask8; + source += 8; + } + + // + // Apply mask to remaining data. + // + uint8_t *pmaskAlignedArray = maskAlignedArray; + + length %= 8; + while (length--) { + *source++ ^= *pmaskAlignedArray++; + } + + return NULL; +} + +napi_value Init(napi_env env, napi_value exports) { + napi_status status; + napi_value mask; + napi_value unmask; + + status = napi_create_function(env, NULL, 0, Mask, NULL, &mask); + assert(status == napi_ok); + + status = napi_create_function(env, NULL, 0, Unmask, NULL, &unmask); + assert(status == napi_ok); + + status = napi_set_named_property(env, exports, "mask", mask); + assert(status == napi_ok); + + status = napi_set_named_property(env, exports, "unmask", unmask); + assert(status == napi_ok); + + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/core/node/node_modules/mqtt/node_modules/ws/lib/validation.js b/core/node/node_modules/mqtt/node_modules/ws/lib/validation.js deleted file mode 100644 index 32db5a570..000000000 --- a/core/node/node_modules/mqtt/node_modules/ws/lib/validation.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -try { - const isValidUTF8 = require('utf-8-validate'); - - exports.isValidUTF8 = - typeof isValidUTF8 === 'object' - ? isValidUTF8.Validation.isValidUTF8 // utf-8-validate@<3.0.0 - : isValidUTF8; -} catch (e) /* istanbul ignore next */ { - exports.isValidUTF8 = () => true; -} - -/** - * Checks if a status code is allowed in a close frame. - * - * @param {Number} code The status code - * @return {Boolean} `true` if the status code is valid, else `false` - * @public - */ -exports.isValidStatusCode = (code) => { - return ( - (code >= 1000 && - code <= 1014 && - code !== 1004 && - code !== 1005 && - code !== 1006) || - (code >= 3000 && code <= 4999) - ); -}; diff --git a/core/node/node_modules/mqtt/node_modules/ws/package.json b/core/node/node_modules/mqtt/node_modules/ws/package.json deleted file mode 100644 index d6935bea7..000000000 --- a/core/node/node_modules/mqtt/node_modules/ws/package.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "_args": [ - [ - "ws@7.4.2", - "/Users/david/.dmt/core/node" - ] - ], - "_from": "ws@7.4.2", - "_id": "ws@7.4.2", - "_inBundle": false, - "_integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==", - "_location": "/mqtt/ws", - "_phantomChildren": {}, - "_requested": { - "type": "version", - "registry": true, - "raw": "ws@7.4.2", - "name": "ws", - "escapedName": "ws", - "rawSpec": "7.4.2", - "saveSpec": null, - "fetchSpec": "7.4.2" - }, - "_requiredBy": [ - "/mqtt" - ], - "_resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", - "_spec": "7.4.2", - "_where": "/Users/david/.dmt/core/node", - "author": { - "name": "Einar Otto Stangvik", - "email": "einaros@gmail.com", - "url": "http://2x.io" - }, - "browser": "browser.js", - "bugs": { - "url": "https://github.com/websockets/ws/issues" - }, - "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", - "devDependencies": { - "benchmark": "^2.1.4", - "bufferutil": "^4.0.1", - "coveralls": "^3.0.3", - "eslint": "^7.2.0", - "eslint-config-prettier": "^7.1.0", - "eslint-plugin-prettier": "^3.0.1", - "mocha": "^7.0.0", - "nyc": "^15.0.0", - "prettier": "^2.0.5", - "utf-8-validate": "^5.0.2" - }, - "engines": { - "node": ">=8.3.0" - }, - "files": [ - "browser.js", - "index.js", - "lib/*.js" - ], - "homepage": "https://github.com/websockets/ws", - "keywords": [ - "HyBi", - "Push", - "RFC-6455", - "WebSocket", - "WebSockets", - "real-time" - ], - "license": "MIT", - "main": "index.js", - "name": "ws", - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - }, - "repository": { - "type": "git", - "url": "git+https://github.com/websockets/ws.git" - }, - "scripts": { - "integration": "mocha --throw-deprecation test/*.integration.js", - "lint": "eslint --ignore-path .gitignore . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\"", - "test": "nyc --reporter=html --reporter=text mocha --throw-deprecation test/*.test.js" - }, - "version": "7.4.2" -} diff --git a/core/node/node_modules/node-gyp-build/LICENSE b/core/node/node_modules/node-gyp-build/LICENSE new file mode 100644 index 000000000..56fce0895 --- /dev/null +++ b/core/node/node_modules/node-gyp-build/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/core/node/node_modules/node-gyp-build/README.md b/core/node/node_modules/node-gyp-build/README.md new file mode 100644 index 000000000..f712ca686 --- /dev/null +++ b/core/node/node_modules/node-gyp-build/README.md @@ -0,0 +1,58 @@ +# node-gyp-build + +> Build tool and bindings loader for [`node-gyp`][node-gyp] that supports prebuilds. + +``` +npm install node-gyp-build +``` + +[![Test](https://github.com/prebuild/node-gyp-build/actions/workflows/test.yml/badge.svg)](https://github.com/prebuild/node-gyp-build/actions/workflows/test.yml) + +Use together with [`prebuildify`][prebuildify] to easily support prebuilds for your native modules. + +## Usage + +> **Note.** Prebuild names have changed in [`prebuildify@3`][prebuildify] and `node-gyp-build@4`. Please see the documentation below. + +`node-gyp-build` works similar to [`node-gyp build`][node-gyp] except that it will check if a build or prebuild is present before rebuilding your project. + +It's main intended use is as an npm install script and bindings loader for native modules that bundle prebuilds using [`prebuildify`][prebuildify]. + +First add `node-gyp-build` as an install script to your native project + +``` js +{ + ... + "scripts": { + "install": "node-gyp-build" + } +} +``` + +Then in your `index.js`, instead of using the [`bindings`](https://www.npmjs.com/package/bindings) module use `node-gyp-build` to load your binding. + +``` js +var binding = require('node-gyp-build')(__dirname) +``` + +If you do these two things and bundle prebuilds with [`prebuildify`][prebuildify] your native module will work for most platforms +without having to compile on install time AND will work in both node and electron without the need to recompile between usage. + +Users can override `node-gyp-build` and force compiling by doing `npm install --build-from-source`. + +Prebuilds will be attempted loaded from `MODULE_PATH/prebuilds/...` and then next `EXEC_PATH/prebuilds/...` (the latter allowing use with `zeit/pkg`) + +## Supported prebuild names + +If so desired you can bundle more specific flavors, for example `musl` builds to support Alpine, or targeting a numbered ARM architecture version. + +These prebuilds can be bundled in addition to generic prebuilds; `node-gyp-build` will try to find the most specific flavor first. Prebuild filenames are composed of _tags_. The runtime tag takes precedence, as does an `abi` tag over `napi`. For more details on tags, please see [`prebuildify`][prebuildify]. + +Values for the `libc` and `armv` tags are auto-detected but can be overridden through the `LIBC` and `ARM_VERSION` environment variables, respectively. + +## License + +MIT + +[prebuildify]: https://github.com/prebuild/prebuildify +[node-gyp]: https://www.npmjs.com/package/node-gyp diff --git a/core/node/node_modules/node-gyp-build/bin.js b/core/node/node_modules/node-gyp-build/bin.js new file mode 100755 index 000000000..36bd51544 --- /dev/null +++ b/core/node/node_modules/node-gyp-build/bin.js @@ -0,0 +1,77 @@ +#!/usr/bin/env node + +var proc = require('child_process') +var os = require('os') +var path = require('path') + +if (!buildFromSource()) { + proc.exec('node-gyp-build-test', function (err, stdout, stderr) { + if (err) { + if (verbose()) console.error(stderr) + preinstall() + } + }) +} else { + preinstall() +} + +function build () { + var args = [os.platform() === 'win32' ? 'node-gyp.cmd' : 'node-gyp', 'rebuild'] + + try { + args = [ + process.execPath, + path.join(require.resolve('node-gyp/package.json'), '..', require('node-gyp/package.json').bin['node-gyp']), + 'rebuild' + ] + } catch (_) {} + + proc.spawn(args[0], args.slice(1), { stdio: 'inherit' }).on('exit', function (code) { + if (code || !process.argv[3]) process.exit(code) + exec(process.argv[3]).on('exit', function (code) { + process.exit(code) + }) + }) +} + +function preinstall () { + if (!process.argv[2]) return build() + exec(process.argv[2]).on('exit', function (code) { + if (code) process.exit(code) + build() + }) +} + +function exec (cmd) { + if (process.platform !== 'win32') { + var shell = os.platform() === 'android' ? 'sh' : '/bin/sh' + return proc.spawn(shell, ['-c', '--', cmd], { + stdio: 'inherit' + }) + } + + return proc.spawn(process.env.comspec || 'cmd.exe', ['/s', '/c', '"' + cmd + '"'], { + windowsVerbatimArguments: true, + stdio: 'inherit' + }) +} + +function buildFromSource () { + return hasFlag('--build-from-source') || process.env.npm_config_build_from_source === 'true' +} + +function verbose () { + return hasFlag('--verbose') || process.env.npm_config_loglevel === 'verbose' +} + +// TODO (next major): remove in favor of env.npm_config_* which works since npm +// 0.1.8 while npm_config_argv will stop working in npm 7. See npm/rfcs#90 +function hasFlag (flag) { + if (!process.env.npm_config_argv) return false + + try { + return JSON.parse(process.env.npm_config_argv).original.indexOf(flag) !== -1 + } catch (_) { + return false + } +} diff --git a/core/node/node_modules/node-gyp-build/build-test.js b/core/node/node_modules/node-gyp-build/build-test.js new file mode 100755 index 000000000..b6622a5c2 --- /dev/null +++ b/core/node/node_modules/node-gyp-build/build-test.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +process.env.NODE_ENV = 'test' + +var path = require('path') +var test = null + +try { + var pkg = require(path.join(process.cwd(), 'package.json')) + if (pkg.name && process.env[pkg.name.toUpperCase().replace(/-/g, '_')]) { + process.exit(0) + } + test = pkg.prebuild.test +} catch (err) { + // do nothing +} + +if (test) require(path.join(process.cwd(), test)) +else require('./')() diff --git a/core/node/node_modules/node-gyp-build/index.js b/core/node/node_modules/node-gyp-build/index.js new file mode 100644 index 000000000..d065ae66f --- /dev/null +++ b/core/node/node_modules/node-gyp-build/index.js @@ -0,0 +1,5 @@ +if (typeof process.addon === 'function') { // if the platform supports native resolving prefer that + module.exports = process.addon.bind(process) +} else { // else use the runtime version here + module.exports = require('./node-gyp-build.js') +} diff --git a/core/node/node_modules/node-gyp-build/node-gyp-build.js b/core/node/node_modules/node-gyp-build/node-gyp-build.js new file mode 100644 index 000000000..61b398efc --- /dev/null +++ b/core/node/node_modules/node-gyp-build/node-gyp-build.js @@ -0,0 +1,207 @@ +var fs = require('fs') +var path = require('path') +var os = require('os') + +// Workaround to fix webpack's build warnings: 'the request of a dependency is an expression' +var runtimeRequire = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require // eslint-disable-line + +var vars = (process.config && process.config.variables) || {} +var prebuildsOnly = !!process.env.PREBUILDS_ONLY +var abi = process.versions.modules // TODO: support old node where this is undef +var runtime = isElectron() ? 'electron' : (isNwjs() ? 'node-webkit' : 'node') + +var arch = process.env.npm_config_arch || os.arch() +var platform = process.env.npm_config_platform || os.platform() +var libc = process.env.LIBC || (isAlpine(platform) ? 'musl' : 'glibc') +var armv = process.env.ARM_VERSION || (arch === 'arm64' ? '8' : vars.arm_version) || '' +var uv = (process.versions.uv || '').split('.')[0] + +module.exports = load + +function load (dir) { + return runtimeRequire(load.resolve(dir)) +} + +load.resolve = load.path = function (dir) { + dir = path.resolve(dir || '.') + + try { + var name = runtimeRequire(path.join(dir, 'package.json')).name.toUpperCase().replace(/-/g, '_') + if (process.env[name + '_PREBUILD']) dir = process.env[name + '_PREBUILD'] + } catch (err) {} + + if (!prebuildsOnly) { + var release = getFirst(path.join(dir, 'build/Release'), matchBuild) + if (release) return release + + var debug = getFirst(path.join(dir, 'build/Debug'), matchBuild) + if (debug) return debug + } + + var prebuild = resolve(dir) + if (prebuild) return prebuild + + var nearby = resolve(path.dirname(process.execPath)) + if (nearby) return nearby + + var target = [ + 'platform=' + platform, + 'arch=' + arch, + 'runtime=' + runtime, + 'abi=' + abi, + 'uv=' + uv, + armv ? 'armv=' + armv : '', + 'libc=' + libc, + 'node=' + process.versions.node, + process.versions.electron ? 'electron=' + process.versions.electron : '', + typeof __webpack_require__ === 'function' ? 'webpack=true' : '' // eslint-disable-line + ].filter(Boolean).join(' ') + + throw new Error('No native build was found for ' + target + '\n loaded from: ' + dir + '\n') + + function resolve (dir) { + // Find matching "prebuilds/-" directory + var tuples = readdirSync(path.join(dir, 'prebuilds')).map(parseTuple) + var tuple = tuples.filter(matchTuple(platform, arch)).sort(compareTuples)[0] + if (!tuple) return + + // Find most specific flavor first + var prebuilds = path.join(dir, 'prebuilds', tuple.name) + var parsed = readdirSync(prebuilds).map(parseTags) + var candidates = parsed.filter(matchTags(runtime, abi)) + var winner = candidates.sort(compareTags(runtime))[0] + if (winner) return path.join(prebuilds, winner.file) + } +} + +function readdirSync (dir) { + try { + return fs.readdirSync(dir) + } catch (err) { + return [] + } +} + +function getFirst (dir, filter) { + var files = readdirSync(dir).filter(filter) + return files[0] && path.join(dir, files[0]) +} + +function matchBuild (name) { + return /\.node$/.test(name) +} + +function parseTuple (name) { + // Example: darwin-x64+arm64 + var arr = name.split('-') + if (arr.length !== 2) return + + var platform = arr[0] + var architectures = arr[1].split('+') + + if (!platform) return + if (!architectures.length) return + if (!architectures.every(Boolean)) return + + return { name, platform, architectures } +} + +function matchTuple (platform, arch) { + return function (tuple) { + if (tuple == null) return false + if (tuple.platform !== platform) return false + return tuple.architectures.includes(arch) + } +} + +function compareTuples (a, b) { + // Prefer single-arch prebuilds over multi-arch + return a.architectures.length - b.architectures.length +} + +function parseTags (file) { + var arr = file.split('.') + var extension = arr.pop() + var tags = { file: file, specificity: 0 } + + if (extension !== 'node') return + + for (var i = 0; i < arr.length; i++) { + var tag = arr[i] + + if (tag === 'node' || tag === 'electron' || tag === 'node-webkit') { + tags.runtime = tag + } else if (tag === 'napi') { + tags.napi = true + } else if (tag.slice(0, 3) === 'abi') { + tags.abi = tag.slice(3) + } else if (tag.slice(0, 2) === 'uv') { + tags.uv = tag.slice(2) + } else if (tag.slice(0, 4) === 'armv') { + tags.armv = tag.slice(4) + } else if (tag === 'glibc' || tag === 'musl') { + tags.libc = tag + } else { + continue + } + + tags.specificity++ + } + + return tags +} + +function matchTags (runtime, abi) { + return function (tags) { + if (tags == null) return false + if (tags.runtime !== runtime && !runtimeAgnostic(tags)) return false + if (tags.abi !== abi && !tags.napi) return false + if (tags.uv && tags.uv !== uv) return false + if (tags.armv && tags.armv !== armv) return false + if (tags.libc && tags.libc !== libc) return false + + return true + } +} + +function runtimeAgnostic (tags) { + return tags.runtime === 'node' && tags.napi +} + +function compareTags (runtime) { + // Precedence: non-agnostic runtime, abi over napi, then by specificity. + return function (a, b) { + if (a.runtime !== b.runtime) { + return a.runtime === runtime ? -1 : 1 + } else if (a.abi !== b.abi) { + return a.abi ? -1 : 1 + } else if (a.specificity !== b.specificity) { + return a.specificity > b.specificity ? -1 : 1 + } else { + return 0 + } + } +} + +function isNwjs () { + return !!(process.versions && process.versions.nw) +} + +function isElectron () { + if (process.versions && process.versions.electron) return true + if (process.env.ELECTRON_RUN_AS_NODE) return true + return typeof window !== 'undefined' && window.process && window.process.type === 'renderer' +} + +function isAlpine (platform) { + return platform === 'linux' && fs.existsSync('/etc/alpine-release') +} + +// Exposed for unit tests +// TODO: move to lib +load.parseTags = parseTags +load.matchTags = matchTags +load.compareTags = compareTags +load.parseTuple = parseTuple +load.matchTuple = matchTuple +load.compareTuples = compareTuples diff --git a/core/node/node_modules/node-gyp-build/optional.js b/core/node/node_modules/node-gyp-build/optional.js new file mode 100755 index 000000000..8daa04a6f --- /dev/null +++ b/core/node/node_modules/node-gyp-build/optional.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +/* +I am only useful as an install script to make node-gyp not compile for purely optional native deps +*/ + +process.exit(0) diff --git a/core/node/node_modules/node-gyp-build/package.json b/core/node/node_modules/node-gyp-build/package.json new file mode 100644 index 000000000..bc2f53af9 --- /dev/null +++ b/core/node/node_modules/node-gyp-build/package.json @@ -0,0 +1,29 @@ +{ + "name": "node-gyp-build", + "version": "4.6.0", + "description": "Build tool and bindings loader for node-gyp that supports prebuilds", + "main": "index.js", + "devDependencies": { + "array-shuffle": "^1.0.1", + "standard": "^14.0.0", + "tape": "^5.0.0" + }, + "scripts": { + "test": "standard && node test" + }, + "bin": { + "node-gyp-build": "./bin.js", + "node-gyp-build-optional": "./optional.js", + "node-gyp-build-test": "./build-test.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/prebuild/node-gyp-build.git" + }, + "author": "Mathias Buus (@mafintosh)", + "license": "MIT", + "bugs": { + "url": "https://github.com/prebuild/node-gyp-build/issues" + }, + "homepage": "https://github.com/prebuild/node-gyp-build" +} diff --git a/core/node/node_modules/utf-8-validate/LICENSE b/core/node/node_modules/utf-8-validate/LICENSE new file mode 100644 index 000000000..710d09fc1 --- /dev/null +++ b/core/node/node_modules/utf-8-validate/LICENSE @@ -0,0 +1,30 @@ +This project is licensed for use as follows: + +""" +Copyright (c) 2011 Einar Otto Stangvik (http://2x.io) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +This license applies to parts originating from +https://www.cl.cam.ac.uk/~mgk25/ucs/utf8_check.c: + +""" +Markus Kuhn -- 2005-03-30 +License: http://www.cl.cam.ac.uk/~mgk25/short-license.html +""" diff --git a/core/node/node_modules/utf-8-validate/README.md b/core/node/node_modules/utf-8-validate/README.md new file mode 100644 index 000000000..3a95ff8da --- /dev/null +++ b/core/node/node_modules/utf-8-validate/README.md @@ -0,0 +1,50 @@ +# utf-8-validate + +[![Version npm](https://img.shields.io/npm/v/utf-8-validate.svg?logo=npm)](https://www.npmjs.com/package/utf-8-validate) +[![Linux/macOS/Windows Build](https://img.shields.io/github/workflow/status/websockets/utf-8-validate/CI/master?label=build&logo=github)](https://github.com/websockets/utf-8-validate/actions?query=workflow%3ACI+branch%3Amaster) + +Check if a buffer contains valid UTF-8 encoded text. + +## Installation + +``` +npm install utf-8-validate --save-optional +``` + +The `--save-optional` flag tells npm to save the package in your package.json +under the +[`optionalDependencies`](https://docs.npmjs.com/files/package.json#optionaldependencies) +key. + +## API + +The module exports a single function which takes one argument. + +### `isValidUTF8(buffer)` + +Checks whether a buffer contains valid UTF-8. + +#### Arguments + +- `buffer` - The buffer to check. + +#### Return value + +`true` if the buffer contains only correct UTF-8, else `false`. + +#### Example + +```js +'use strict'; + +const isValidUTF8 = require('utf-8-validate'); + +const buf = Buffer.from([0xf0, 0x90, 0x80, 0x80]); + +console.log(isValidUTF8(buf)); +// => true +``` + +## License + +[MIT](LICENSE) diff --git a/core/node/node_modules/utf-8-validate/binding.gyp b/core/node/node_modules/utf-8-validate/binding.gyp new file mode 100644 index 000000000..30edf2742 --- /dev/null +++ b/core/node/node_modules/utf-8-validate/binding.gyp @@ -0,0 +1,18 @@ +{ + 'targets': [ + { + 'target_name': 'validation', + 'sources': ['src/validation.c'], + 'cflags': ['-std=c99'], + 'conditions': [ + ["OS=='mac'", { + 'xcode_settings': { + 'MACOSX_DEPLOYMENT_TARGET': '10.7', + 'OTHER_CFLAGS': ['-arch x86_64', '-arch arm64'], + 'OTHER_LDFLAGS': ['-arch x86_64', '-arch arm64'] + } + }] + ] + } + ] +} diff --git a/core/node/node_modules/utf-8-validate/fallback.js b/core/node/node_modules/utf-8-validate/fallback.js new file mode 100644 index 000000000..c493d491d --- /dev/null +++ b/core/node/node_modules/utf-8-validate/fallback.js @@ -0,0 +1,62 @@ +'use strict'; + +/** + * Checks if a given buffer contains only correct UTF-8. + * Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by + * Markus Kuhn. + * + * @param {Buffer} buf The buffer to check + * @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false` + * @public + */ +function isValidUTF8(buf) { + const len = buf.length; + let i = 0; + + while (i < len) { + if ((buf[i] & 0x80) === 0x00) { // 0xxxxxxx + i++; + } else if ((buf[i] & 0xe0) === 0xc0) { // 110xxxxx 10xxxxxx + if ( + i + 1 === len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i] & 0xfe) === 0xc0 // overlong + ) { + return false; + } + + i += 2; + } else if ((buf[i] & 0xf0) === 0xe0) { // 1110xxxx 10xxxxxx 10xxxxxx + if ( + i + 2 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80 || // overlong + buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0 // surrogate (U+D800 - U+DFFF) + ) { + return false; + } + + i += 3; + } else if ((buf[i] & 0xf8) === 0xf0) { // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if ( + i + 3 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i + 3] & 0xc0) !== 0x80 || + buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80 || // overlong + buf[i] === 0xf4 && buf[i + 1] > 0x8f || buf[i] > 0xf4 // > U+10FFFF + ) { + return false; + } + + i += 4; + } else { + return false; + } + } + + return true; +} + +module.exports = isValidUTF8; diff --git a/core/node/node_modules/utf-8-validate/index.js b/core/node/node_modules/utf-8-validate/index.js new file mode 100644 index 000000000..8c30561ae --- /dev/null +++ b/core/node/node_modules/utf-8-validate/index.js @@ -0,0 +1,7 @@ +'use strict'; + +try { + module.exports = require('node-gyp-build')(__dirname); +} catch (e) { + module.exports = require('./fallback'); +} diff --git a/core/node/node_modules/utf-8-validate/package.json b/core/node/node_modules/utf-8-validate/package.json new file mode 100644 index 000000000..149e65233 --- /dev/null +++ b/core/node/node_modules/utf-8-validate/package.json @@ -0,0 +1,36 @@ +{ + "name": "utf-8-validate", + "version": "5.0.10", + "description": "Check if a buffer contains valid UTF-8", + "main": "index.js", + "engines": { + "node": ">=6.14.2" + }, + "scripts": { + "install": "node-gyp-build", + "prebuild": "prebuildify --napi --strip --target=14.0.0", + "prebuild-darwin-x64+arm64": "prebuildify --arch x64+arm64 --napi --strip --target=14.0.0", + "test": "mocha" + }, + "repository": { + "type": "git", + "url": "https://github.com/websockets/utf-8-validate" + }, + "keywords": [ + "utf-8-validate" + ], + "author": "Einar Otto Stangvik (http://2x.io)", + "license": "MIT", + "bugs": { + "url": "https://github.com/websockets/utf-8-validate/issues" + }, + "homepage": "https://github.com/websockets/utf-8-validate", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "devDependencies": { + "mocha": "^10.0.0", + "node-gyp": "^9.1.0", + "prebuildify": "^5.0.0" + } +} diff --git a/core/node/node_modules/utf-8-validate/prebuilds/darwin-x64+arm64/node.napi.node b/core/node/node_modules/utf-8-validate/prebuilds/darwin-x64+arm64/node.napi.node new file mode 100644 index 0000000000000000000000000000000000000000..bed98d4a43a56135c659459c59f7e74e0b9ac269 GIT binary patch literal 116000 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 literal 0 HcmV?d00001 diff --git a/core/node/node_modules/utf-8-validate/prebuilds/linux-x64/node.napi.node b/core/node/node_modules/utf-8-validate/prebuilds/linux-x64/node.napi.node new file mode 100644 index 0000000000000000000000000000000000000000..bf547d8a764ac73f8da558f6af8793591467f3b0 GIT binary patch literal 6232 zcmc&&Z){W76~DGa2#~}E(Kh_)@H*IFT^|XMHKEO#0D%{jOq3-9tqre^?IbRV&Fp6g zlz}xTv}tCIF*dcGCb6xm(7qH+h^^~~8i82aX_DJ&Q>1NL+O3nVGS%%i76ME5JNMn= zJYGTDhiSXk^SS4D{@(lUJ@3AEytRG1+vO76JmL#NaupWRtOOH_Swo0s(IDpIf4NvD zb?42AKGqwmvLL2H_$5Fw%5fEyyjW%7Oefq@O-w4vcEplhMYScbsP@Ya)0`R#Q#A}x z1}(bAmE1{JOD{q&C0%N}-ZMkgAFh<_$LJ;t7EIahQP@$Q|7r6n#oa32o#w@-=7Xsk zjvU)k`0(J`@x?Cj=&IA1LpS&AtK9l6*UK-R|IFUa?>|N%NKQlH`9GWWdwgZh3kahr zU4m;%>#wiA`o~j8hw7G1PJDIm%9`sZj=qxlu=@SCKTxv{B}e-guIb_2N234*_g%O~y@<39f_ zQZEwniF-uOfd`F1Kt&n~j`_gh4cX$hR{ny&yw?gm{Vs>J8i z`YlNfqi--4H4RCh7fG1mcxT7ffmk%$8R!`ZTe90r zjNnj!90Ud;N5Y~l@nv*EU7gz-L^N<9V)TVgBiMsDy|Fo1rfCR>KKfpF z;W!j}HSelnDe|t`hVf*YegHAl<~>hpjZ@Gxmtlx6%(sSE)jc~oXViU(Mm5L7K0tPv z`mabg>ty6Hg}AT#7s zsgE_C0++uRBgpIcyE3l;-bLmq8qHrX!6n8+>fME28mPs$*1g_Zmj(JIee{N&>6p|r zU0K}42K^VUS-K4&eXMNFT6FZ$)_h$y|K!Z<>`2x-vIqA>A4@{NBL|ytee@6cV@kGC z$~tmzJeC~SUrVW`H(oq=ub$aep*Ox6@76~iE>G^!M{|G=6|8*fuEEy?fM)OME9COG z|FpvW7`fHXZQo3L1tP9rWPrVnGmJh#m_=A?wTuXX# zvL!W@^$uUdFf5=5VA4B0QUay`6qBj;%q~w`;~U;|0#mfjd!fDD)jB#34_U}l-jT;G zcw4!94x{;o>b7#v9JUEqYP#qh{v{%$rvK(0IY*&V*T3m~x&|^#LRrCuj=DeXnPzue z`zH&&C2kH_Y8s2L;2P3_H#yfl-GQ{u{zK@P~ zm4JU2`5*osJ95n>`!ulhyECb2Owm)8p}PVK)u)eDEiCh6v>iqJx}Czh{Rl=Q+2mdNSl1L5 z(NbPS#pEYnzl4N(tcahoBGytwDognnV3!}kpH{kA76Jjo^>+UVJj2d)*KhbSHut*g_sv+bY*2xx*H1t|6amMdkd8lbG?p}MYV5xbGp*A^nm_C@4Bg=-i$TAsh8&d#dxr%2u+Q$3?bYk}I7dZ?FMtf)n0rN(E8%}Sro5H*%~?qsH()5=Vo zRdQZGd{((f;b&DK#-CA%aGrh@Z-LV1cvuQD|Gi2d%T)48T@)3+LCLQxp2wx!{m%)% zH#-BLP!sT7MPE|%f}&Rx{f(l3R@CW-&!0}5d7d9Wk222ZQ0D&~-LZA+X8+o*o@CTa z`XAKlwb}6R?QXNpRSc?wj!1lcFnQ}f9m(~#P zPu`F8JR#p~&vk?;*A1x1#_N!Idhe$_%=Wx*nSNR+a{jExv=wry3Cwd{U`qK>bLMZM zLUbw_+jAXa%5{k2JI7B)4f4H3Ipn%lq6#PbyKp(}`+?D%aDC#s&y?#WRAh7B{~%Di z{Q*@Un5HbV(my{Zm_{A;MP<*F^JNFjGd<+6FAY#B-0w)3ydA2%rkuz7_B|F=jRlE_syvd%j0G{&MOXEQek`k zo-sj5p{=z@>7VVHUWUYOFVs1LKT7N>Sy~;oWBMzHJwFGZPzm-s?EMa&UK_|4=g;2( r$CW+DVKX%y%!&*k2R)_7^IdHbOQ;uy{{ZPe!#}1;;b`1XuVqej< literal 0 HcmV?d00001 diff --git a/core/node/node_modules/utf-8-validate/prebuilds/win32-ia32/node.napi.node b/core/node/node_modules/utf-8-validate/prebuilds/win32-ia32/node.napi.node new file mode 100644 index 0000000000000000000000000000000000000000..bb40b239d877c876cba0bf7d386d96c091c4a47c GIT binary patch literal 121856 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%0D* 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 F{{USO@H7Ab literal 0 HcmV?d00001 diff --git a/core/node/node_modules/utf-8-validate/src/validation.c b/core/node/node_modules/utf-8-validate/src/validation.c new file mode 100644 index 000000000..dd260b1dc --- /dev/null +++ b/core/node/node_modules/utf-8-validate/src/validation.c @@ -0,0 +1,109 @@ +#define NAPI_VERSION 1 +#include +#include +#include + +napi_value IsValidUTF8(napi_env env, napi_callback_info info) { + napi_status status; + size_t argc = 1; + napi_value argv[1]; + + status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + assert(status == napi_ok); + + uint8_t *buf; + size_t len; + + status = napi_get_buffer_info(env, argv[0], (void **)&buf, &len); + assert(status == napi_ok); + + size_t i = 0; + + // + // This code has been taken from utf8_check.c which was developed by + // Markus Kuhn . + // + // For original code / licensing please refer to + // https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c + // + while (i < len) { + size_t j = i + 8; + + if (j <= len) { + // + // Read 8 bytes and check if they are ASCII. + // + uint64_t chunk; + memcpy(&chunk, buf + i, 8); + + if ((chunk & 0x8080808080808080) == 0x00) { + i = j; + continue; + } + } + + while ((buf[i] & 0x80) == 0x00) { // 0xxxxxxx + if (++i == len) { + goto exit; + } + } + + if ((buf[i] & 0xe0) == 0xc0) { // 110xxxxx 10xxxxxx + if ( + i + 1 == len || + (buf[i + 1] & 0xc0) != 0x80 || + (buf[i] & 0xfe) == 0xc0 // overlong + ) { + break; + } + + i += 2; + } else if ((buf[i] & 0xf0) == 0xe0) { // 1110xxxx 10xxxxxx 10xxxxxx + if ( + i + 2 >= len || + (buf[i + 1] & 0xc0) != 0x80 || + (buf[i + 2] & 0xc0) != 0x80 || + (buf[i] == 0xe0 && (buf[i + 1] & 0xe0) == 0x80) || // overlong + (buf[i] == 0xed && (buf[i + 1] & 0xe0) == 0xa0) // surrogate (U+D800 - U+DFFF) + ) { + break; + } + + i += 3; + } else if ((buf[i] & 0xf8) == 0xf0) { // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if ( + i + 3 >= len || + (buf[i + 1] & 0xc0) != 0x80 || + (buf[i + 2] & 0xc0) != 0x80 || + (buf[i + 3] & 0xc0) != 0x80 || + (buf[i] == 0xf0 && (buf[i + 1] & 0xf0) == 0x80) || // overlong + (buf[i] == 0xf4 && buf[i + 1] > 0x8f) || buf[i] > 0xf4 // > U+10FFFF + ) { + break; + } + + i += 4; + } else { + break; + } + } + +exit:; + napi_value result; + status = napi_get_boolean(env, i == len, &result); + assert(status == napi_ok); + + return result; +} + +napi_value Init(napi_env env, napi_value exports) { + napi_status status; + napi_value isValidUTF8; + + status = napi_create_function(env, NULL, 0, IsValidUTF8, NULL, &isValidUTF8); + assert(status == napi_ok); + + return isValidUTF8; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/core/node/node_modules/mqtt/node_modules/ws/LICENSE b/core/node/node_modules/ws/LICENSE similarity index 100% rename from core/node/node_modules/mqtt/node_modules/ws/LICENSE rename to core/node/node_modules/ws/LICENSE diff --git a/core/node/node_modules/mqtt/node_modules/ws/README.md b/core/node/node_modules/ws/README.md similarity index 95% rename from core/node/node_modules/mqtt/node_modules/ws/README.md rename to core/node/node_modules/ws/README.md index f36a354bb..20a611496 100644 --- a/core/node/node_modules/mqtt/node_modules/ws/README.md +++ b/core/node/node_modules/ws/README.md @@ -1,9 +1,8 @@ # ws: a Node.js WebSocket library [![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws) -[![Build](https://img.shields.io/travis/websockets/ws/master.svg?logo=travis)](https://travis-ci.com/websockets/ws) -[![Windows x86 Build](https://img.shields.io/appveyor/ci/lpinca/ws/master.svg?logo=appveyor)](https://ci.appveyor.com/project/lpinca/ws) -[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg)](https://coveralls.io/github/websockets/ws) +[![CI](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) +[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws) ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and server implementation. @@ -23,7 +22,7 @@ can use one of the many wrappers available on npm, like - [Protocol support](#protocol-support) - [Installing](#installing) - - [Opt-in for performance and spec compliance](#opt-in-for-performance-and-spec-compliance) + - [Opt-in for performance](#opt-in-for-performance) - [API docs](#api-docs) - [WebSocket compression](#websocket-compression) - [Usage examples](#usage-examples) @@ -56,7 +55,7 @@ can use one of the many wrappers available on npm, like npm install ws ``` -### Opt-in for performance and spec compliance +### Opt-in for performance There are 2 optional modules that can be installed along side with the ws module. These modules are binary addons which improve certain operations. @@ -67,7 +66,7 @@ necessarily need to have a C++ compiler installed on your machine. operations such as masking and unmasking the data payload of the WebSocket frames. - `npm install --save-optional utf-8-validate`: Allows to efficiently check if a - message contains valid UTF-8 as required by the spec. + message contains valid UTF-8. ## API docs @@ -395,7 +394,7 @@ the `X-Forwarded-For` header. ```js wss.on('connection', function connection(ws, req) { - const ip = req.headers['x-forwarded-for'].split(/\s*,\s*/)[0]; + const ip = req.headers['x-forwarded-for'].split(',')[0].trim(); }); ``` diff --git a/core/node/node_modules/mqtt/node_modules/ws/browser.js b/core/node/node_modules/ws/browser.js similarity index 100% rename from core/node/node_modules/mqtt/node_modules/ws/browser.js rename to core/node/node_modules/ws/browser.js diff --git a/core/node/node_modules/mqtt/node_modules/ws/index.js b/core/node/node_modules/ws/index.js similarity index 100% rename from core/node/node_modules/mqtt/node_modules/ws/index.js rename to core/node/node_modules/ws/index.js diff --git a/core/node/node_modules/mqtt/node_modules/ws/lib/buffer-util.js b/core/node/node_modules/ws/lib/buffer-util.js similarity index 100% rename from core/node/node_modules/mqtt/node_modules/ws/lib/buffer-util.js rename to core/node/node_modules/ws/lib/buffer-util.js diff --git a/core/node/node_modules/mqtt/node_modules/ws/lib/constants.js b/core/node/node_modules/ws/lib/constants.js similarity index 100% rename from core/node/node_modules/mqtt/node_modules/ws/lib/constants.js rename to core/node/node_modules/ws/lib/constants.js diff --git a/core/node/node_modules/mqtt/node_modules/ws/lib/event-target.js b/core/node/node_modules/ws/lib/event-target.js similarity index 100% rename from core/node/node_modules/mqtt/node_modules/ws/lib/event-target.js rename to core/node/node_modules/ws/lib/event-target.js diff --git a/core/node/node_modules/mqtt/node_modules/ws/lib/extension.js b/core/node/node_modules/ws/lib/extension.js similarity index 100% rename from core/node/node_modules/mqtt/node_modules/ws/lib/extension.js rename to core/node/node_modules/ws/lib/extension.js diff --git a/core/node/node_modules/mqtt/node_modules/ws/lib/limiter.js b/core/node/node_modules/ws/lib/limiter.js similarity index 100% rename from core/node/node_modules/mqtt/node_modules/ws/lib/limiter.js rename to core/node/node_modules/ws/lib/limiter.js diff --git a/core/node/node_modules/mqtt/node_modules/ws/lib/permessage-deflate.js b/core/node/node_modules/ws/lib/permessage-deflate.js similarity index 97% rename from core/node/node_modules/mqtt/node_modules/ws/lib/permessage-deflate.js rename to core/node/node_modules/ws/lib/permessage-deflate.js index 7d7209b9e..ce9178429 100644 --- a/core/node/node_modules/mqtt/node_modules/ws/lib/permessage-deflate.js +++ b/core/node/node_modules/ws/lib/permessage-deflate.js @@ -376,12 +376,16 @@ class PerMessageDeflate { this._inflate[kTotalLength] ); - if (fin && this.params[`${endpoint}_no_context_takeover`]) { + if (this._inflate._readableState.endEmitted) { this._inflate.close(); this._inflate = null; } else { this._inflate[kTotalLength] = 0; this._inflate[kBuffers] = []; + + if (fin && this.params[`${endpoint}_no_context_takeover`]) { + this._inflate.reset(); + } } callback(null, data); @@ -448,12 +452,11 @@ class PerMessageDeflate { // this._deflate[kCallback] = null; + this._deflate[kTotalLength] = 0; + this._deflate[kBuffers] = []; + if (fin && this.params[`${endpoint}_no_context_takeover`]) { - this._deflate.close(); - this._deflate = null; - } else { - this._deflate[kTotalLength] = 0; - this._deflate[kBuffers] = []; + this._deflate.reset(); } callback(null, data); @@ -492,6 +495,7 @@ function inflateOnData(chunk) { } this[kError] = new RangeError('Max payload size exceeded'); + this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; this[kError][kStatusCode] = 1009; this.removeListener('data', inflateOnData); this.reset(); diff --git a/core/node/node_modules/mqtt/node_modules/ws/lib/receiver.js b/core/node/node_modules/ws/lib/receiver.js similarity index 79% rename from core/node/node_modules/mqtt/node_modules/ws/lib/receiver.js rename to core/node/node_modules/ws/lib/receiver.js index 65a5ab45f..1d2af76e1 100644 --- a/core/node/node_modules/mqtt/node_modules/ws/lib/receiver.js +++ b/core/node/node_modules/ws/lib/receiver.js @@ -22,7 +22,7 @@ const INFLATING = 5; /** * HyBi Receiver implementation. * - * @extends stream.Writable + * @extends Writable */ class Receiver extends Writable { /** @@ -168,14 +168,26 @@ class Receiver extends Writable { if ((buf[0] & 0x30) !== 0x00) { this._loop = false; - return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002); + return error( + RangeError, + 'RSV2 and RSV3 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_2_3' + ); } const compressed = (buf[0] & 0x40) === 0x40; if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } this._fin = (buf[0] & 0x80) === 0x80; @@ -185,31 +197,61 @@ class Receiver extends Writable { if (this._opcode === 0x00) { if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } if (!this._fragmented) { this._loop = false; - return error(RangeError, 'invalid opcode 0', true, 1002); + return error( + RangeError, + 'invalid opcode 0', + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._opcode = this._fragmented; } else if (this._opcode === 0x01 || this._opcode === 0x02) { if (this._fragmented) { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } this._compressed = compressed; } else if (this._opcode > 0x07 && this._opcode < 0x0b) { if (!this._fin) { this._loop = false; - return error(RangeError, 'FIN must be set', true, 1002); + return error( + RangeError, + 'FIN must be set', + true, + 1002, + 'WS_ERR_EXPECTED_FIN' + ); } if (compressed) { this._loop = false; - return error(RangeError, 'RSV1 must be clear', true, 1002); + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); } if (this._payloadLength > 0x7d) { @@ -218,12 +260,19 @@ class Receiver extends Writable { RangeError, `invalid payload length ${this._payloadLength}`, true, - 1002 + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' ); } } else { this._loop = false; - return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); } if (!this._fin && !this._fragmented) this._fragmented = this._opcode; @@ -232,11 +281,23 @@ class Receiver extends Writable { if (this._isServer) { if (!this._masked) { this._loop = false; - return error(RangeError, 'MASK must be set', true, 1002); + return error( + RangeError, + 'MASK must be set', + true, + 1002, + 'WS_ERR_EXPECTED_MASK' + ); } } else if (this._masked) { this._loop = false; - return error(RangeError, 'MASK must be clear', true, 1002); + return error( + RangeError, + 'MASK must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_MASK' + ); } if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; @@ -285,7 +346,8 @@ class Receiver extends Writable { RangeError, 'Unsupported WebSocket frame: payload length > 2^53 - 1', false, - 1009 + 1009, + 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' ); } @@ -304,7 +366,13 @@ class Receiver extends Writable { this._totalPayloadLength += this._payloadLength; if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { this._loop = false; - return error(RangeError, 'Max payload size exceeded', false, 1009); + return error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); } } @@ -384,7 +452,13 @@ class Receiver extends Writable { this._messageLength += buf.length; if (this._messageLength > this._maxPayload && this._maxPayload > 0) { return cb( - error(RangeError, 'Max payload size exceeded', false, 1009) + error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ) ); } @@ -431,7 +505,13 @@ class Receiver extends Writable { if (!isValidUTF8(buf)) { this._loop = false; - return error(Error, 'invalid UTF-8 sequence', true, 1007); + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } this.emit('message', buf.toString()); @@ -456,18 +536,36 @@ class Receiver extends Writable { this.emit('conclude', 1005, ''); this.end(); } else if (data.length === 1) { - return error(RangeError, 'invalid payload length 1', true, 1002); + return error( + RangeError, + 'invalid payload length 1', + true, + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' + ); } else { const code = data.readUInt16BE(0); if (!isValidStatusCode(code)) { - return error(RangeError, `invalid status code ${code}`, true, 1002); + return error( + RangeError, + `invalid status code ${code}`, + true, + 1002, + 'WS_ERR_INVALID_CLOSE_CODE' + ); } const buf = data.slice(2); if (!isValidUTF8(buf)) { - return error(Error, 'invalid UTF-8 sequence', true, 1007); + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); } this.emit('conclude', code, buf.toString()); @@ -488,20 +586,22 @@ module.exports = Receiver; /** * Builds an error object. * - * @param {(Error|RangeError)} ErrorCtor The error constructor + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor * @param {String} message The error message * @param {Boolean} prefix Specifies whether or not to add a default prefix to * `message` * @param {Number} statusCode The status code + * @param {String} errorCode The exposed error code * @return {(Error|RangeError)} The error * @private */ -function error(ErrorCtor, message, prefix, statusCode) { +function error(ErrorCtor, message, prefix, statusCode, errorCode) { const err = new ErrorCtor( prefix ? `Invalid WebSocket frame: ${message}` : message ); Error.captureStackTrace(err, error); + err.code = errorCode; err[kStatusCode] = statusCode; return err; } diff --git a/core/node/node_modules/mqtt/node_modules/ws/lib/sender.js b/core/node/node_modules/ws/lib/sender.js similarity index 98% rename from core/node/node_modules/mqtt/node_modules/ws/lib/sender.js rename to core/node/node_modules/ws/lib/sender.js index ad71e1950..441171c57 100644 --- a/core/node/node_modules/mqtt/node_modules/ws/lib/sender.js +++ b/core/node/node_modules/ws/lib/sender.js @@ -1,5 +1,9 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls$" }] */ + 'use strict'; +const net = require('net'); +const tls = require('tls'); const { randomFillSync } = require('crypto'); const PerMessageDeflate = require('./permessage-deflate'); @@ -16,7 +20,7 @@ class Sender { /** * Creates a Sender instance. * - * @param {net.Socket} socket The connection socket + * @param {(net.Socket|tls.Socket)} socket The connection socket * @param {Object} [extensions] An object containing the negotiated extensions */ constructor(socket, extensions) { diff --git a/core/node/node_modules/mqtt/node_modules/ws/lib/stream.js b/core/node/node_modules/ws/lib/stream.js similarity index 81% rename from core/node/node_modules/mqtt/node_modules/ws/lib/stream.js rename to core/node/node_modules/ws/lib/stream.js index 604cf366b..19e1bff4a 100644 --- a/core/node/node_modules/mqtt/node_modules/ws/lib/stream.js +++ b/core/node/node_modules/ws/lib/stream.js @@ -5,7 +5,7 @@ const { Duplex } = require('stream'); /** * Emits the `'close'` event on a stream. * - * @param {stream.Duplex} The stream. + * @param {Duplex} stream The stream. * @private */ function emitClose(stream) { @@ -43,11 +43,12 @@ function duplexOnError(err) { * * @param {WebSocket} ws The `WebSocket` to wrap * @param {Object} [options] The options for the `Duplex` constructor - * @return {stream.Duplex} The duplex stream + * @return {Duplex} The duplex stream * @public */ function createWebSocketStream(ws, options) { let resumeOnReceiverDrain = true; + let terminateOnDestroy = true; function receiverOnDrain() { if (resumeOnReceiverDrain) ws._socket.resume(); @@ -81,6 +82,16 @@ function createWebSocketStream(ws, options) { ws.once('error', function error(err) { if (duplex.destroyed) return; + // Prevent `ws.terminate()` from being called by `duplex._destroy()`. + // + // - If the `'error'` event is emitted before the `'open'` event, then + // `ws.terminate()` is a noop as no socket is assigned. + // - Otherwise, the error is re-emitted by the listener of the `'error'` + // event of the `Receiver` object. The listener already closes the + // connection by calling `ws.close()`. This allows a close frame to be + // sent to the other peer. If `ws.terminate()` is called right after this, + // then the close frame might not be sent. + terminateOnDestroy = false; duplex.destroy(err); }); @@ -108,7 +119,8 @@ function createWebSocketStream(ws, options) { if (!called) callback(err); process.nextTick(emitClose, duplex); }); - ws.terminate(); + + if (terminateOnDestroy) ws.terminate(); }; duplex._final = function (callback) { @@ -140,7 +152,10 @@ function createWebSocketStream(ws, options) { }; duplex._read = function () { - if (ws.readyState === ws.OPEN && !resumeOnReceiverDrain) { + if ( + (ws.readyState === ws.OPEN || ws.readyState === ws.CLOSING) && + !resumeOnReceiverDrain + ) { resumeOnReceiverDrain = true; if (!ws._receiver._writableState.needDrain) ws._socket.resume(); } diff --git a/core/node/node_modules/ws/lib/validation.js b/core/node/node_modules/ws/lib/validation.js new file mode 100644 index 000000000..169ac6f06 --- /dev/null +++ b/core/node/node_modules/ws/lib/validation.js @@ -0,0 +1,104 @@ +'use strict'; + +/** + * Checks if a status code is allowed in a close frame. + * + * @param {Number} code The status code + * @return {Boolean} `true` if the status code is valid, else `false` + * @public + */ +function isValidStatusCode(code) { + return ( + (code >= 1000 && + code <= 1014 && + code !== 1004 && + code !== 1005 && + code !== 1006) || + (code >= 3000 && code <= 4999) + ); +} + +/** + * Checks if a given buffer contains only correct UTF-8. + * Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by + * Markus Kuhn. + * + * @param {Buffer} buf The buffer to check + * @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false` + * @public + */ +function _isValidUTF8(buf) { + const len = buf.length; + let i = 0; + + while (i < len) { + if ((buf[i] & 0x80) === 0) { + // 0xxxxxxx + i++; + } else if ((buf[i] & 0xe0) === 0xc0) { + // 110xxxxx 10xxxxxx + if ( + i + 1 === len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i] & 0xfe) === 0xc0 // Overlong + ) { + return false; + } + + i += 2; + } else if ((buf[i] & 0xf0) === 0xe0) { + // 1110xxxx 10xxxxxx 10xxxxxx + if ( + i + 2 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong + (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) + ) { + return false; + } + + i += 3; + } else if ((buf[i] & 0xf8) === 0xf0) { + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if ( + i + 3 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i + 3] & 0xc0) !== 0x80 || + (buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong + (buf[i] === 0xf4 && buf[i + 1] > 0x8f) || + buf[i] > 0xf4 // > U+10FFFF + ) { + return false; + } + + i += 4; + } else { + return false; + } + } + + return true; +} + +try { + let isValidUTF8 = require('utf-8-validate'); + + /* istanbul ignore if */ + if (typeof isValidUTF8 === 'object') { + isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0 + } + + module.exports = { + isValidStatusCode, + isValidUTF8(buf) { + return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); + } + }; +} catch (e) /* istanbul ignore next */ { + module.exports = { + isValidStatusCode, + isValidUTF8: _isValidUTF8 + }; +} diff --git a/core/node/node_modules/mqtt/node_modules/ws/lib/websocket-server.js b/core/node/node_modules/ws/lib/websocket-server.js similarity index 84% rename from core/node/node_modules/mqtt/node_modules/ws/lib/websocket-server.js rename to core/node/node_modules/ws/lib/websocket-server.js index be481a0f0..fe7fdf501 100644 --- a/core/node/node_modules/mqtt/node_modules/ws/lib/websocket-server.js +++ b/core/node/node_modules/ws/lib/websocket-server.js @@ -1,8 +1,13 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */ + 'use strict'; const EventEmitter = require('events'); +const http = require('http'); +const https = require('https'); +const net = require('net'); +const tls = require('tls'); const { createHash } = require('crypto'); -const { createServer, STATUS_CODES } = require('http'); const PerMessageDeflate = require('./permessage-deflate'); const WebSocket = require('./websocket'); @@ -11,6 +16,10 @@ const { GUID, kWebSocket } = require('./constants'); const keyRegex = /^[+/0-9A-Za-z]{22}==$/; +const RUNNING = 0; +const CLOSING = 1; +const CLOSED = 2; + /** * Class representing a WebSocket server. * @@ -34,7 +43,8 @@ class WebSocketServer extends EventEmitter { * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable * permessage-deflate * @param {Number} [options.port] The port where to bind the server - * @param {http.Server} [options.server] A pre-created HTTP/S server to use + * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S + * server to use * @param {Function} [options.verifyClient] A hook to reject connections * @param {Function} [callback] A listener for the `listening` event */ @@ -56,15 +66,20 @@ class WebSocketServer extends EventEmitter { ...options }; - if (options.port == null && !options.server && !options.noServer) { + if ( + (options.port == null && !options.server && !options.noServer) || + (options.port != null && (options.server || options.noServer)) || + (options.server && options.noServer) + ) { throw new TypeError( - 'One of the "port", "server", or "noServer" options must be specified' + 'One and only one of the "port", "server", or "noServer" options ' + + 'must be specified' ); } if (options.port != null) { - this._server = createServer((req, res) => { - const body = STATUS_CODES[426]; + this._server = http.createServer((req, res) => { + const body = http.STATUS_CODES[426]; res.writeHead(426, { 'Content-Length': body.length, @@ -97,6 +112,7 @@ class WebSocketServer extends EventEmitter { if (options.perMessageDeflate === true) options.perMessageDeflate = {}; if (options.clientTracking) this.clients = new Set(); this.options = options; + this._state = RUNNING; } /** @@ -126,6 +142,14 @@ class WebSocketServer extends EventEmitter { close(cb) { if (cb) this.once('close', cb); + if (this._state === CLOSED) { + process.nextTick(emitClose, this); + return; + } + + if (this._state === CLOSING) return; + this._state = CLOSING; + // // Terminate all associated clients. // @@ -143,7 +167,7 @@ class WebSocketServer extends EventEmitter { // Close the http server if it was internally created. // if (this.options.port != null) { - server.close(() => this.emit('close')); + server.close(emitClose.bind(undefined, this)); return; } } @@ -173,7 +197,8 @@ class WebSocketServer extends EventEmitter { * Handle a HTTP Upgrade request. * * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @public @@ -225,7 +250,7 @@ class WebSocketServer extends EventEmitter { const info = { origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], - secure: !!(req.connection.authorized || req.connection.encrypted), + secure: !!(req.socket.authorized || req.socket.encrypted), req }; @@ -252,7 +277,8 @@ class WebSocketServer extends EventEmitter { * @param {String} key The value of the `Sec-WebSocket-Key` header * @param {Object} extensions The accepted extensions * @param {http.IncomingMessage} req The request object - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @throws {Error} If called more than once with the same socket @@ -271,6 +297,8 @@ class WebSocketServer extends EventEmitter { ); } + if (this._state > RUNNING) return abortHandshake(socket, 503); + const digest = createHash('sha1') .update(key + GUID) .digest('base64'); @@ -286,7 +314,7 @@ class WebSocketServer extends EventEmitter { let protocol = req.headers['sec-websocket-protocol']; if (protocol) { - protocol = protocol.trim().split(/ *, */); + protocol = protocol.split(',').map(trim); // // Optionally call external protocol selection handler. @@ -360,6 +388,7 @@ function addListeners(server, map) { * @private */ function emitClose(server) { + server._state = CLOSED; server.emit('close'); } @@ -375,7 +404,7 @@ function socketOnError() { /** * Close the connection when preconditions are not fulfilled. * - * @param {net.Socket} socket The socket of the upgrade request + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} [message] The HTTP response body * @param {Object} [headers] Additional HTTP response headers @@ -383,7 +412,7 @@ function socketOnError() { */ function abortHandshake(socket, code, message, headers) { if (socket.writable) { - message = message || STATUS_CODES[code]; + message = message || http.STATUS_CODES[code]; headers = { Connection: 'close', 'Content-Type': 'text/html', @@ -392,7 +421,7 @@ function abortHandshake(socket, code, message, headers) { }; socket.write( - `HTTP/1.1 ${code} ${STATUS_CODES[code]}\r\n` + + `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + Object.keys(headers) .map((h) => `${h}: ${headers[h]}`) .join('\r\n') + @@ -404,3 +433,15 @@ function abortHandshake(socket, code, message, headers) { socket.removeListener('error', socketOnError); socket.destroy(); } + +/** + * Remove whitespace characters from both ends of a string. + * + * @param {String} str The string + * @return {String} A new string representing `str` stripped of whitespace + * characters from both its beginning and end + * @private + */ +function trim(str) { + return str.trim(); +} diff --git a/core/node/node_modules/mqtt/node_modules/ws/lib/websocket.js b/core/node/node_modules/ws/lib/websocket.js similarity index 74% rename from core/node/node_modules/mqtt/node_modules/ws/lib/websocket.js rename to core/node/node_modules/ws/lib/websocket.js index 0e2a83d06..1df89675d 100644 --- a/core/node/node_modules/mqtt/node_modules/ws/lib/websocket.js +++ b/core/node/node_modules/ws/lib/websocket.js @@ -1,3 +1,5 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Readable$" }] */ + 'use strict'; const EventEmitter = require('events'); @@ -6,6 +8,7 @@ const http = require('http'); const net = require('net'); const tls = require('tls'); const { randomBytes, createHash } = require('crypto'); +const { Readable } = require('stream'); const { URL } = require('url'); const PerMessageDeflate = require('./permessage-deflate'); @@ -36,7 +39,7 @@ class WebSocket extends EventEmitter { /** * Create a new `WebSocket`. * - * @param {(String|url.URL)} address The URL to which to connect + * @param {(String|URL)} address The URL to which to connect * @param {(String|String[])} [protocols] The subprotocols * @param {Object} [options] Connection options */ @@ -112,6 +115,50 @@ class WebSocket extends EventEmitter { return Object.keys(this._extensions).join(); } + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onclose() { + return undefined; + } + + /* istanbul ignore next */ + set onclose(listener) {} + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onerror() { + return undefined; + } + + /* istanbul ignore next */ + set onerror(listener) {} + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onopen() { + return undefined; + } + + /* istanbul ignore next */ + set onopen(listener) {} + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onmessage() { + return undefined; + } + + /* istanbul ignore next */ + set onmessage(listener) {} + /** * @type {String} */ @@ -136,7 +183,8 @@ class WebSocket extends EventEmitter { /** * Set up the socket and the internal resources. * - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Number} [maxPayload=0] The maximum allowed message size * @private @@ -225,7 +273,13 @@ class WebSocket extends EventEmitter { } if (this.readyState === WebSocket.CLOSING) { - if (this._closeFrameSent && this._closeFrameReceived) this._socket.end(); + if ( + this._closeFrameSent && + (this._closeFrameReceived || this._receiver._writableState.errorEmitted) + ) { + this._socket.end(); + } + return; } @@ -238,7 +292,13 @@ class WebSocket extends EventEmitter { if (err) return; this._closeFrameSent = true; - if (this._closeFrameReceived) this._socket.end(); + + if ( + this._closeFrameReceived || + this._receiver._writableState.errorEmitted + ) { + this._socket.end(); + } }); // @@ -380,11 +440,76 @@ class WebSocket extends EventEmitter { } } -readyStates.forEach((readyState, i) => { - const descriptor = { enumerable: true, value: i }; +/** + * @constant {Number} CONNECTING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} CONNECTING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); - Object.defineProperty(WebSocket.prototype, readyState, descriptor); - Object.defineProperty(WebSocket, readyState, descriptor); +/** + * @constant {Number} OPEN + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') }); [ @@ -404,14 +529,7 @@ readyStates.forEach((readyState, i) => { // ['open', 'error', 'close', 'message'].forEach((method) => { Object.defineProperty(WebSocket.prototype, `on${method}`, { - configurable: true, enumerable: true, - /** - * Return the listener of the event. - * - * @return {(Function|undefined)} The event listener or `undefined` - * @public - */ get() { const listeners = this.listeners(method); for (let i = 0; i < listeners.length; i++) { @@ -420,12 +538,6 @@ readyStates.forEach((readyState, i) => { return undefined; }, - /** - * Add a listener for the event. - * - * @param {Function} listener The listener to add - * @public - */ set(listener) { const listeners = this.listeners(method); for (let i = 0; i < listeners.length; i++) { @@ -448,7 +560,7 @@ module.exports = WebSocket; * Initialize a WebSocket client. * * @param {WebSocket} websocket The client to initialize - * @param {(String|url.URL)} address The URL to which to connect + * @param {(String|URL)} address The URL to which to connect * @param {String} [protocols] The subprotocols * @param {Object} [options] Connection options * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable @@ -506,7 +618,14 @@ function initAsClient(websocket, address, protocols, options) { const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) { - throw new Error(`Invalid URL: ${websocket.url}`); + const err = new Error(`Invalid URL: ${websocket.url}`); + + if (websocket._redirects === 0) { + throw err; + } else { + emitErrorAndClose(websocket, err); + return; + } } const isSecure = @@ -563,6 +682,61 @@ function initAsClient(websocket, address, protocols, options) { opts.path = parts[1]; } + if (opts.followRedirects) { + if (websocket._redirects === 0) { + websocket._originalUnixSocket = isUnixSocket; + websocket._originalSecure = isSecure; + websocket._originalHostOrSocketPath = isUnixSocket + ? opts.socketPath + : parsedUrl.host; + + const headers = options && options.headers; + + // + // Shallow copy the user provided options so that headers can be changed + // without mutating the original object. + // + options = { ...options, headers: {} }; + + if (headers) { + for (const [key, value] of Object.entries(headers)) { + options.headers[key.toLowerCase()] = value; + } + } + } else { + const isSameHost = isUnixSocket + ? websocket._originalUnixSocket + ? opts.socketPath === websocket._originalHostOrSocketPath + : false + : websocket._originalUnixSocket + ? false + : parsedUrl.host === websocket._originalHostOrSocketPath; + + if (!isSameHost || (websocket._originalSecure && !isSecure)) { + // + // Match curl 7.77.0 behavior and drop the following headers. These + // headers are also dropped when following a redirect to a subdomain. + // + delete opts.headers.authorization; + delete opts.headers.cookie; + + if (!isSameHost) delete opts.headers.host; + + opts.auth = undefined; + } + } + + // + // Match curl 7.77.0 behavior and make the first `Authorization` header win. + // If the `Authorization` header is set, then there is nothing to do as it + // will take precedence. + // + if (opts.auth && !options.headers.authorization) { + options.headers.authorization = + 'Basic ' + Buffer.from(opts.auth).toString('base64'); + } + } + let req = (websocket._req = get(opts)); if (opts.timeout) { @@ -575,9 +749,7 @@ function initAsClient(websocket, address, protocols, options) { if (req === null || req.aborted) return; req = websocket._req = null; - websocket._readyState = WebSocket.CLOSING; - websocket.emit('error', err); - websocket.emitClose(); + emitErrorAndClose(websocket, err); }); req.on('response', (res) => { @@ -597,7 +769,14 @@ function initAsClient(websocket, address, protocols, options) { req.abort(); - const addr = new URL(location, address); + let addr; + + try { + addr = new URL(location, address); + } catch (err) { + emitErrorAndClose(websocket, err); + return; + } initAsClient(websocket, addr, protocols, options); } else if (!websocket.emit('unexpected-response', req, res)) { @@ -620,6 +799,11 @@ function initAsClient(websocket, address, protocols, options) { req = websocket._req = null; + if (res.headers.upgrade.toLowerCase() !== 'websocket') { + abortHandshake(websocket, socket, 'Invalid Upgrade header'); + return; + } + const digest = createHash('sha1') .update(key + GUID) .digest('base64'); @@ -648,23 +832,50 @@ function initAsClient(websocket, address, protocols, options) { if (serverProt) websocket._protocol = serverProt; - if (perMessageDeflate) { + const secWebSocketExtensions = res.headers['sec-websocket-extensions']; + + if (secWebSocketExtensions !== undefined) { + if (!perMessageDeflate) { + const message = + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested'; + abortHandshake(websocket, socket, message); + return; + } + + let extensions; + try { - const extensions = parse(res.headers['sec-websocket-extensions']); + extensions = parse(secWebSocketExtensions); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } - if (extensions[PerMessageDeflate.extensionName]) { + const extensionNames = Object.keys(extensions); + + if (extensionNames.length) { + if ( + extensionNames.length !== 1 || + extensionNames[0] !== PerMessageDeflate.extensionName + ) { + const message = + 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } + + try { perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); - websocket._extensions[ - PerMessageDeflate.extensionName - ] = perMessageDeflate; + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; } - } catch (err) { - abortHandshake( - websocket, - socket, - 'Invalid Sec-WebSocket-Extensions header' - ); - return; + + websocket._extensions[PerMessageDeflate.extensionName] = + perMessageDeflate; } } @@ -672,6 +883,19 @@ function initAsClient(websocket, address, protocols, options) { }); } +/** + * Emit the `'error'` and `'close'` event. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {Error} The error to emit + * @private + */ +function emitErrorAndClose(websocket, err) { + websocket._readyState = WebSocket.CLOSING; + websocket.emit('error', err); + websocket.emitClose(); +} + /** * Create a `net.Socket` and initiate a connection. * @@ -705,8 +929,8 @@ function tlsConnect(options) { * Abort the handshake and emit an error. * * @param {WebSocket} websocket The WebSocket instance - * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the - * socket to destroy + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to + * abort or the socket to destroy * @param {String} message The error message * @private */ @@ -718,6 +942,16 @@ function abortHandshake(websocket, stream, message) { if (stream.setHeader) { stream.abort(); + + if (stream.socket && !stream.socket.destroyed) { + // + // On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if + // called after the request completed. See + // https://github.com/websockets/ws/issues/1869. + // + stream.socket.destroy(); + } + stream.once('abort', websocket.emitClose.bind(websocket)); websocket.emit('error', err); } else { @@ -769,13 +1003,15 @@ function sendAfterClose(websocket, data, cb) { function receiverOnConclude(code, reason) { const websocket = this[kWebSocket]; - websocket._socket.removeListener('data', socketOnData); - websocket._socket.resume(); - websocket._closeFrameReceived = true; websocket._closeMessage = reason; websocket._closeCode = code; + if (websocket._socket[kWebSocket] === undefined) return; + + websocket._socket.removeListener('data', socketOnData); + process.nextTick(resume, websocket._socket); + if (code === 1005) websocket.close(); else websocket.close(code, reason); } @@ -798,12 +1034,19 @@ function receiverOnDrain() { function receiverOnError(err) { const websocket = this[kWebSocket]; - websocket._socket.removeListener('data', socketOnData); + if (websocket._socket[kWebSocket] !== undefined) { + websocket._socket.removeListener('data', socketOnData); + + // + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See + // https://github.com/websockets/ws/issues/1940. + // + process.nextTick(resume, websocket._socket); + + websocket.close(err[kStatusCode]); + } - websocket._readyState = WebSocket.CLOSING; - websocket._closeCode = err[kStatusCode]; websocket.emit('error', err); - websocket._socket.destroy(); } /** @@ -848,6 +1091,16 @@ function receiverOnPong(data) { this[kWebSocket].emit('pong', data); } +/** + * Resume a readable stream + * + * @param {Readable} stream The readable stream + * @private + */ +function resume(stream) { + stream.resume(); +} + /** * The listener of the `net.Socket` `'close'` event. * @@ -857,10 +1110,13 @@ function socketOnClose() { const websocket = this[kWebSocket]; this.removeListener('close', socketOnClose); + this.removeListener('data', socketOnData); this.removeListener('end', socketOnEnd); websocket._readyState = WebSocket.CLOSING; + let chunk; + // // The close frame might not have been received or the `'end'` event emitted, // for example, if the socket was destroyed due to an error. Ensure that the @@ -868,13 +1124,19 @@ function socketOnClose() { // it. If the readable side of the socket is in flowing mode then there is no // buffered data as everything has been already written and `readable.read()` // will return `null`. If instead, the socket is paused, any possible buffered - // data will be read as a single chunk and emitted synchronously in a single - // `'data'` event. + // data will be read as a single chunk. // - websocket._socket.read(); + if ( + !this._readableState.endEmitted && + !websocket._closeFrameReceived && + !websocket._receiver._writableState.errorEmitted && + (chunk = websocket._socket.read()) !== null + ) { + websocket._receiver.write(chunk); + } + websocket._receiver.end(); - this.removeListener('data', socketOnData); this[kWebSocket] = undefined; clearTimeout(websocket._closeTimer); diff --git a/core/node/node_modules/ws/package.json b/core/node/node_modules/ws/package.json new file mode 100644 index 000000000..832203f65 --- /dev/null +++ b/core/node/node_modules/ws/package.json @@ -0,0 +1,56 @@ +{ + "name": "ws", + "version": "7.5.9", + "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", + "keywords": [ + "HyBi", + "Push", + "RFC-6455", + "WebSocket", + "WebSockets", + "real-time" + ], + "homepage": "https://github.com/websockets/ws", + "bugs": "https://github.com/websockets/ws/issues", + "repository": "websockets/ws", + "author": "Einar Otto Stangvik (http://2x.io)", + "license": "MIT", + "main": "index.js", + "browser": "browser.js", + "engines": { + "node": ">=8.3.0" + }, + "files": [ + "browser.js", + "index.js", + "lib/*.js" + ], + "scripts": { + "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js", + "integration": "mocha --throw-deprecation test/*.integration.js", + "lint": "eslint --ignore-path .gitignore . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\"" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + }, + "devDependencies": { + "benchmark": "^2.1.4", + "bufferutil": "^4.0.1", + "eslint": "^7.2.0", + "eslint-config-prettier": "^8.1.0", + "eslint-plugin-prettier": "^4.0.0", + "mocha": "^7.0.0", + "nyc": "^15.0.0", + "prettier": "^2.0.5", + "utf-8-validate": "^5.0.2" + } +} diff --git a/core/node/package-lock.json b/core/node/package-lock.json index 871d8f347..91a227369 100644 --- a/core/node/package-lock.json +++ b/core/node/package-lock.json @@ -51,6 +51,31 @@ "xstate": "^4.13.0" } }, + "connectome": { + "version": "0.2.9", + "extraneous": true, + "license": "ISC", + "dependencies": { + "browser-util-inspect": "^0.2.0", + "bufferutil": "^4.0.2", + "fast-json-patch": "^3.0.0-1", + "kleur": "^4.1.5", + "quantum-generator": "^1.9.1", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1", + "utf-8-validate": "^5.0.3", + "ws": "^8.13.0" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^16.0.0", + "@rollup/plugin-node-resolve": "^10.0.0", + "builtin-modules": "^3.1.0", + "rollup": "^2.33.3" + } + }, + "connectome-next": { + "extraneous": true + }, "node_modules/@types/http-proxy": { "version": "1.17.9", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", @@ -346,6 +371,20 @@ "node": ">=0.2.0" } }, + "node_modules/bufferutil": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", + "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -1781,14 +1820,6 @@ "node": ">= 6" } }, - "node_modules/mqtt/node_modules/ws": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", - "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==", - "engines": { - "node": ">=8.3.0" - } - }, "node_modules/ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -1873,6 +1904,18 @@ "node": "4.x || >=6.0.0" } }, + "node_modules/node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "optional": true, + "peer": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-ipc": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.1.1.tgz", @@ -2823,6 +2866,20 @@ "iconv-lite": "~0.4.11" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2932,6 +2989,26 @@ "typedarray-to-buffer": "^3.1.5" } }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wtfnode": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.9.1.tgz", @@ -3244,6 +3321,16 @@ "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" }, + "bufferutil": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", + "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", + "optional": true, + "peer": true, + "requires": { + "node-gyp-build": "^4.3.0" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -4382,11 +4469,6 @@ "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } - }, - "ws": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", - "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==" } } }, @@ -4490,6 +4572,13 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, + "node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "optional": true, + "peer": true + }, "node-ipc": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.1.1.tgz", @@ -5256,6 +5345,16 @@ "iconv-lite": "~0.4.11" } }, + "utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "optional": true, + "peer": true, + "requires": { + "node-gyp-build": "^4.3.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5343,6 +5442,12 @@ "typedarray-to-buffer": "^3.1.5" } }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "requires": {} + }, "wtfnode": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.9.1.tgz", diff --git a/etc/.abc_version b/etc/.abc_version index 5b46ceb26..f38f48df5 100644 --- a/etc/.abc_version +++ b/etc/.abc_version @@ -1 +1 @@ -0.043 +0.047 diff --git a/etc/.deployignore b/etc/.deployignore index 34d10bba9..d0419d82d 100644 --- a/etc/.deployignore +++ b/etc/.deployignore @@ -2,4 +2,3 @@ .deploy .deployignore .git -node_modules/eslint* diff --git a/etc/integrate/README.md b/etc/integrate/README.md index f75283011..7f99570b8 100644 --- a/etc/integrate/README.md +++ b/etc/integrate/README.md @@ -2,7 +2,7 @@ Run `dmt integrate` inside an _installable DMT app directory_. -This will install or **integrate the app into DMT ENGINE**. It is not called simply "dmt install" because this would mean installing the DMT ENGINE somewhere, to avoid confusion and be even more descriptive we call installing apps into the engine "to integrate". +This will **integrate (install) the app into DMT ENGINE**. It is not called simply "dmt install" because this could mean installing the DMT ENGINE somewhere, to avoid confusion and be even more descriptive we call installing apps into the engine "integration". See [svelte-demo](https://github.com/dmtsys/svelte-demo) for a nice example of a simple DMT-installable app. @@ -26,7 +26,7 @@ target: user - `build` — directory with frontend result which is synced into `~/.dmt/user/apps` (user) or `~/.dmt-here/apps` (device) - `target` — `device` or `user` -### DMT hook +### DMT ENGINE SubPrograms If installable app has a `dmt` directory then this is synced to `~/.dmt/user/apps/[app_name]/dmt`. This directory contains `index.js` which is integrated into DMT ENGINE. This directory is called DMT hook and should be used for backend logic, not to serve the frontend or things like that (for that use SSR handler). @@ -49,6 +49,21 @@ This works with SvelteKit and other apps that use express-compatible server midd If app has `index.html` then directory is served statically without SSR. +### SvelteKit app preparation + +In `svelte.config.js` you have to add `base` key [like this](https://github.com/dmtsys/svelte-demo/blob/main/svelte.config.js#L7-L9): + +```js +kit: { + paths: { + base: process.env.BASE ? `/${process.env.BASE}` : '' + }, + ... + } +``` + +`dmt integrate` will set the BASE environment variable correctly based on value in `settings.def`. + ### Special options - `dmt integrate --reset` — will first delete the target app directory if it exists instead of syncing over it diff --git a/etc/integrate/dmt-integrate b/etc/integrate/dmt-integrate index 370a75a86..3afdcefae 100755 --- a/etc/integrate/dmt-integrate +++ b/etc/integrate/dmt-integrate @@ -108,25 +108,18 @@ if [ -n "$BUILD" ]; then if [ "$SYNC_ONLY" != true ]; then printf "${GREEN}Changing app_base in svelte configuration:${NC}\n" - # setBase - node $INTEGRATE/editBase.js "$APP_ROOT_DIR" $APP_BASE # build - pnpm build + BASE=$APP_BASE pnpm build + # npx vite build --base $APP_BASE # after build if [ $? -ne 0 ]; then # error - # resetBase - node $INTEGRATE/resetBase.js "$APP_ROOT_DIR" $APP_BASE - echo printf "${RED}Build error${NC}\n" exit fi - - # resetBase - node $INTEGRATE/resetBase.js "$APP_ROOT_DIR" $APP_BASE fi SOURCE_PUBLIC="${APP_ROOT_DIR}/${BUILD}" @@ -217,6 +210,7 @@ if [ "$SYNC_ONLY" != true ]; then if [ -f "./index.js" ] || [ -f "./dmt/index.js" ]; then # dmt restart + printf "${GREEN}curl http://127.0.0.1:7777/__dmt__reload?app=$DMT_APPS_TARGET/$APP_BASE ${NC}\n" curl "http://127.0.0.1:7777/__dmt__reload?app=$DMT_APPS_TARGET/$APP_BASE" echo "" fi diff --git a/etc/integrate/editBase.js b/etc/integrate/editBase.js deleted file mode 100644 index f0265bf4f..000000000 --- a/etc/integrate/editBase.js +++ /dev/null @@ -1,37 +0,0 @@ -import fs from 'fs'; - -import { join as pathJoin, basename } from 'path'; - -import colors from './colors.js'; - -const appBase = process.argv[3]; -const projectRoot = process.argv[2]; - -const base = pathJoin('/', appBase); - -const canEditRe = `paths\\:[\\ ]{[\\t\\n\\ ]*base:[\\ ]*\\'${base}\\'[\\t\\n\\ ]*\\}[\\ ]*\\,`; - -function edit(filePath) { - const re = /kit:[\ ]*{/; - const toAdd = `kit: { - paths: { - base: '${base}' - },`; - if (fs.existsSync(filePath)) { - let fileStr = fs.readFileSync(filePath, 'utf8'); - const canEdit = !RegExp(canEditRe).test(fileStr); - if (canEdit) { - fileStr = fileStr.replace(re, toAdd); - fs.writeFileSync(filePath, fileStr); - console.log( - `${colors.green('✓')} Changed app base to ${colors.green(base)} (file: ${colors.cyan(projectRoot)}${colors.cyan('/')}${colors.yellow( - basename(filePath) - )})` - ); - } else { - console.log(colors.yellow(`Correct app base was already present in ${colors.cyan(projectRoot)}${colors.cyan('/')}${basename(filePath)}`)); - } - } -} - -edit(pathJoin(projectRoot, 'svelte.config.js')); diff --git a/etc/integrate/resetBase.js b/etc/integrate/resetBase.js deleted file mode 100644 index 15cdc7094..000000000 --- a/etc/integrate/resetBase.js +++ /dev/null @@ -1,23 +0,0 @@ -import fs from 'fs'; -import { join as pathJoin } from 'path'; - -import colors from './colors.js'; - -const appBase = process.argv[3]; -const projectRoot = process.argv[2]; - -const base = pathJoin('/', appBase); - -const AddedConfig = `[\\t\\n\\ ]*paths\\:[\\ ]{[\\t\\n\\ ]*base:[\\ ]*\\'${base}\\'[\\t\\n\\ ]*\\}[\\ ]*\\,[\\t\\n\\ ]*`; - -const restore = filePath => { - if (fs.existsSync(filePath)) { - let svelteConfigFile = fs.readFileSync(filePath, 'utf8'); - svelteConfigFile = svelteConfigFile.replace(RegExp(AddedConfig), `\n\t`); - fs.writeFileSync(filePath, svelteConfigFile); - - console.log(colors.yellow('— Restored svelte.config.cjs')); - } -}; - -restore(pathJoin(projectRoot, 'svelte.config.js')); diff --git a/etc/scripts/prepare_apps_and_user_engine/dmt_apps/package.json b/etc/scripts/prepare_apps_and_user_engine/dmt_apps/package.json index 28ddc9206..1a1222af3 100644 --- a/etc/scripts/prepare_apps_and_user_engine/dmt_apps/package.json +++ b/etc/scripts/prepare_apps_and_user_engine/dmt_apps/package.json @@ -7,9 +7,10 @@ "exports": { "./common": "./_dmt_deps/common/index.js", "./notify": "./_dmt_deps/notify/index.js", - "./search": "./_dmt_deps/search/index.js", - "./connectome": "./_dmt_deps/connectome/index.js", - "./connectome-stores": "./_dmt_deps/connectome-stores/index.js", - "./connectome-next": "./_dmt_deps/connectome-next/index.js" + "./search": "./_dmt_deps/search/index.js" + }, + "devDependencies": { + "connectome": "file:~/.dmt/core/node/connectome", + "connectome-next": "file:~/.dmt/core/node/connectome-next" } } diff --git a/etc/scripts/prepare_apps_and_user_engine/dmt_user_engine/devDependencies.json b/etc/scripts/prepare_apps_and_user_engine/dmt_user_engine/devDependencies.json new file mode 100644 index 000000000..df3375ce2 --- /dev/null +++ b/etc/scripts/prepare_apps_and_user_engine/dmt_user_engine/devDependencies.json @@ -0,0 +1,3 @@ +{ + "connectome": "file:~/.dmt/core/node/connectome" +} diff --git a/etc/scripts/prepare_apps_and_user_engine/dmt_user_engine/exports.json b/etc/scripts/prepare_apps_and_user_engine/dmt_user_engine/exports.json index 6dd8ee051..6d207652a 100644 --- a/etc/scripts/prepare_apps_and_user_engine/dmt_user_engine/exports.json +++ b/etc/scripts/prepare_apps_and_user_engine/dmt_user_engine/exports.json @@ -1,8 +1,5 @@ { "./common": "./_dmt_deps/common/index.js", "./notify": "./_dmt_deps/notify/index.js", - "./iot": "./_dmt_deps/iot/index.js", - "./connectome": "./_dmt_deps/connectome/index.js", - "./connectome-server": "./_dmt_deps/connectome-server/index.js", - "./connectome-stores": "./_dmt_deps/connectome-stores/index.js" + "./iot": "./_dmt_deps/iot/index.js" } diff --git a/etc/scripts/prepare_apps_and_user_engine/dmt_user_engine/package.json b/etc/scripts/prepare_apps_and_user_engine/dmt_user_engine/package.json index bb70cf19e..ea6cfb2af 100644 --- a/etc/scripts/prepare_apps_and_user_engine/dmt_user_engine/package.json +++ b/etc/scripts/prepare_apps_and_user_engine/dmt_user_engine/package.json @@ -8,6 +8,7 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, + "devDependencies": {}, "author": "uniqpath", "license": "ISC" } diff --git a/etc/scripts/prepare_apps_and_user_engine/prepare_apps b/etc/scripts/prepare_apps_and_user_engine/prepare_apps index 20b761953..6d934b611 100755 --- a/etc/scripts/prepare_apps_and_user_engine/prepare_apps +++ b/etc/scripts/prepare_apps_and_user_engine/prepare_apps @@ -4,38 +4,45 @@ DMT_APPS="$HOME/.dmt/apps" DMT_USER_APPS="$HOME/.dmt/user/apps" DMT_DEVICE_APPS="$HOME/.dmt-here/apps" -mkdir -p "$DMT_APPS" -mkdir -p "$DMT_USER_APPS" -mkdir -p "$DMT_DEVICE_APPS" +function install_node_modules { + local cwd="`pwd`" + cd "$1" + local LOCK_FILE="package-lock.json" + if [ -f $LOCK_FILE ]; then + rm $LOCK_FILE + fi + npm install + cd "$cwd" +} + +function prepare { + local DIR="$1" + + mkdir -p "$DIR" + + if [ -f "$DIR/package.json" ]; then + if ! diff ./dmt_apps/package.json "$DIR/package.json" > /dev/null + then + cp ./dmt_apps/package.json "$DIR" + install_node_modules "$DIR" + fi + # if someone deleted node_modules ... + if [ ! -d "$DIR/node_modules" ]; then + install_node_modules "$DIR" + fi + else + cp ./dmt_apps/package.json "$DIR" + install_node_modules "$DIR" + fi +} # a) create _dmt_deps with symlinks to dmt node_modules + ./create_symlinks_apps -# b) copy related package.json into ~/.dmt/apps +# b) copy related package.json into system, user and device apps + +prepare "$DMT_APPS" +prepare "$DMT_USER_APPS" +prepare "$DMT_DEVICE_APPS" -if [ -f "$DMT_APPS/package.json" ]; then - if ! diff ./dmt_apps/package.json "$DMT_APPS/package.json" > /dev/null - then - cp ./dmt_apps/package.json "$DMT_APPS" - fi -else - cp ./dmt_apps/package.json "$DMT_APPS" -fi - -if [ -f "$DMT_USER_APPS/package.json" ]; then - if ! diff ./dmt_apps/package.json "$DMT_USER_APPS/package.json" > /dev/null - then - cp ./dmt_apps/package.json "$DMT_USER_APPS" - fi -else - cp ./dmt_apps/package.json "$DMT_USER_APPS" -fi - -if [ -f "$DMT_DEVICE_APPS/package.json" ]; then - if ! diff ./dmt_apps/package.json "$DMT_DEVICE_APPS/package.json" > /dev/null - then - cp ./dmt_apps/package.json "$DMT_DEVICE_APPS" - fi -else - cp ./dmt_apps/package.json "$DMT_DEVICE_APPS" -fi diff --git a/etc/scripts/prepare_apps_and_user_engine/prepare_user_engine b/etc/scripts/prepare_apps_and_user_engine/prepare_user_engine index 2ae04fb01..357124031 100755 --- a/etc/scripts/prepare_apps_and_user_engine/prepare_user_engine +++ b/etc/scripts/prepare_apps_and_user_engine/prepare_user_engine @@ -10,6 +10,9 @@ if [ ! -f "$DMT_USER_ENGINE/package.json" ]; then cp ./dmt_user_engine/package.json "$DMT_USER_ENGINE" fi +# ⚠️ user cannot have anything in devDependencies and exports in ~/.dmt/user/engine/package.json +# + USER_ENGINE_ENTRY="$DMT_USER_ENGINE/index.js" # ⚠️ TODO: remove this soon diff --git a/shell/.bash_dep b/shell/.bash_dep index c8ff84476..8bc684928 100644 --- a/shell/.bash_dep +++ b/shell/.bash_dep @@ -284,6 +284,8 @@ function dep { if [ -f ./truffle.js ]; then truffle deploy --reset # deploy target (manual from cli or from ./.deploy file) + elif [ -f ./deploy ]; then + ./deploy else dep_rsync "$@" fi @@ -380,7 +382,7 @@ function dirsync { printf "${YELLOW}Usage:${NC}\n\n" printf "${GREEN}dirsync dir1 dir2${NC}\n" printf "${GREEN}dirsync --dry dir1 dir2${NC} ${GRAY}(simulate)${NC}\n" - printf "${GREEN}dirsync --total${NC} ${GRAY}(ignoring every .deployignore file and copying everything)${NC}\n" + printf "${GREEN}dirsync --total${NC} ${GRAY}(ignore every .deployignore file and copy everything)${NC}\n" printf "${GREEN}dirsync --exclude dir1 --exclude dir2${NC} ${GRAY}(exclude some dirs)${NC}\n" printf "${GREEN}dirsync --compress dir1 dir2${NC} ${GRAY}(compress - use over internet, but not LAN, see https://unix.stackexchange.com/questions/188737/does-compression-option-z-with-rsync-speed-up-backup)${NC}\n" printf "${GREEN}dirsync --checksum dir1 dir2${NC} ${GRAY}(use when timestamps on target are different but contents is likely same... This will sync & equalize timestamps, it's slow though but still probably faster than copying files over for no reason)${NC}\n" @@ -523,6 +525,18 @@ function dirsync { return fi + if dmt_macos; then + # these nasty files even if "--excluded" can prevent directory deletions on target + # exclusion means the files are not copied over but if they already exist they are not deleted + # (unless --delete-excluded option is used which we cannot do because that will delete everything that we are excluding!) + if [ -d "$TARGET" ]; then + local cwd="`pwd`" + cd "$TARGET" + remove_ds_store "silent" + cd "$cwd" + fi + fi + printf "${GREEN}Syncing... ${GRAY}${SOURCE} ${CYAN}→ ${GRAY}${TARGET}${NC}\n" local e_params="ssh" @@ -578,6 +592,11 @@ function dirsync { params="${params} --exclude-from $SOURCE/.deployignore" fi + # echo $params + # echo $e_params + # echo $excludes + # return + rsync $params $excludes -e "$e_params" "$SOURCE"/ "$TARGET"/ 2>&1 | grep -v "$grep_ignore" local exitStatus=${PIPESTATUS[0]} diff --git a/shell/.bash_dmt b/shell/.bash_dmt index 7acd9af99..e5bafc230 100644 --- a/shell/.bash_dmt +++ b/shell/.bash_dmt @@ -801,7 +801,7 @@ function dmt { # delete older than 7 days find isolate*.log -mtime +7 -exec rm {} \; - $DMT_NODEJS_EXEC --trace-warnings --prof "${DMT_NODE_CORE}/controller/processes/dmt-proc.js" --fg --profile # --fg: only for informative purposes to signal that we ran it in foreground as opposed to daemonizing (dmt start) + $DMT_NODEJS_EXEC --trace-warnings --prof "${DMT_NODE_CORE}/controller/processes/dmt-proc.js" --fg --profile # --fg: only for informative purposes to signal that we ran it in foreground as opposed to daemonizing (dmt start) echo printf "${GRAY}Now please open ${CYAN}https://nodejs.org/es/docs/guides/simple-profiling/ ${GRAY}for further instructions...${NC}\n" @@ -820,7 +820,8 @@ function dmt { # we need FORCE_COLOR and TERM here only for when dmt will spawn abc-proc # if we don't do it then abc log will not have colors # rel && killall abc-proc && d run - FORCE_COLOR=true TERM=xterm-256color $DMT_NODEJS_EXEC --trace-warnings "${DMT_NODE_CORE}/controller/processes/dmt-proc.js" --fg # --fg: only for informative purposes to signal that we ran it in foreground as opposed to daemonizing (dmt start) + FORCE_COLOR=true TERM=xterm-256color $DMT_NODEJS_EXEC --trace-warnings "${DMT_NODE_CORE}/controller/processes/dmt-proc.js" --fg + # --fg: only for informative purposes to signal that we ran it in foreground as opposed to daemonizing (dmt start) fi if [[ $? -eq 1 ]]; then diff --git a/shell/.bash_short_useful b/shell/.bash_short_useful index e3de3076c..83c7aa161 100644 --- a/shell/.bash_short_useful +++ b/shell/.bash_short_useful @@ -65,6 +65,10 @@ function d { dmt "$@" } +function din { + dmt integrate +} + function u { if [ -n "$2" ]; then dmt update --parallel "$@" diff --git a/shell/.bash_util b/shell/.bash_util index eae09e050..2fbd213e5 100644 --- a/shell/.bash_util +++ b/shell/.bash_util @@ -46,6 +46,13 @@ function dmt_macos { return 1 # false } +function remove_ds_store { + find . -name ".DS_Store" -depth -exec rm {} \; + if [ "$1" != 'silent' ]; then + printf "${GREEN}✓ Done.${NC}\n" + fi +} + function dmt_is_linux { if [[ $OSTYPE == linux* ]]; then return 0 # true From 51dc0c3f9a017d243275442e0d7aa30cdc14049a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Jul 2023 10:32:38 +0000 Subject: [PATCH 2/2] Bump semver from 5.7.1 to 5.7.2 in /core/node Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2. - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md) - [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2) --- updated-dependencies: - dependency-name: semver dependency-type: indirect ... Signed-off-by: dependabot[bot] --- core/node/package-lock.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/node/package-lock.json b/core/node/package-lock.json index 91a227369..3cd24e9c9 100644 --- a/core/node/package-lock.json +++ b/core/node/package-lock.json @@ -2207,7 +2207,7 @@ }, "node_modules/readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dependencies": { "core-util-is": "~1.0.0", @@ -2326,9 +2326,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { "semver": "bin/semver" } @@ -4797,7 +4797,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -4900,9 +4900,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" }, "send": { "version": "0.17.1",