@gjsify/tls 0.52.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/esm/connect.js +1 -1
- package/lib/esm/index.js +1 -1
- package/lib/esm/internal/hostname.js +1 -1
- package/lib/esm/tls-server.js +1 -1
- package/lib/esm/tls-socket.js +1 -1
- package/lib/esm/verify.js +1 -0
- package/lib/types/connect.d.ts +8 -2
- package/lib/types/given-socket.spec.d.ts +2 -0
- package/lib/types/starttls-upgrade.gjs.spec.d.ts +2 -0
- package/lib/types/tls-server.d.ts +20 -1
- package/lib/types/tls-socket.d.ts +163 -2
- package/lib/types/verify.d.ts +37 -0
- package/package.json +14 -7
package/lib/esm/connect.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{TLSSocket as e}from"./tls-socket.js";function connect(t,n){let r=t.socket,i=new e(r,t);if(n&&i.once(`secureConnect`,n),!r){let e=t.port||443,n=t.host||`localhost`;i.servername=t.servername||n,i._awaitingSecure=!0,i.once(`connect`,()=>i._performHandshake(t)),i.connect({port:e,host:n})}return i}export{connect};
|
package/lib/esm/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{checkServerIdentity as e}from"./internal/hostname.js";import{
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{checkServerIdentity as e}from"./internal/hostname.js";import{createSecureContext as t}from"./secure-context.js";import{TlsChannelBindingType as n,hasTlsSessionAccess as r}from"./session-access.js";import{TLSSocket as i}from"./tls-socket.js";import{connect as a}from"./connect.js";import{TLSServer as o,createServer as s}from"./tls-server.js";import{OcspCertStatus as c,OcspResponseStatus as l,hasOcspSupport as u,parseOcspResponse as d}from"./ocsp.js";const f=`TLSv1.2`,p=`TLSv1.3`,m=`TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384`;function getCiphers(){return[`aes-128-gcm`,`aes-256-gcm`,`chacha20-poly1305`,`aes-128-cbc`,`aes-256-cbc`]}const h=[],g={TLSSocket:i,TLSServer:o,Server:o,connect:a,createServer:s,createSecureContext:t,checkServerIdentity:e,getCiphers,rootCertificates:h,DEFAULT_MIN_VERSION:f,DEFAULT_MAX_VERSION:p,DEFAULT_CIPHERS:m,parseOcspResponse:d,hasOcspSupport:u,OcspCertStatus:c,OcspResponseStatus:l,hasTlsSessionAccess:r,TlsChannelBindingType:n};export{m as DEFAULT_CIPHERS,p as DEFAULT_MAX_VERSION,f as DEFAULT_MIN_VERSION,c as OcspCertStatus,l as OcspResponseStatus,o as Server,o as TLSServer,i as TLSSocket,n as TlsChannelBindingType,e as checkServerIdentity,a as connect,t as createSecureContext,s as createServer,g as default,getCiphers,u as hasOcspSupport,r as hasTlsSessionAccess,d as parseOcspResponse,h as rootCertificates};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import"../_virtual/_rolldown/runtime.js";function unfqdn(e){return e.endsWith(`.`)?e.slice(0,-1):e}function splitHost(e){return unfqdn(e).toLowerCase().split(`.`)}function isPrintableAscii(e){for(let t=0;t<e.length;t++){let n=e.charCodeAt(t);if(n<33||n>126)return!1}return!0}function checkHostMatch(e,t){if(!t)return!1;let n=splitHost(t);if(e.length!==n.length||n.includes(``)||!n.every(isPrintableAscii))return!1;for(let t=e.length-1;t>0;t--)if(e[t]!==n[t])return!1;let r=e[0],i=n[0],a=i.split(`*`,3);if(a.length===1||i.includes(`xn--`))return r===i;if(a.length>2||n.length<=2)return!1;let o=a[0],s=a[1];return!(o.length+s.length>r.length||!r.startsWith(o)||!r.endsWith(s))}function checkServerIdentity(e,t){let n=t.subject,r=t.subjectaltname,i=[],a=[];if(e=String(e),r){let e=r.split(`, `);for(let t of e)t.startsWith(`DNS:`)?i.push(t.slice(4)):t.startsWith(`IP Address:`)&&a.push(t.slice(11).trim())}let o=!1,s=`Unknown reason`;e=unfqdn(e);let c=/^(\d{1,3}\.){3}\d{1,3}$/.test(e),l=e.includes(`:`);if(c||l)o=a.some(t=>t.toLowerCase()===e.toLowerCase()),o||(s=`IP: ${e} is not in the cert's list: ${a.join(`, `)}`);else if(i.length>0||n?.CN){let t=splitHost(e);if(i.length>0)o=i.some(e=>checkHostMatch(t,e.trim())),o||(s=`Host: ${e}. is not in the cert's altnames: ${r}`);else{let r=n?.CN;Array.isArray(r)?o=r.some(e=>checkHostMatch(t,e)):r&&(o=checkHostMatch(t,r)),o||(s=`Host: ${e}. is not cert's CN: ${r}`)}}else s=`Cert does not contain a DNS name`;if(!o){let n=Error(s);return n.reason=s,n.host=e,n.cert=t,n.code=`ERR_TLS_CERT_ALTNAME_INVALID`,n}}export{checkHostMatch,checkServerIdentity,isPrintableAscii,splitHost,unfqdn};
|
|
1
|
+
import"../_virtual/_rolldown/runtime.js";function unfqdn(e){return e.endsWith(`.`)?e.slice(0,-1):e}function splitHost(e){return unfqdn(e).toLowerCase().split(`.`)}function isPrintableAscii(e){for(let t=0;t<e.length;t++){let n=e.charCodeAt(t);if(n<33||n>126)return!1}return!0}function checkHostMatch(e,t){if(!t)return!1;let n=splitHost(t);if(e.length!==n.length||n.includes(``)||!n.every(isPrintableAscii))return!1;for(let t=e.length-1;t>0;t--)if(e[t]!==n[t])return!1;let r=e[0],i=n[0],a=i.split(`*`,3);if(a.length===1||i.includes(`xn--`))return r===i;if(a.length>2||n.length<=2)return!1;let o=a[0],s=a[1];return!(o.length+s.length>r.length||!r.startsWith(o)||!r.endsWith(s))}function checkServerIdentity(e,t){let n=t.subject,r=t.subjectaltname,i=[],a=[];if(e=String(e),r){let e=r.split(`, `);for(let t of e)t.startsWith(`DNS:`)?i.push(t.slice(4)):t.startsWith(`IP Address:`)&&a.push(t.slice(11).trim())}let o=!1,s=`Unknown reason`;e=unfqdn(e);let c=/^(\d{1,3}\.){3}\d{1,3}$/.test(e),l=e.includes(`:`);if(c||l)o=a.some(t=>t.toLowerCase()===e.toLowerCase()),o||(s=`IP: ${e} is not in the cert's list: ${a.join(`, `)}`);else if(i.length>0||n?.CN){let t=splitHost(e);if(i.length>0)o=i.some(e=>checkHostMatch(t,e.trim())),o||(s=`Host: ${e}. is not in the cert's altnames: ${r}`);else{let r=n?.CN;Array.isArray(r)?o=r.some(e=>checkHostMatch(t,e)):r&&(o=checkHostMatch(t,r)),o||(s=`Host: ${e}. is not cert's CN: ${r}`)}}else s=`Cert does not contain a DNS name`;if(!o){let n=Error(`Hostname/IP does not match certificate's altnames: ${s}`);return n.reason=s,n.host=e,n.cert=t,n.code=`ERR_TLS_CERT_ALTNAME_INVALID`,n}}export{checkHostMatch,checkServerIdentity,isPrintableAscii,splitHost,unfqdn};
|
package/lib/esm/tls-server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{checkHostMatch as e,splitHost as t}from"./internal/hostname.js";import{
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{checkHostMatch as e,splitHost as t}from"./internal/hostname.js";import{createSecureContext as n}from"./secure-context.js";import{TLSSocket as r,claimGjsifyConnection as i,foreignSocketError as a}from"./tls-socket.js";import{parseClientHelloSni as o}from"./internal/sni-parser.js";import s from"@girs/gio-2.0";import c from"@girs/glib-2.0";import{Server as l}from"node:net";import{createNodeError as u,deferEmit as d}from"@gjsify/utils/core";var TLSServer=class extends l{_tlsCertificate=null;_tlsOptions;_sniContexts=new Map;_secureContext;constructor(e,t){super(),this._tlsOptions=e??{},this._secureContext=n(this._tlsOptions),this._tlsCertificate=this._secureContext.certificate,t&&this.on(`secureConnection`,t),this._tlsOptions.cert&&!this._tlsCertificate&&d(this,`error`,u(Error(`Failed to parse TLS certificate`),`createServer`,{}))}addContext(e,t){try{let r=n(t);this._sniContexts.set(e.toLowerCase(),r)}catch(e){this.emit(`error`,u(e,`addContext`,{}))}}_resolveSniContext(n,r){let i=this._secureContext;if(!n){r(i);return}let a=n.toLowerCase(),o=this._sniContexts.get(a);if(o){r(o);return}let s=t(a);for(let[t,n]of this._sniContexts)if(e(s,t)){r(n);return}if(this._tlsOptions.SNICallback)try{this._tlsOptions.SNICallback(n,(e,t)=>{if(e||!t){r(i);return}r(t)});return}catch{r(i);return}r(i)}listen(...e){return this.on(`connection`,e=>{this._upgradeTls(e)}),super.listen(...e)}_upgradeTls(e){let t=i(e);if(!t){this.emit(`tlsClientError`,a(),e),e.destroy();return}let n=t.connection;if(!n){let t=Error(`Cannot upgrade socket: no underlying connection`);this.emit(`tlsClientError`,t,e),e.destroy();return}let closeClaimedConnection=()=>{try{(t.connection??t.ioStream)?.close(null)}catch{}};if(!this._tlsCertificate&&this._sniContexts.size===0&&!this._tlsOptions.SNICallback){let t=Error(`TLS server has no certificate configured`);this.emit(`tlsClientError`,t,e),closeClaimedConnection(),e.destroy();return}let r=n;if(this._sniContexts.size===0&&!this._tlsOptions.SNICallback){let t=this._tlsCertificate;if(!t){let t=Error(`TLS server has no certificate configured`);this.emit(`tlsClientError`,t,e),closeClaimedConnection(),e.destroy();return}this._completeUpgrade(r,t,this._secureContext,e,closeClaimedConnection);return}let l=r.get_input_stream(),u=r.get_output_stream(),d=s.BufferedInputStream.new_sized(l,4096);d.fill_async(4096,c.PRIORITY_DEFAULT,null,(t,n)=>{let r=null;try{d.fill_finish(n),r=o(d.peek_buffer())}catch{}this._resolveSniContext(r,t=>{let n=t.certificate??this._tlsCertificate;if(!n){let t=Error(`SNI resolution returned no certificate`);this.emit(`tlsClientError`,t,e),closeClaimedConnection(),e.destroy();return}let r=new s.SimpleIOStream({inputStream:d,outputStream:u});this._completeUpgrade(r,n,t,e,closeClaimedConnection)})})}_completeUpgrade(e,t,n,i,a){try{let o=s.TlsServerConnection.new(e,t);this._tlsOptions.requestCert?o.authenticationMode=this._tlsOptions.rejectUnauthorized===!1?s.TlsAuthenticationMode.REQUESTED:s.TlsAuthenticationMode.REQUIRED:o.authenticationMode=s.TlsAuthenticationMode.NONE;let l=!!this._tlsOptions.requestCert&&this._tlsOptions.rejectUnauthorized!==!1,d=this._secureContext.caCertificates;o.connect(`accept-certificate`,(e,t,n)=>{if(!l)return!0;if(d.length===0)return!1;for(let e of d)try{if(t.verify(null,e)===s.TlsCertificateFlags.NO_FLAGS)return!0}catch{}return!1}),this._tlsOptions.ALPNProtocols&&this._tlsOptions.ALPNProtocols.length>0&&o.set_advertised_protocols(this._tlsOptions.ALPNProtocols);let f=new s.Cancellable;o.handshake_async(c.PRIORITY_DEFAULT,f,(e,t)=>{try{o.handshake_finish(t);let e=new r;e.encrypted=!0,e.authorized=!0,e._secureContext=n,e._setupTlsStreams(o),e.alpnProtocol=e.getAlpnProtocol(),e.once(`close`,()=>i.destroy()),e._startReading(),this.emit(`secureConnection`,e)}catch(e){let t=u(e,`handshake`,{});this.emit(`tlsClientError`,t,i),a(),i.destroy()}})}catch(e){let t=u(e,`tls_wrap`,{});this.emit(`tlsClientError`,t,i),a(),i.destroy()}}};function createServer(e,t){return typeof e==`function`?new TLSServer(void 0,e):new TLSServer(e,t)}export{TLSServer,createServer};
|
package/lib/esm/tls-socket.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{tlsCertToPeerCert as e}from"./internal/cert-utils.js";import{createSessionAccess as
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{tlsCertToPeerCert as e}from"./internal/cert-utils.js";import{createSecureContext as t}from"./secure-context.js";import{createSessionAccess as n,hasTlsSessionAccess as r}from"./session-access.js";import i from"@girs/gio-2.0";import a from"@girs/glib-2.0";import{Buffer as o}from"node:buffer";import{Socket as s}from"node:net";import c from"node:process";function claimGjsifyConnection(e){let t=e;return typeof t?._claimConnection==`function`?t._claimConnection():null}var TLSSocket=class extends s{encrypted=!0;authorized=!1;authorizationError;alpnProtocol=!1;servername;_tlsConnection=null;_secureContext=null;_sessionAccess=null;_adoptedSocket=null;_awaitingSecure=!1;_afterSecure=[];_handshakeClaim=null;_certRejection=null;constructor(e,t){super(),e&&this._startClient(e,t??{})}_claimSocketStreams(e){let t=claimGjsifyConnection(e);return t||this.destroy(foreignSocketError()),t}_startClient(e,t){this.servername=t.servername||t.host||`localhost`,this._adoptedSocket=e,this._awaitingSecure=!0,this.once(`close`,()=>{e.destroyed||e.destroy()});let begin=()=>{this._adoptConnection(e).then(e=>{e&&this._performHandshake(t)})};e.connecting?e.once(`connect`,begin):c.nextTick(begin)}async _adoptConnection(e){if(this.destroyed||e.destroyed)return!1;let t=e;if(typeof t._detachReader!=`function`)return this.destroy(foreignSocketError()),!1;let n=this._claimSocketStreams(t);if(!n)return!1;let r=await t._detachReader();if(this.destroyed){try{(n.connection??n.ioStream)?.close(null)}catch{}return!1}if(r&&r.length>0){this.destroy(_upgradeRaceError());try{(n.connection??n.ioStream)?.close(null)}catch{}return!1}let i=this;i._connection=n.connection,i._ioStream=n.ioStream,i._inputStream=n.inputStream,i._outputStream=n.outputStream;let a=this,o=e;return a.remoteAddress=o.remoteAddress,a.remotePort=o.remotePort,a.remoteFamily=o.remoteFamily,a.localAddress=o.localAddress,a.localPort=o.localPort,a.connecting=!1,a.pending=!1,!0}_performHandshake(e){let n=this.servername||e.servername||e.host||`localhost`;this.servername=n;let o=e.port||this.remotePort||443,s=e.rejectUnauthorized!==!1,c=e.secureContext??t(e);this._secureContext=c;let l=e.checkServerIdentity,u=this,d=this._claimSocketStreams(u);if(!d)return;let f=d.connection;if(!f){this.destroy(Error(`No underlying connection for TLS upgrade`));return}this._handshakeClaim=d;let fail=e=>{this.destroy(e instanceof Error?e:Error(String(e)))};try{let t=i.NetworkAddress.new(n,o),p=i.TlsClientConnection.new(f,t);if(p.set_server_identity(t),e.session&&r())try{this._tlsConnection=p,this.setSession(e.session)}catch{}if(c.certificate)try{p.set_certificate(c.certificate)}catch(e){console.warn(`[tls] failed to set client certificate:`,e)}e.ALPNProtocols&&e.ALPNProtocols.length>0&&p.set_advertised_protocols(e.ALPNProtocols),p.connect(`accept-certificate`,(e,r,a)=>{if(!s)return!0;let o=a;for(let e of c.caCertificates)try{if(o=r.verify(t,e),o===i.TlsCertificateFlags.NO_FLAGS)return!0}catch{}return this._certRejection=_certRejection(r,o,n),!1}),u._pendingIo++,p.handshake_async(a.PRIORITY_DEFAULT,u._cancellable,(e,t)=>{let r=null;try{p.handshake_finish(t)}catch(e){r=e}if(this.destroyed){u._ioSettled();return}if(this._handshakeClaim=null,u._ioSettled(),r){this.authorized=!1;let e=this._certRejection,t=e?Object.assign(Error(e.message),{code:e.code}):r;this.authorizationError=t instanceof Error?t.message:String(t),u._connection=d.connection,u._ioStream=d.ioStream,fail(t);return}this._secureEstablished(p,n,s,l)})}catch(e){fail(e)}}_secureEstablished(e,t,n,i){if(this.authorized=!0,this._setupTlsStreams(e),this.alpnProtocol=this.getAlpnProtocol(),i){let e=i(t,this.getPeerCertificate());if(e&&(this.authorized=!1,this.authorizationError=e.message,n)){this.destroy(e);return}}let a=this;if(a._reading=!1,a._startReading(),r()){let e=this.getSession();e&&this.emit(`session`,e)}this.emit(`secureConnect`),this._flushAfterSecure()}_write(e,t,n){if(this._awaitingSecure){this._afterSecure.push({run:()=>super._write(e,t,n),abort:()=>n(_destroyedError())});return}super._write(e,t,n)}_flushAfterSecure(){let e=this._afterSecure;this._afterSecure=[];for(let t of e)t.run()}_destroy(e,t){let n=this._handshakeClaim;if(n){this._handshakeClaim=null;let e=this;e._connection=n.connection,e._ioStream=n.ioStream}let r=this._afterSecure;this._afterSecure=[];for(let e of r)e.abort();super._destroy(e,t)}_final(e){if(this._awaitingSecure){this._afterSecure.push({run:()=>this._final(e),abort:()=>{}});return}let t=this._tlsConnection;if(!t){super._final(e);return}let n=this,r=t.get_output_stream();n._pendingIo++,r.close_async(a.PRIORITY_DEFAULT,null,(t,i)=>{try{r.close_finish(i)}catch{}n._ioSettled(),e()})}_setupTlsStreams(e){this._awaitingSecure=!1,this._tlsConnection=e;let t=this;t._inputStream=e.get_input_stream(),t._outputStream=e.get_output_stream(),t._connection=e}_getSessionAccess(){if(this._sessionAccess)return this._sessionAccess;if(!this._tlsConnection)return null;try{this._sessionAccess=n(this._tlsConnection)}catch{this._sessionAccess=null}return this._sessionAccess}getPeerCertificate(t=!1){if(!this._tlsConnection)return{};try{let n=this._tlsConnection.get_peer_certificate();return n?e(n,t):{}}catch{return{}}}getProtocol(){if(!this._tlsConnection)return null;switch(this._tlsConnection.get_protocol_version()){case i.TlsProtocolVersion.TLS_1_0:return`TLSv1`;case i.TlsProtocolVersion.TLS_1_1:return`TLSv1.1`;case i.TlsProtocolVersion.TLS_1_2:return`TLSv1.2`;case i.TlsProtocolVersion.TLS_1_3:return`TLSv1.3`;default:return null}}getCipher(){return this._tlsConnection?{name:this._tlsConnection.get_ciphersuite_name()||`unknown`,version:this.getProtocol()||`unknown`}:null}getAlpnProtocol(){return this._tlsConnection&&this._tlsConnection.get_negotiated_protocol()||!1}getFinished(){if(!r())return;let e=this._getSessionAccess();if(e)try{return _bytesToBuffer(e.get_finished())}catch{return}}getPeerFinished(){if(!r())return;let e=this._getSessionAccess();if(e)try{return _bytesToBuffer(e.get_peer_finished())}catch{return}}getSession(){if(!r())return;let e=this._getSessionAccess();if(e)try{return _bytesToBuffer(e.get_session_data())}catch{return}}setSession(e){if(!r())return;let t=this._getSessionAccess();if(t)try{t.set_session_data(_bufferToBytes(e))}catch{}}isSessionReused(){if(!r())return!1;let e=this._getSessionAccess();if(!e)return!1;try{return e.is_session_reused()}catch{return!1}}};function _destroyedError(){let e=Error(`Cannot call write after a stream was destroyed`);return e.code=`ERR_STREAM_DESTROYED`,e}function _certRejection(e,t,n){return t&i.TlsCertificateFlags.BAD_IDENTITY?{code:`ERR_TLS_CERT_ALTNAME_INVALID`,message:`Hostname/IP does not match certificate's altnames: Host: ${n}. is not in the cert's altnames`}:t&i.TlsCertificateFlags.EXPIRED?{code:`CERT_HAS_EXPIRED`,message:`certificate has expired`}:t&i.TlsCertificateFlags.NOT_ACTIVATED?{code:`CERT_NOT_YET_VALID`,message:`certificate is not yet valid`}:e.subjectName&&e.subjectName===e.issuerName?{code:`DEPTH_ZERO_SELF_SIGNED_CERT`,message:`self-signed certificate`}:{code:`UNABLE_TO_VERIFY_LEAF_SIGNATURE`,message:`unable to verify the first certificate`}}function _upgradeRaceError(){let e=Error(`tls: data arrived on the socket before the TLS handshake could take over reading its input stream — Gio.TlsClientConnection has no API to replay already-read bytes into the handshake. This should not happen for a well-behaved STARTTLS peer.`);return e.code=`ERR_GJSIFY_TLS_UPGRADE_RACE`,e}function foreignSocketError(){let e=Error(`tls.connect({socket}) / new tls.TLSSocket(socket, …) can only upgrade a @gjsify/net Socket today — the given socket doesn't carry the Gio connection needed to build a Gio.TlsClientConnection. Generic Duplex support (Node's real contract) is tracked in status/open-todos/README.md.`);return e.code=`ERR_GJSIFY_TLS_FOREIGN_SOCKET`,e}function _bytesToBuffer(e){if(e==null)return;if(e instanceof Uint8Array)return o.from(e);let t=e;if(typeof t.toArray==`function`)try{return o.from(t.toArray())}catch{return}}function _bufferToBytes(e){return e}export{TLSSocket,claimGjsifyConnection,foreignSocketError};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{checkServerIdentity as e}from"./internal/hostname.js";import{tlsCertToPeerCert as t}from"./internal/cert-utils.js";import{createSecureContext as n}from"./secure-context.js";import r from"@girs/gio-2.0";import i from"@girs/glib-2.0";function resolveRejectUnauthorized(e){return e===void 0?i.getenv(`NODE_TLS_REJECT_UNAUTHORIZED`)!==`0`:e!==!1}function createPeerVerifier(n){let r=resolveRejectUnauthorized(n.rejectUnauthorized),i=n.checkServerIdentity??e,a=n.caCertificates??[],o=null;return{accept(e){return o=chainError(e,a)??i(n.host,t(e,!0))??null,o===null||!r},get error(){return o},get authorized(){return o===null},get authorizationError(){if(o)return o.code??o.message}}}const a=r.TlsCertificateFlags.VALIDATE_ALL&~r.TlsCertificateFlags.BAD_IDENTITY;function chainFlags(e,t){if(t.length===0)return r.TlsBackend.get_default().get_default_database().verify_chain(e,r.TLS_DATABASE_PURPOSE_AUTHENTICATE_SERVER,null,null,r.TlsDatabaseVerifyFlags.NONE,null);let n=r.TlsCertificateFlags.UNKNOWN_CA;for(let i of t){let t=e.verify(null,i)&a;if(t===r.TlsCertificateFlags.NO_FLAGS)return t;t&r.TlsCertificateFlags.UNKNOWN_CA||(n=t)}return n}function isSelfSigned(e){return e.get_subject_name()===e.get_issuer_name()}function verifyError(e,t){let n=Error(t);return n.code=e,n}function chainError(e,t=[]){let n=chainFlags(e,t)&a,i=r.TlsCertificateFlags;if(n===i.NO_FLAGS)return null;if(n&i.UNKNOWN_CA){if(isSelfSigned(e))return verifyError(`DEPTH_ZERO_SELF_SIGNED_CERT`,`self-signed certificate`);let t=e;for(let e=t.get_issuer();e&&!e.is_same(t);e=t.get_issuer())t=e;return t===e?verifyError(`UNABLE_TO_VERIFY_LEAF_SIGNATURE`,`unable to verify the first certificate`):isSelfSigned(t)?verifyError(`SELF_SIGNED_CERT_IN_CHAIN`,`self-signed certificate in certificate chain`):verifyError(`UNABLE_TO_GET_ISSUER_CERT_LOCALLY`,`unable to get local issuer certificate`)}return n&i.REVOKED?verifyError(`CERT_REVOKED`,`certificate revoked`):n&i.EXPIRED?verifyError(`CERT_HAS_EXPIRED`,`certificate has expired`):n&i.NOT_ACTIVATED?verifyError(`CERT_NOT_YET_VALID`,`certificate is not yet valid`):n&i.INSECURE?verifyError(`CA_MD_TOO_WEAK`,`CA signature digest algorithm too weak`):verifyError(`CERT_REJECTED`,`certificate rejected`)}export{chainError,createPeerVerifier,n as createSecureContext,resolveRejectUnauthorized};
|
package/lib/types/connect.d.ts
CHANGED
|
@@ -2,7 +2,13 @@ import { TLSSocket, type TlsConnectOptions } from './tls-socket.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Create a TLS client connection.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* the
|
|
5
|
+
* Without `options.socket`: connects via TCP first (net.Socket.connect on
|
|
6
|
+
* the TLSSocket itself), then upgrades to TLS once connected.
|
|
7
|
+
*
|
|
8
|
+
* With `options.socket`: adopts that already-connected (or still
|
|
9
|
+
* connecting) `net.Socket` and upgrades it in place — the STARTTLS shape
|
|
10
|
+
* (`@xmpp/starttls`, SMTP `STARTTLS`, IMAP `STARTTLS`, …): the caller has
|
|
11
|
+
* already exchanged some plaintext over the socket and is now handing it
|
|
12
|
+
* over for the TLS handshake to take over reading/writing.
|
|
7
13
|
*/
|
|
8
14
|
export declare function connect(options: TlsConnectOptions, callback?: () => void): TLSSocket;
|
|
@@ -34,8 +34,27 @@ export declare class TLSServer extends Server {
|
|
|
34
34
|
*/
|
|
35
35
|
private _resolveSniContext;
|
|
36
36
|
listen(...args: unknown[]): this;
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Upgrade a raw TCP socket to TLS using Gio.TlsServerConnection.
|
|
39
|
+
*
|
|
40
|
+
* Called synchronously from the 'connection' listener `listen()`
|
|
41
|
+
* registers — `Server._handleConnection` defers its own auto-start of
|
|
42
|
+
* `socket`'s plaintext read loop specifically so this can run first.
|
|
43
|
+
* `_claimConnection()` nulls `socket`'s stream fields right away: that
|
|
44
|
+
* makes the deferred auto-start a no-op, pre-empting a second,
|
|
45
|
+
* conflicting reader on the same Gio.InputStream this method's own
|
|
46
|
+
* SNI peek + handshake are about to read from (the incident behind
|
|
47
|
+
* `Server._handleConnection`'s `autoStartReading` parameter).
|
|
48
|
+
*/
|
|
38
49
|
private _upgradeTls;
|
|
50
|
+
/**
|
|
51
|
+
* Configure mTLS/ALPN/accept-certificate on a `Gio.TlsServerConnection`
|
|
52
|
+
* built over `baseIo`, run the handshake, and emit 'secureConnection'
|
|
53
|
+
* on success. Shared by `_upgradeTls`'s SNI-less fast path and its
|
|
54
|
+
* SNI-peek path (they differ only in how `baseIo`/`certificate` were
|
|
55
|
+
* obtained).
|
|
56
|
+
*/
|
|
57
|
+
private _completeUpgrade;
|
|
39
58
|
}
|
|
40
59
|
/**
|
|
41
60
|
* Create a TLS server.
|
|
@@ -2,7 +2,7 @@ import Gio from '@girs/gio-2.0';
|
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
3
|
import { Socket } from 'node:net';
|
|
4
4
|
import { type PeerCertificate } from './internal/cert-utils.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type SecureContext, type SecureContextOptions } from './secure-context.js';
|
|
6
6
|
import type { NativeSessionAccess } from './session-access.js';
|
|
7
7
|
export interface TlsConnectOptions extends SecureContextOptions {
|
|
8
8
|
host?: string;
|
|
@@ -37,7 +37,41 @@ export interface SocketInternals {
|
|
|
37
37
|
_outputStream: Gio.OutputStream | null;
|
|
38
38
|
_reading: boolean;
|
|
39
39
|
_startReading(): void;
|
|
40
|
+
/** In-flight Gio operations `_destroy` waits for before closing the connection. */
|
|
41
|
+
_pendingIo: number;
|
|
42
|
+
_ioSettled(): void;
|
|
43
|
+
/** Cancelled by `destroy()`; guards every Gio operation on this socket. */
|
|
44
|
+
_cancellable: Gio.Cancellable;
|
|
45
|
+
/**
|
|
46
|
+
* Stop the read loop and cancel any in-flight read, resolving once
|
|
47
|
+
* settled — see `@gjsify/net`'s `Socket._detachReader()` for the full
|
|
48
|
+
* contract. Used to hand an already-connected `net.Socket`'s Gio
|
|
49
|
+
* connection off to a `TLSSocket` (`tls.connect({socket})` / STARTTLS).
|
|
50
|
+
*/
|
|
51
|
+
_detachReader(): Promise<Buffer | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Atomically null out the four stream fields above and return what
|
|
54
|
+
* they were — see `@gjsify/net`'s `Socket._claimConnection()`. Call
|
|
55
|
+
* SYNCHRONOUSLY within a 'connect'/'connection' listener to pre-empt
|
|
56
|
+
* an auto-start that hasn't run yet (nulling `_inputStream` makes it
|
|
57
|
+
* a no-op); pair with `_detachReader()` when a read may already be
|
|
58
|
+
* running.
|
|
59
|
+
*/
|
|
60
|
+
_claimConnection(): {
|
|
61
|
+
connection: Gio.SocketConnection | null;
|
|
62
|
+
ioStream: Gio.IOStream | null;
|
|
63
|
+
inputStream: Gio.InputStream | null;
|
|
64
|
+
outputStream: Gio.OutputStream | null;
|
|
65
|
+
};
|
|
40
66
|
}
|
|
67
|
+
/** What {@link SocketInternals._claimConnection} hands over. */
|
|
68
|
+
export type ClaimedConnection = ReturnType<SocketInternals['_claimConnection']>;
|
|
69
|
+
/**
|
|
70
|
+
* Claim `socket`'s Gio stream fields, or answer `null` when it is not a
|
|
71
|
+
* `@gjsify/net` Socket (no `_claimConnection`): the caller then fails with
|
|
72
|
+
* {@link foreignSocketError} instead of a bare TypeError.
|
|
73
|
+
*/
|
|
74
|
+
export declare function claimGjsifyConnection(socket: unknown): ClaimedConnection | null;
|
|
41
75
|
/**
|
|
42
76
|
* TLSSocket wraps a net.Socket with TLS via Gio.TlsConnection.
|
|
43
77
|
*/
|
|
@@ -57,7 +91,123 @@ export declare class TLSSocket extends Socket {
|
|
|
57
91
|
* (`_getSessionAccess()`), reused across subsequent calls.
|
|
58
92
|
*/
|
|
59
93
|
_sessionAccess: NativeSessionAccess | null;
|
|
60
|
-
|
|
94
|
+
/**
|
|
95
|
+
* @internal Set while a `net.Socket` given to the constructor (or to
|
|
96
|
+
* `tls.connect({socket})`) is being — or has been — adopted, so
|
|
97
|
+
* `destroy()` can tear it down too. See `_startClient`.
|
|
98
|
+
*/
|
|
99
|
+
_adoptedSocket: Socket | null;
|
|
100
|
+
/**
|
|
101
|
+
* @internal True from the moment a client handshake is scheduled
|
|
102
|
+
* (`tls.connect()`, `new TLSSocket(socket)`) until `_setupTlsStreams()`
|
|
103
|
+
* wires the encrypted streams. While set, `_write`/`_final` wait in
|
|
104
|
+
* `_afterSecure` instead of reaching the plaintext transport — Node
|
|
105
|
+
* buffers `tls.connect(...).write(req)` the same way.
|
|
106
|
+
*/
|
|
107
|
+
_awaitingSecure: boolean;
|
|
108
|
+
/** Writes/end() queued while `_awaitingSecure`; run in order once secure. */
|
|
109
|
+
private _afterSecure;
|
|
110
|
+
/**
|
|
111
|
+
* The raw streams `_performHandshake` claimed, held here (not in a
|
|
112
|
+
* closure) while the handshake runs so `_destroy` can hand them back to
|
|
113
|
+
* `@gjsify/net`'s release path — otherwise a `destroy()` mid-handshake
|
|
114
|
+
* leaked the descriptor.
|
|
115
|
+
*/
|
|
116
|
+
private _handshakeClaim;
|
|
117
|
+
/** Why 'accept-certificate' refused the peer, as Node's error code + message. */
|
|
118
|
+
private _certRejection;
|
|
119
|
+
constructor(socket?: Socket, options?: TlsConnectOptions);
|
|
120
|
+
/**
|
|
121
|
+
* @internal Adopt `providedSocket` — already connected, or still
|
|
122
|
+
* connecting — as this TLSSocket's transport, then run the client TLS
|
|
123
|
+
* handshake over it. Backs both `new tls.TLSSocket(socket, options)`
|
|
124
|
+
* and `tls.connect({socket, ...})`: Node's `tls.connect` is a thin
|
|
125
|
+
* wrapper over the TLSSocket constructor plus starting the handshake,
|
|
126
|
+
* so we fold the two entry points together here instead of keeping
|
|
127
|
+
* the handshake logic twice (see `connect.ts`).
|
|
128
|
+
*/
|
|
129
|
+
/**
|
|
130
|
+
* Claim a socket's stream fields, or destroy `this` and answer `null`.
|
|
131
|
+
*
|
|
132
|
+
* Node's real `tls.connect({socket})` accepts ANY Duplex; we can only
|
|
133
|
+
* adopt a `@gjsify/net` Socket today — its Gio connection is what gets
|
|
134
|
+
* handed to Gio.TlsClientConnection, and a foreign Duplex has none.
|
|
135
|
+
* Feature-detect rather than let a bare `_claimConnection is not a
|
|
136
|
+
* function` TypeError surface three calls deep: reached in practice when
|
|
137
|
+
* a build aliases `node:tls` to this polyfill but leaves `node:net` on a
|
|
138
|
+
* runtime's own native module (e.g. `@gjsify/node-gi`'s consumer harness,
|
|
139
|
+
* which forces `runtimes.node === "native"` deps onto their polyfill body
|
|
140
|
+
* but `@gjsify/net` declares `"none"`, so it stays native — see
|
|
141
|
+
* status/open-todos/README.md). Generic-Duplex support is tracked there too.
|
|
142
|
+
*
|
|
143
|
+
* ONE guard for every claim site: this method for the two client
|
|
144
|
+
* ones, {@link claimGjsifyConnection} directly for `TLSServer`'s
|
|
145
|
+
* accepted sockets. `_adoptConnection` had a guard; `_performHandshake`
|
|
146
|
+
* and `TLSServer._upgradeTls` called `_claimConnection()` bare. The
|
|
147
|
+
* server one is what kept four `given-socket.spec.ts` tests red on the
|
|
148
|
+
* `node-gi` consumer harness since #1837 added them: `withServer` is a
|
|
149
|
+
* `tls.createServer` whose accepted sockets are native there too, so
|
|
150
|
+
* every connection threw the TypeError out of the `'connection'`
|
|
151
|
+
* listener, uncaught, into whichever test was running.
|
|
152
|
+
*/
|
|
153
|
+
private _claimSocketStreams;
|
|
154
|
+
private _startClient;
|
|
155
|
+
/**
|
|
156
|
+
* @internal Detach `providedSocket`'s read loop (see
|
|
157
|
+
* `SocketInternals._detachReader`) and steal its Gio connection/streams
|
|
158
|
+
* onto `this`. Returns `false` — having already cleaned up — when
|
|
159
|
+
* either socket was destroyed meanwhile, or when the detach lost the
|
|
160
|
+
* race to data that had already arrived (see `_detachReader`'s doc):
|
|
161
|
+
* `Gio.TlsClientConnection` has no API to replay bytes already pulled
|
|
162
|
+
* off the wire, unlike Node's OpenSSL BIO, so that case is surfaced as
|
|
163
|
+
* a destroy() error rather than silently dropped.
|
|
164
|
+
*
|
|
165
|
+
* Claims the streams (`_claimConnection`, nulling them on
|
|
166
|
+
* `providedSocket`) SYNCHRONOUSLY, before the `_detachReader` await:
|
|
167
|
+
* when `_startClient` runs this from `providedSocket`'s OWN 'connect'
|
|
168
|
+
* listener (the still-connecting case), `Socket.connect()`'s
|
|
169
|
+
* `_setupConnection()` is about to auto-start reading right after
|
|
170
|
+
* 'connect' listeners finish — nulling `_inputStream` now makes that a
|
|
171
|
+
* no-op instead of a second reader racing the handshake this method
|
|
172
|
+
* goes on to start. For the already-connected case, a read may
|
|
173
|
+
* already be running; `_detachReader()` (order relative to the claim
|
|
174
|
+
* doesn't matter — the running loop holds its own local stream
|
|
175
|
+
* reference, not `providedSocket`'s field) settles that.
|
|
176
|
+
*/
|
|
177
|
+
private _adoptConnection;
|
|
178
|
+
/**
|
|
179
|
+
* @internal Perform the client TLS handshake over this socket's Gio
|
|
180
|
+
* connection — either self-connected via `tls.connect()` (no
|
|
181
|
+
* `options.socket`), or just adopted from a caller-supplied
|
|
182
|
+
* `net.Socket` via `_startClient()`. Moved here from `connect.ts` so
|
|
183
|
+
* both entry points share one implementation.
|
|
184
|
+
*
|
|
185
|
+
* Reference: Node.js `lib/internal/tls/wrap.js` `TLSSocket.prototype._init`.
|
|
186
|
+
*/
|
|
187
|
+
_performHandshake(options: TlsConnectOptions): void;
|
|
188
|
+
private _secureEstablished;
|
|
189
|
+
_write(chunk: unknown, encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
|
|
190
|
+
private _flushAfterSecure;
|
|
191
|
+
/**
|
|
192
|
+
* Hands a mid-handshake connection back to `@gjsify/net`'s release path:
|
|
193
|
+
* `super._destroy` cancels `_cancellable` (aborting `handshake_async`)
|
|
194
|
+
* and, since the handshake counts as pending I/O, closes the restored
|
|
195
|
+
* raw connection only once that callback has settled.
|
|
196
|
+
*/
|
|
197
|
+
_destroy(err: Error | null, callback: (error?: Error | null) => void): void;
|
|
198
|
+
/**
|
|
199
|
+
* `end()` on a TLS socket must send TLS close_notify — the peer's TLS
|
|
200
|
+
* layer only reports EOF on that alert, never on a bare TCP half-close.
|
|
201
|
+
* `@gjsify/net`'s `_final` half-closes via `get_socket().shutdown()`,
|
|
202
|
+
* which a `Gio.TlsConnection` (stored in `_connection`) doesn't have:
|
|
203
|
+
* the call threw, was swallowed, and nothing reached the peer, so an
|
|
204
|
+
* `end()` with the default `allowHalfOpen: false` on both sides waited
|
|
205
|
+
* forever for the other side's 'end'. Closing the TLS output stream
|
|
206
|
+
* sends close_notify and closes the base stream's write side
|
|
207
|
+
* (glib-networking's `g_tls_connection_base_close_internal`), while
|
|
208
|
+
* the read side stays open for the peer's reply.
|
|
209
|
+
*/
|
|
210
|
+
_final(callback: (error?: Error | null) => void): void;
|
|
61
211
|
/**
|
|
62
212
|
* @internal Wire the TLS connection's I/O streams into this socket
|
|
63
213
|
* so that read/write operations go through the encrypted channel.
|
|
@@ -138,3 +288,14 @@ export declare class TLSSocket extends Socket {
|
|
|
138
288
|
*/
|
|
139
289
|
isSessionReused(): boolean;
|
|
140
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Error surfaced by `TLSSocket._adoptConnection()` when `options.socket` /
|
|
293
|
+
* the constructor's `socket` argument doesn't carry a `@gjsify/net` Gio
|
|
294
|
+
* connection (`_claimConnection`/`_detachReader` missing). Node's real
|
|
295
|
+
* `tls.connect({socket})` accepts any Duplex; adopting a foreign one needs
|
|
296
|
+
* a Duplex→Gio.IOStream adapter this package doesn't have yet — tracked in
|
|
297
|
+
* status/open-todos/README.md, next to the SNI-peek entry.
|
|
298
|
+
*/
|
|
299
|
+
export declare function foreignSocketError(): Error & {
|
|
300
|
+
code: string;
|
|
301
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import Gio from '@girs/gio-2.0';
|
|
2
|
+
import { type PeerCertificate } from './internal/cert-utils.js';
|
|
3
|
+
export { createSecureContext, type SecureContext, type SecureContextOptions } from './secure-context.js';
|
|
4
|
+
export type { PeerCertificate } from './internal/cert-utils.js';
|
|
5
|
+
/** Error carrying Node's OpenSSL-derived `code` (e.g. `DEPTH_ZERO_SELF_SIGNED_CERT`). */
|
|
6
|
+
export interface TlsVerifyError extends Error {
|
|
7
|
+
code: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PeerVerifierOptions {
|
|
10
|
+
/** Trust anchors (`SecureContext.caCertificates`). Empty ⇒ the system trust store. */
|
|
11
|
+
caCertificates?: Gio.TlsCertificate[];
|
|
12
|
+
/** Unset ⇒ verify, unless `NODE_TLS_REJECT_UNAUTHORIZED=0` (Node's rule: explicit wins). */
|
|
13
|
+
rejectUnauthorized?: boolean;
|
|
14
|
+
/** Name the certificate must be valid for — Node's `servername || host`. */
|
|
15
|
+
host: string;
|
|
16
|
+
checkServerIdentity?: (host: string, cert: PeerCertificate) => Error | undefined;
|
|
17
|
+
}
|
|
18
|
+
export interface PeerVerifier {
|
|
19
|
+
/** `accept-certificate` handler: true accepts the peer. */
|
|
20
|
+
accept(peer: Gio.TlsCertificate): boolean;
|
|
21
|
+
/** Why the peer is not authorized (null when it is) — the error a rejected request reports. */
|
|
22
|
+
readonly error: Error | null;
|
|
23
|
+
readonly authorized: boolean;
|
|
24
|
+
/** Node's `TLSSocket.authorizationError`: the error's code, else its message. */
|
|
25
|
+
readonly authorizationError: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
/** Resolve `rejectUnauthorized` the way Node's tls.connect does. */
|
|
28
|
+
export declare function resolveRejectUnauthorized(rejectUnauthorized: boolean | undefined): boolean;
|
|
29
|
+
/** Build a verifier for one connection attempt. */
|
|
30
|
+
export declare function createPeerVerifier(options: PeerVerifierOptions): PeerVerifier;
|
|
31
|
+
/**
|
|
32
|
+
* The chain's verdict as the error Node (OpenSSL) reports for it, or null.
|
|
33
|
+
* An unknown issuer splits four ways by the shape of the presented chain,
|
|
34
|
+
* matching X509_V_ERR_{DEPTH_ZERO_SELF_SIGNED_CERT,SELF_SIGNED_CERT_IN_CHAIN,
|
|
35
|
+
* UNABLE_TO_VERIFY_LEAF_SIGNATURE,UNABLE_TO_GET_ISSUER_CERT_LOCALLY}.
|
|
36
|
+
*/
|
|
37
|
+
export declare function chainError(peer: Gio.TlsCertificate, anchors?: Gio.TlsCertificate[]): TlsVerifyError | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/tls",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "Node.js tls module for Gjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "lib/esm/index.js",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"types": "./lib/types/index.d.ts",
|
|
11
11
|
"default": "./lib/esm/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./verify": {
|
|
14
|
+
"types": "./lib/types/verify.d.ts",
|
|
15
|
+
"default": "./lib/esm/verify.js"
|
|
16
|
+
},
|
|
13
17
|
"./browser": {
|
|
14
18
|
"types": "./lib/types/browser.d.ts",
|
|
15
19
|
"default": "./lib/esm/browser.js"
|
|
@@ -37,17 +41,17 @@
|
|
|
37
41
|
"tls"
|
|
38
42
|
],
|
|
39
43
|
"devDependencies": {
|
|
40
|
-
"@gjsify/cli": "^0.
|
|
41
|
-
"@gjsify/unit": "^0.
|
|
44
|
+
"@gjsify/cli": "^0.53.0",
|
|
45
|
+
"@gjsify/unit": "^0.53.0",
|
|
42
46
|
"@types/node": "^25.9.2",
|
|
43
47
|
"typescript": "^6.0.3"
|
|
44
48
|
},
|
|
45
49
|
"dependencies": {
|
|
46
50
|
"@girs/gio-2.0": "5.4.0",
|
|
47
51
|
"@girs/glib-2.0": "5.4.0",
|
|
48
|
-
"@gjsify/net": "^0.
|
|
49
|
-
"@gjsify/tls-native": "^0.
|
|
50
|
-
"@gjsify/utils": "^0.
|
|
52
|
+
"@gjsify/net": "^0.53.0",
|
|
53
|
+
"@gjsify/tls-native": "^0.53.0",
|
|
54
|
+
"@gjsify/utils": "^0.53.0"
|
|
51
55
|
},
|
|
52
56
|
"gjsify": {
|
|
53
57
|
"runtimes": {
|
|
@@ -56,7 +60,10 @@
|
|
|
56
60
|
"browser": "none",
|
|
57
61
|
"nativescript": "none"
|
|
58
62
|
},
|
|
59
|
-
"tier": 1
|
|
63
|
+
"tier": 1,
|
|
64
|
+
"polyfillPeers": {
|
|
65
|
+
"net": "tls.connect({ socket }) and new TLSSocket(socket) adopt a @gjsify/net Socket by claiming its Gio connection; a runtime's own net.Socket has none to hand over"
|
|
66
|
+
}
|
|
60
67
|
},
|
|
61
68
|
"license": "MIT",
|
|
62
69
|
"repository": {
|