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 499277c2f..b1c1772ab 100755 Binary files a/core/node/connectome/node_modules/bufferutil/build/Release/bufferutil.node and b/core/node/connectome/node_modules/bufferutil/build/Release/bufferutil.node differ diff --git a/core/node/connectome/node_modules/bufferutil/build/Release/obj.target/bufferutil/src/bufferutil.o b/core/node/connectome/node_modules/bufferutil/build/Release/obj.target/bufferutil/src/bufferutil.o index f96889e34..e84ad5868 100644 Binary files a/core/node/connectome/node_modules/bufferutil/build/Release/obj.target/bufferutil/src/bufferutil.o and b/core/node/connectome/node_modules/bufferutil/build/Release/obj.target/bufferutil/src/bufferutil.o differ diff --git a/core/node/connectome/node_modules/bufferutil/build/bufferutil.target.mk b/core/node/connectome/node_modules/bufferutil/build/bufferutil.target.mk index e4d897730..915e4d12a 100644 --- a/core/node/connectome/node_modules/bufferutil/build/bufferutil.target.mk +++ b/core/node/connectome/node_modules/bufferutil/build/bufferutil.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=bufferutil' \ @@ -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/bufferutil.o diff --git a/core/node/connectome/node_modules/bufferutil/build/config.gypi b/core/node/connectome/node_modules/bufferutil/build/config.gypi index ca5f2eb6c..b3dc02373 100644 --- a/core/node/connectome/node_modules/bufferutil/build/config.gypi +++ b/core/node/connectome/node_modules/bufferutil/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/Makefile b/core/node/connectome/node_modules/utf-8-validate/build/Makefile index 1fe1f214c..ecda655cb 100644 --- a/core/node/connectome/node_modules/utf-8-validate/build/Makefile +++ b/core/node/connectome/node_modules/utf-8-validate/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/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 0792e31ff..db831b67e 100644 Binary files a/core/node/connectome/node_modules/utf-8-validate/build/Release/obj.target/validation/src/validation.o and b/core/node/connectome/node_modules/utf-8-validate/build/Release/obj.target/validation/src/validation.o differ 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 b0559a657..26ba5e771 100755 Binary files a/core/node/connectome/node_modules/utf-8-validate/build/Release/validation.node and b/core/node/connectome/node_modules/utf-8-validate/build/Release/validation.node differ 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 000000000..824132dd5 Binary files /dev/null and b/core/node/node_modules/bufferutil/prebuilds/darwin-x64+arm64/node.napi.node differ diff --git a/core/node/node_modules/bufferutil/prebuilds/linux-x64/node.napi.node b/core/node/node_modules/bufferutil/prebuilds/linux-x64/node.napi.node new file mode 100644 index 000000000..b96d32d2f Binary files /dev/null and b/core/node/node_modules/bufferutil/prebuilds/linux-x64/node.napi.node differ 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 000000000..aea0bf30a Binary files /dev/null and b/core/node/node_modules/bufferutil/prebuilds/win32-ia32/node.napi.node differ 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 000000000..dd47faa46 Binary files /dev/null and b/core/node/node_modules/bufferutil/prebuilds/win32-x64/node.napi.node differ diff --git a/core/node/node_modules/bufferutil/src/bufferutil.c b/core/node/node_modules/bufferutil/src/bufferutil.c new file mode 100644 index 000000000..b89f8dd17 --- /dev/null +++ b/core/node/node_modules/bufferutil/src/bufferutil.c @@ -0,0 +1,171 @@ +#define NAPI_VERSION 1 +#include +#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 000000000..bed98d4a4 Binary files /dev/null and b/core/node/node_modules/utf-8-validate/prebuilds/darwin-x64+arm64/node.napi.node differ 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 000000000..bf547d8a7 Binary files /dev/null and b/core/node/node_modules/utf-8-validate/prebuilds/linux-x64/node.napi.node differ 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 000000000..bb40b239d Binary files /dev/null and b/core/node/node_modules/utf-8-validate/prebuilds/win32-ia32/node.napi.node differ diff --git a/core/node/node_modules/utf-8-validate/prebuilds/win32-x64/node.napi.node b/core/node/node_modules/utf-8-validate/prebuilds/win32-x64/node.napi.node new file mode 100644 index 000000000..861d66126 Binary files /dev/null and b/core/node/node_modules/utf-8-validate/prebuilds/win32-x64/node.napi.node differ 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..3cd24e9c9 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", @@ -2164,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", @@ -2283,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" } @@ -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", @@ -4708,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", @@ -4811,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", @@ -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