import { connect } from 'cloudflare:sockets'; const CONTENT_TYPE = { CHANGE_CIPHER_SPEC: 0x14, ALERT: 0x15, HANDSHAKE: 0x16, APPLICATION_DATA: 0x17 }; const HANDSHAKE_TYPE = { CLIENT_HELLO: 1, SERVER_HELLO: 2, NEW_SESSION_TICKET: 4, ENCRYPTED_EXTENSIONS: 8, CERTIFICATE: 11, CERTIFICATE_VERIFY: 15, FINISHED: 20, SERVER_KEY_EXCHANGE: 12, SERVER_HELLO_DONE: 14, CLIENT_KEY_EXCHANGE: 16 }; const EXTENSION_TYPE = { SERVER_NAME: 0, EC_POINT_FORMATS: 11, SUPPORTED_GROUPS: 10, SIGNATURE_ALGORITHMS: 13, ENCRYPT_THEN_MAC: 22, EXTENDED_MASTER_SECRET: 23, SUPPORTED_VERSIONS: 43, PSK_KEY_EXCHANGE_MODES: 45, KEY_SHARE: 51, RENEGOTIATION_INFO: 0xff01 }; const NAMED_GROUP = { SECP256R1: 0x0017, SECP384R1: 0x0018, SECP521R1: 0x0019 }; const CURVE_MAP = { 0x0017: 'P-256', 0x0018: 'P-384', 0x0019: 'P-521' }; const CURVE_BITS = { 0x0017: 256, 0x0018: 384, 0x0019: 521 }; const TLS13_CIPHERS = { 0x1301: { name: 'TLS_AES_128_GCM_SHA256', keyLen: 16, ivLen: 12, hash: 'SHA-256', hashLen: 32 }, 0x1302: { name: 'TLS_AES_256_GCM_SHA384', keyLen: 32, ivLen: 12, hash: 'SHA-384', hashLen: 48 } }; const TLS12_CIPHERS = { 0xc02f: { name: 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256', keyLen: 16, ivLen: 4, hash: 'SHA-256' }, 0xc030: { name: 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', keyLen: 32, ivLen: 4, hash: 'SHA-384' }, 0xc02b: { name: 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256', keyLen: 16, ivLen: 4, hash: 'SHA-256' }, 0xc02c: { name: 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384', keyLen: 32, ivLen: 4, hash: 'SHA-384' } }; const SIGNATURE_ALGORITHMS = [0x0403, 0x0503, 0x0603, 0x0804, 0x0805, 0x0806, 0x0401, 0x0501, 0x0601]; const ALERT_DESCRIPTIONS = { 0: 'close_notify', 10: 'unexpected_message', 20: 'bad_record_mac', 40: 'handshake_failure', 42: 'bad_certificate', 43: 'unsupported_certificate', 44: 'certificate_revoked', 45: 'certificate_expired', 46: 'certificate_unknown', 47: 'illegal_parameter', 48: 'unknown_ca', 50: 'decode_error', 51: 'decrypt_error', 70: 'protocol_version', 71: 'insufficient_security', 80: 'internal_error', 86: 'inappropriate_fallback', 109: 'missing_extension', 110: 'unsupported_extension', 112: 'unrecognized_name', 116: 'certificate_required', 120: 'no_application_protocol' }; class BufferBuilder { constructor(size = 4096) { this.buf = new Uint8Array(size); this.pos = 0; } ensure(n) { if (this.pos + n > this.buf.length) { const nb = new Uint8Array(Math.max(this.buf.length * 2, this.pos + n)); nb.set(this.buf); this.buf = nb; } } u8(v) { this.ensure(1); this.buf[this.pos++] = v & 0xff; return this; } u16(v) { this.ensure(2); this.buf[this.pos++] = (v >> 8) & 0xff; this.buf[this.pos++] = v & 0xff; return this; } u24(v) { this.ensure(3); this.buf[this.pos++] = (v >> 16) & 0xff; this.buf[this.pos++] = (v >> 8) & 0xff; this.buf[this.pos++] = v & 0xff; return this; } bytes(b) { const arr = b instanceof ArrayBuffer ? new Uint8Array(b) : b; this.ensure(arr.length); this.buf.set(arr, this.pos); this.pos += arr.length; return this; } build() { return this.buf.slice(0, this.pos); } } class BufferReader { constructor(buf) { this.buf = buf instanceof ArrayBuffer ? new Uint8Array(buf) : buf; this.pos = 0; } get left() { return this.buf.length - this.pos; } u8() { return this.buf[this.pos++]; } u16() { const v = (this.buf[this.pos] << 8) | this.buf[this.pos + 1]; this.pos += 2; return v; } u24() { const v = (this.buf[this.pos] << 16) | (this.buf[this.pos + 1] << 8) | this.buf[this.pos + 2]; this.pos += 3; return v; } bytes(n) { const b = this.buf.slice(this.pos, this.pos + n); this.pos += n; return b; } skip(n) { this.pos += n; } } function concat(...bufs) { const total = bufs.reduce((s, b) => s + (b instanceof ArrayBuffer ? b.byteLength : b.length), 0); const result = new Uint8Array(total); let off = 0; for (const b of bufs) { const arr = b instanceof ArrayBuffer ? new Uint8Array(b) : b; result.set(arr, off); off += arr.length; } return result; } function rand(n) { const b = new Uint8Array(n); crypto.getRandomValues(b); return b; } function arrEq(a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; } class TLSCrypto { constructor() { this.reset(); } reset() { this.clientRandom = null; this.serverRandom = null; this.privateKey = null; this.publicKey = null; this.sharedSecret = null; this.namedGroup = NAMED_GROUP.SECP256R1; this.cipher = null; this.cipherInfo = null; this.tlsVersion = null; this.handshakeSecret = null; this.masterSecret = null; this.clientHSSecret = null; this.serverHSSecret = null; this.clientHandshakeKey = null; this.serverHandshakeKey = null; this.clientHandshakeIV = null; this.serverHandshakeIV = null; this.clientAppKey = null; this.serverAppKey = null; this.clientAppIV = null; this.serverAppIV = null; this.hsSeqClient = 0n; this.hsSeqServer = 0n; this.appSeqClient = 0n; this.appSeqServer = 0n; this.preMasterSecret = null; this.masterSecret12 = null; this.clientWriteKey = null; this.serverWriteKey = null; this.clientWriteIV = null; this.serverWriteIV = null; this.seqClient = 0n; this.seqServer = 0n; this.transcriptMessages = []; } async init() { this.clientRandom = rand(32); const keyPair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']); this.privateKey = keyPair.privateKey; this.publicKey = new Uint8Array(await crypto.subtle.exportKey('raw', keyPair.publicKey)); return this.publicKey; } async computeSharedSecret(serverPubKey, namedGroup) { this.namedGroup = namedGroup; const curve = CURVE_MAP[namedGroup] || 'P-256'; const bits = CURVE_BITS[namedGroup] || 256; if ((namedGroup === 0x0018 && this.publicKey.length !== 97) || (namedGroup === 0x0017 && this.publicKey.length !== 65)) { const keyPair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: curve }, true, ['deriveBits']); this.privateKey = keyPair.privateKey; this.publicKey = new Uint8Array(await crypto.subtle.exportKey('raw', keyPair.publicKey)); } const serverKey = await crypto.subtle.importKey('raw', serverPubKey, { name: 'ECDH', namedCurve: curve }, false, []); const shared = await crypto.subtle.deriveBits({ name: 'ECDH', public: serverKey }, this.privateKey, bits); this.sharedSecret = new Uint8Array(shared); this.preMasterSecret = this.sharedSecret; return this.sharedSecret; } get hash() { return this.cipherInfo?.hash || 'SHA-256'; } get hashLen() { return this.cipherInfo?.hashLen || (this.hash === 'SHA-384' ? 48 : 32); } addTranscript(msg) { this.transcriptMessages.push(msg); } async transcriptHash(msgs = this.transcriptMessages) { return new Uint8Array(await crypto.subtle.digest(this.hash, concat(...msgs))); } async hkdfExtract(salt, ikm) { const saltBytes = salt.length === 0 ? new Uint8Array(this.hashLen) : salt; const key = await crypto.subtle.importKey('raw', saltBytes, { name: 'HMAC', hash: this.hash }, false, ['sign']); return new Uint8Array(await crypto.subtle.sign('HMAC', key, ikm)); } async hkdfExpand(prk, info, length) { const key = await crypto.subtle.importKey('raw', prk, { name: 'HMAC', hash: this.hash }, false, ['sign']); const n = Math.ceil(length / this.hashLen); const result = new Uint8Array(n * this.hashLen); let t = new Uint8Array(0); for (let i = 0; i < n; i++) { t = new Uint8Array(await crypto.subtle.sign('HMAC', key, concat(t, info, new Uint8Array([i + 1])))); result.set(t, i * this.hashLen); } return result.slice(0, length); } async hkdfExpandLabel(secret, label, context, length) { const labelBytes = new TextEncoder().encode('tls13 ' + label); const b = new BufferBuilder(); b.u16(length).u8(labelBytes.length).bytes(labelBytes).u8(context.length).bytes(context); return this.hkdfExpand(secret, b.build(), length); } async prf(secret, label, seed, length) { const labelBytes = new TextEncoder().encode(label); const fullSeed = concat(labelBytes, seed); return this.pHash(secret, fullSeed, length); } async pHash(secret, seed, length) { const key = await crypto.subtle.importKey('raw', secret, { name: 'HMAC', hash: this.hash }, false, ['sign']); const result = new Uint8Array(length); let offset = 0, a = seed; while (offset < length) { a = new Uint8Array(await crypto.subtle.sign('HMAC', key, a)); const output = new Uint8Array(await crypto.subtle.sign('HMAC', key, concat(a, seed))); const toCopy = Math.min(output.length, length - offset); result.set(output.slice(0, toCopy), offset); offset += toCopy; } return result; } async deriveHandshakeSecrets13() { const zeroKey = new Uint8Array(this.hashLen); const earlySecret = await this.hkdfExtract(new Uint8Array(0), zeroKey); const emptyHash = new Uint8Array(await crypto.subtle.digest(this.hash, new Uint8Array(0))); const derivedEarly = await this.hkdfExpandLabel(earlySecret, 'derived', emptyHash, this.hashLen); this.handshakeSecret = await this.hkdfExtract(derivedEarly, this.sharedSecret); const hsHash = await this.transcriptHash(); this.clientHSSecret = await this.hkdfExpandLabel(this.handshakeSecret, 'c hs traffic', hsHash, this.hashLen); this.serverHSSecret = await this.hkdfExpandLabel(this.handshakeSecret, 's hs traffic', hsHash, this.hashLen); this.clientHandshakeKey = await this.hkdfExpandLabel(this.clientHSSecret, 'key', new Uint8Array(0), this.cipherInfo.keyLen); this.clientHandshakeIV = await this.hkdfExpandLabel(this.clientHSSecret, 'iv', new Uint8Array(0), this.cipherInfo.ivLen); this.serverHandshakeKey = await this.hkdfExpandLabel(this.serverHSSecret, 'key', new Uint8Array(0), this.cipherInfo.keyLen); this.serverHandshakeIV = await this.hkdfExpandLabel(this.serverHSSecret, 'iv', new Uint8Array(0), this.cipherInfo.ivLen); } async deriveAppSecrets13() { const emptyHash = new Uint8Array(await crypto.subtle.digest(this.hash, new Uint8Array(0))); const derivedHS = await this.hkdfExpandLabel(this.handshakeSecret, 'derived', emptyHash, this.hashLen); this.masterSecret = await this.hkdfExtract(derivedHS, new Uint8Array(this.hashLen)); const appHash = await this.transcriptHash(); const clientAppSecret = await this.hkdfExpandLabel(this.masterSecret, 'c ap traffic', appHash, this.hashLen); const serverAppSecret = await this.hkdfExpandLabel(this.masterSecret, 's ap traffic', appHash, this.hashLen); this.clientAppKey = await this.hkdfExpandLabel(clientAppSecret, 'key', new Uint8Array(0), this.cipherInfo.keyLen); this.clientAppIV = await this.hkdfExpandLabel(clientAppSecret, 'iv', new Uint8Array(0), this.cipherInfo.ivLen); this.serverAppKey = await this.hkdfExpandLabel(serverAppSecret, 'key', new Uint8Array(0), this.cipherInfo.keyLen); this.serverAppIV = await this.hkdfExpandLabel(serverAppSecret, 'iv', new Uint8Array(0), this.cipherInfo.ivLen); this.appSeqClient = 0n; this.appSeqServer = 0n; } async deriveKeys12() { const seed = concat(this.clientRandom, this.serverRandom); this.masterSecret12 = await this.prf(this.preMasterSecret, 'master secret', seed, 48); const keyBlockLen = (this.cipherInfo.keyLen + this.cipherInfo.ivLen) * 2; const keyBlock = await this.prf(this.masterSecret12, 'key expansion', concat(this.serverRandom, this.clientRandom), keyBlockLen); let off = 0; this.clientWriteKey = keyBlock.slice(off, off + this.cipherInfo.keyLen); off += this.cipherInfo.keyLen; this.serverWriteKey = keyBlock.slice(off, off + this.cipherInfo.keyLen); off += this.cipherInfo.keyLen; this.clientWriteIV = keyBlock.slice(off, off + this.cipherInfo.ivLen); off += this.cipherInfo.ivLen; this.serverWriteIV = keyBlock.slice(off, off + this.cipherInfo.ivLen); } buildNonce13(iv, seq) { const nonce = new Uint8Array(iv); const seqBytes = new Uint8Array(8); let s = seq; for (let i = 7; i >= 0; i--) { seqBytes[i] = Number(s & 0xFFn); s >>= 8n; } for (let i = 0; i < 8; i++) nonce[nonce.length - 8 + i] ^= seqBytes[i]; return nonce; } async encryptHandshake13(plaintext) { const inner = concat(plaintext, new Uint8Array([CONTENT_TYPE.HANDSHAKE])); const nonce = this.buildNonce13(this.clientHandshakeIV, this.hsSeqClient); const aad = new Uint8Array([CONTENT_TYPE.APPLICATION_DATA, 0x03, 0x03, ((inner.length + 16) >> 8) & 0xff, (inner.length + 16) & 0xff]); const key = await crypto.subtle.importKey('raw', this.clientHandshakeKey, { name: 'AES-GCM' }, false, ['encrypt']); const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce, additionalData: aad, tagLength: 128 }, key, inner); this.hsSeqClient++; return new Uint8Array(ct); } async decryptRecord13(ciphertext, isHandshake = true) { const iv = isHandshake ? this.serverHandshakeIV : this.serverAppIV; const keyData = isHandshake ? this.serverHandshakeKey : this.serverAppKey; const seq = isHandshake ? this.hsSeqServer : this.appSeqServer; const nonce = this.buildNonce13(iv, seq); const aad = new Uint8Array([CONTENT_TYPE.APPLICATION_DATA, 0x03, 0x03, (ciphertext.length >> 8) & 0xff, ciphertext.length & 0xff]); const key = await crypto.subtle.importKey('raw', keyData, { name: 'AES-GCM' }, false, ['decrypt']); const pt = new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce, additionalData: aad, tagLength: 128 }, key, ciphertext)); if (isHandshake) this.hsSeqServer++; else this.appSeqServer++; let i = pt.length - 1; while (i >= 0 && pt[i] === 0) i--; return { payload: pt.slice(0, i), contentType: pt[i] }; } async encryptAppData13(plaintext) { const inner = concat(plaintext, new Uint8Array([CONTENT_TYPE.APPLICATION_DATA])); const nonce = this.buildNonce13(this.clientAppIV, this.appSeqClient); const aad = new Uint8Array([CONTENT_TYPE.APPLICATION_DATA, 0x03, 0x03, ((inner.length + 16) >> 8) & 0xff, (inner.length + 16) & 0xff]); const key = await crypto.subtle.importKey('raw', this.clientAppKey, { name: 'AES-GCM' }, false, ['encrypt']); const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce, additionalData: aad, tagLength: 128 }, key, inner); this.appSeqClient++; return new Uint8Array(ct); } buildNonce12(implicitIV, seq) { const nonce = new Uint8Array(12); nonce.set(implicitIV, 0); for (let i = 0; i < 8; i++) nonce[4 + i] = Number((seq >> BigInt(56 - i * 8)) & 0xFFn); return nonce; } buildAAD12(seq, contentType, length) { const ad = new Uint8Array(13); for (let i = 0; i < 8; i++) ad[i] = Number((seq >> BigInt(56 - i * 8)) & 0xFFn); ad[8] = contentType; ad[9] = 0x03; ad[10] = 0x03; ad[11] = (length >> 8) & 0xff; ad[12] = length & 0xff; return ad; } async encrypt12(plaintext, contentType) { const nonce = this.buildNonce12(this.clientWriteIV, this.seqClient); const explicitNonce = nonce.slice(4, 12); const aad = this.buildAAD12(this.seqClient, contentType, plaintext.length); const key = await crypto.subtle.importKey('raw', this.clientWriteKey, { name: 'AES-GCM' }, false, ['encrypt']); const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce, additionalData: aad, tagLength: 128 }, key, plaintext); this.seqClient++; return concat(explicitNonce, new Uint8Array(ct)); } async decrypt12(ciphertext, contentType) { const explicitNonce = ciphertext.slice(0, 8); const encData = ciphertext.slice(8); const nonce = new Uint8Array(12); nonce.set(this.serverWriteIV, 0); nonce.set(explicitNonce, 4); const aad = this.buildAAD12(this.seqServer, contentType, encData.length - 16); const key = await crypto.subtle.importKey('raw', this.serverWriteKey, { name: 'AES-GCM' }, false, ['decrypt']); const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce, additionalData: aad, tagLength: 128 }, key, encData); this.seqServer++; return new Uint8Array(pt); } async computeFinishedVerifyData(isClient, isTLS13 = true) { if (isTLS13) { const secret = isClient ? this.clientHSSecret : this.serverHSSecret; const finishedKey = await this.hkdfExpandLabel(secret, 'finished', new Uint8Array(0), this.hashLen); const key = await crypto.subtle.importKey('raw', finishedKey, { name: 'HMAC', hash: this.hash }, false, ['sign']); const th = await this.transcriptHash(); return new Uint8Array(await crypto.subtle.sign('HMAC', key, th)); } else { const label = isClient ? 'client finished' : 'server finished'; const msgsWithoutLast = isClient ? this.transcriptMessages : this.transcriptMessages.slice(0, -1); const th = await this.transcriptHash(msgsWithoutLast); return this.prf(this.masterSecret12, label, th, 12); } } } class TLSSocket { constructor() { this.socket = null; this.writer = null; this.reader = null; this.crypto = new TLSCrypto(); this.connected = false; this.handshakeComplete = false; this.pendingData = new Uint8Array(0); this.serverName = null; this.tlsVersion = null; this.encrypted = false; } async connect(options) { const { host, port, servername, timeout = 15000 } = options; this.serverName = servername || host; this.socket = connect({ hostname: host, port }); this.writer = this.socket.writable.getWriter(); this.reader = this.socket.readable.getReader(); this.connected = true; await Promise.race([ this.performHandshake(), new Promise((_, rej) => setTimeout(() => rej(new Error('Handshake timeout')), timeout)) ]); this.handshakeComplete = true; } buildRecord(contentType, data) { const b = new BufferBuilder(); b.u8(contentType).u8(0x03).u8(0x03).u16(data.length).bytes(data); return b.build(); } buildClientHello() { const b = new BufferBuilder(); b.u8(0x03).u8(0x03); b.bytes(this.crypto.clientRandom); const sessionId = rand(32); b.u8(32).bytes(sessionId); const ciphers = [0x1301, 0x1302, 0xc02f, 0xc030, 0xc02b, 0xc02c]; b.u16(ciphers.length * 2); for (const c of ciphers) b.u16(c); b.u8(1).u8(0); const ext = this.buildExtensions(); b.u16(ext.length).bytes(ext); const body = b.build(); const msg = new BufferBuilder(); msg.u8(HANDSHAKE_TYPE.CLIENT_HELLO).u24(body.length).bytes(body); return msg.build(); } buildExtensions() { const b = new BufferBuilder(); const name = new TextEncoder().encode(this.serverName); b.u16(EXTENSION_TYPE.SERVER_NAME); b.u16(name.length + 5); b.u16(name.length + 3); b.u8(0).u16(name.length).bytes(name); b.u16(EXTENSION_TYPE.SUPPORTED_GROUPS); b.u16(8).u16(6); b.u16(NAMED_GROUP.SECP256R1).u16(NAMED_GROUP.SECP384R1).u16(NAMED_GROUP.SECP521R1); b.u16(EXTENSION_TYPE.EC_POINT_FORMATS); b.u16(2).u8(1).u8(0); b.u16(EXTENSION_TYPE.SIGNATURE_ALGORITHMS); b.u16(SIGNATURE_ALGORITHMS.length * 2 + 2); b.u16(SIGNATURE_ALGORITHMS.length * 2); for (const alg of SIGNATURE_ALGORITHMS) b.u16(alg); b.u16(EXTENSION_TYPE.EXTENDED_MASTER_SECRET).u16(0); b.u16(EXTENSION_TYPE.SUPPORTED_VERSIONS); b.u16(5).u8(4); b.u16(0x0304).u16(0x0303); b.u16(EXTENSION_TYPE.PSK_KEY_EXCHANGE_MODES); b.u16(2).u8(1).u8(1); const pubKey = this.crypto.publicKey; b.u16(EXTENSION_TYPE.KEY_SHARE); const ksEntryLen = 2 + 2 + pubKey.length; b.u16(2 + ksEntryLen); b.u16(ksEntryLen); b.u16(NAMED_GROUP.SECP256R1); b.u16(pubKey.length); b.bytes(pubKey); b.u16(EXTENSION_TYPE.RENEGOTIATION_INFO).u16(1).u8(0); return b.build(); } async performHandshake() { await this.crypto.init(); const clientHello = this.buildClientHello(); this.crypto.addTranscript(clientHello); await this.writer.write(this.buildRecord(CONTENT_TYPE.HANDSHAKE, clientHello)); const serverHelloInfo = await this.readServerHello(); if (this.tlsVersion === 'tls13') { await this.crypto.deriveHandshakeSecrets13(); await this.readEncryptedHandshake13(); await this.crypto.deriveAppSecrets13(); await this.sendClientFinished13(); } else { await this.finishHandshake12(); } } async readData() { while (true) { if (this.pendingData.length >= 5) { const len = (this.pendingData[3] << 8) | this.pendingData[4]; if (this.pendingData.length >= 5 + len) { const record = this.pendingData.slice(0, 5 + len); this.pendingData = this.pendingData.slice(5 + len); return record; } } const { value, done } = await this.reader.read(); if (done) throw new Error('Connection closed'); this.pendingData = concat(this.pendingData, value); } } parseRecord(data) { const r = new BufferReader(data); const ct = r.u8(); r.u8(); r.u8(); const len = r.u16(); return { contentType: ct, fragment: r.bytes(len) }; } async readServerHello() { while (true) { const rec = this.parseRecord(await this.readData()); if (rec.contentType === CONTENT_TYPE.ALERT) { const level = rec.fragment[0], desc = rec.fragment[1]; throw new Error(`TLS Alert: level=${level}, description=${desc} (${ALERT_DESCRIPTIONS[desc] || 'unknown'})`); } if (rec.contentType === CONTENT_TYPE.HANDSHAKE) { const r = new BufferReader(rec.fragment); while (r.left > 0) { const type = r.u8(); const len = r.u24(); const data = r.bytes(len); const fullMsg = new BufferBuilder(); fullMsg.u8(type).u24(len).bytes(data); const msg = fullMsg.build(); if (type === HANDSHAKE_TYPE.SERVER_HELLO) { this.crypto.addTranscript(msg); return this.parseServerHello(data); } } } } } async parseServerHello(data) { const r = new BufferReader(data); r.u8(); r.u8(); this.crypto.serverRandom = r.bytes(32); const sidLen = r.u8(); if (sidLen > 0) r.skip(sidLen); const cipher = r.u16(); r.u8(); let isTLS13 = false; let serverKeyShare = null; let serverGroup = null; if (r.left > 0) { const extLen = r.u16(); const extEnd = r.pos + extLen; while (r.pos < extEnd) { const extType = r.u16(); const extDataLen = r.u16(); const extData = r.bytes(extDataLen); if (extType === EXTENSION_TYPE.SUPPORTED_VERSIONS) { const ver = (extData[0] << 8) | extData[1]; if (ver === 0x0304) isTLS13 = true; } else if (extType === EXTENSION_TYPE.KEY_SHARE) { const kr = new BufferReader(extData); serverGroup = kr.u16(); const keyLen = kr.u16(); serverKeyShare = kr.bytes(keyLen); } } } if (isTLS13) { this.tlsVersion = 'tls13'; this.crypto.tlsVersion = 'tls13'; this.crypto.cipher = cipher; this.crypto.cipherInfo = TLS13_CIPHERS[cipher]; if (serverKeyShare) { await this.crypto.computeSharedSecret(serverKeyShare, serverGroup); } } else { this.tlsVersion = 'tls12'; this.crypto.tlsVersion = 'tls12'; this.crypto.cipher = cipher; this.crypto.cipherInfo = TLS12_CIPHERS[cipher]; } } async readEncryptedHandshake13() { let done = false; while (!done) { const rec = this.parseRecord(await this.readData()); if (rec.contentType === CONTENT_TYPE.CHANGE_CIPHER_SPEC) continue; if (rec.contentType === CONTENT_TYPE.ALERT) { const level = rec.fragment[0], desc = rec.fragment[1]; throw new Error(`TLS Alert: level=${level}, description=${desc} (${ALERT_DESCRIPTIONS[desc] || 'unknown'})`); } if (rec.contentType === CONTENT_TYPE.APPLICATION_DATA) { const { payload, contentType } = await this.crypto.decryptRecord13(rec.fragment, true); if (contentType === CONTENT_TYPE.HANDSHAKE) { const r = new BufferReader(payload); while (r.left > 0) { const type = r.u8(); const len = r.u24(); const data = r.bytes(len); const fullMsg = new BufferBuilder(); fullMsg.u8(type).u24(len).bytes(data); if (type === HANDSHAKE_TYPE.FINISHED) { const expected = await this.crypto.computeFinishedVerifyData(false, true); if (!arrEq(data, expected)) throw new Error('Server Finished mismatch'); this.crypto.addTranscript(fullMsg.build()); done = true; } else { this.crypto.addTranscript(fullMsg.build()); } } } } } } async sendClientFinished13() { const verifyData = await this.crypto.computeFinishedVerifyData(true, true); const finished = new BufferBuilder(); finished.u8(HANDSHAKE_TYPE.FINISHED).u24(verifyData.length).bytes(verifyData); const msg = finished.build(); this.crypto.addTranscript(msg); const ct = await this.crypto.encryptHandshake13(msg); await this.writer.write(this.buildRecord(CONTENT_TYPE.APPLICATION_DATA, ct)); this.encrypted = true; } async finishHandshake12() { let serverPubKey = null; let serverGroup = null; let helloDone = false; while (!helloDone) { const rec = this.parseRecord(await this.readData()); if (rec.contentType === CONTENT_TYPE.ALERT) { const level = rec.fragment[0], desc = rec.fragment[1]; throw new Error(`TLS Alert: level=${level}, description=${desc} (${ALERT_DESCRIPTIONS[desc] || 'unknown'})`); } if (rec.contentType === CONTENT_TYPE.HANDSHAKE) { const r = new BufferReader(rec.fragment); while (r.left > 0) { const type = r.u8(); const len = r.u24(); const data = r.bytes(len); const fullMsg = new BufferBuilder(); fullMsg.u8(type).u24(len).bytes(data); this.crypto.addTranscript(fullMsg.build()); if (type === HANDSHAKE_TYPE.SERVER_KEY_EXCHANGE) { const kr = new BufferReader(data); const curveType = kr.u8(); if (curveType === 3) { serverGroup = kr.u16(); const keyLen = kr.u8(); serverPubKey = kr.bytes(keyLen); } } else if (type === HANDSHAKE_TYPE.SERVER_HELLO_DONE) { helloDone = true; } } } } if (serverPubKey) { await this.crypto.computeSharedSecret(serverPubKey, serverGroup); } await this.crypto.deriveKeys12(); const cke = new BufferBuilder(); cke.u8(HANDSHAKE_TYPE.CLIENT_KEY_EXCHANGE); cke.u24(1 + this.crypto.publicKey.length); cke.u8(this.crypto.publicKey.length); cke.bytes(this.crypto.publicKey); const ckeMsg = cke.build(); this.crypto.addTranscript(ckeMsg); await this.writer.write(this.buildRecord(CONTENT_TYPE.HANDSHAKE, ckeMsg)); await this.writer.write(new Uint8Array([CONTENT_TYPE.CHANGE_CIPHER_SPEC, 0x03, 0x03, 0x00, 0x01, 0x01])); this.encrypted = true; const verifyData = await this.crypto.computeFinishedVerifyData(true, false); const finished = new BufferBuilder(); finished.u8(HANDSHAKE_TYPE.FINISHED).u24(verifyData.length).bytes(verifyData); const finMsg = finished.build(); this.crypto.addTranscript(finMsg); const encFinished = await this.crypto.encrypt12(finMsg, CONTENT_TYPE.HANDSHAKE); await this.writer.write(this.buildRecord(CONTENT_TYPE.HANDSHAKE, encFinished)); let serverFinished = false; while (!serverFinished) { const rec = this.parseRecord(await this.readData()); if (rec.contentType === CONTENT_TYPE.CHANGE_CIPHER_SPEC) continue; if (rec.contentType === CONTENT_TYPE.ALERT) { const level = rec.fragment[0], desc = rec.fragment[1]; throw new Error(`TLS Alert: level=${level}, description=${desc} (${ALERT_DESCRIPTIONS[desc] || 'unknown'})`); } if (rec.contentType === CONTENT_TYPE.HANDSHAKE) { const pt = await this.crypto.decrypt12(rec.fragment, CONTENT_TYPE.HANDSHAKE); const r = new BufferReader(pt); const type = r.u8(); const len = r.u24(); const data = r.bytes(len); if (type === HANDSHAKE_TYPE.FINISHED) { const fullMsg = new BufferBuilder(); fullMsg.u8(type).u24(len).bytes(data); this.crypto.addTranscript(fullMsg.build()); const expected = await this.crypto.computeFinishedVerifyData(false, false); if (!arrEq(data, expected)) throw new Error('Server Finished mismatch'); serverFinished = true; } } } } async write(data) { const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data; let ct; if (this.tlsVersion === 'tls13') { ct = await this.crypto.encryptAppData13(bytes); await this.writer.write(this.buildRecord(CONTENT_TYPE.APPLICATION_DATA, ct)); } else { ct = await this.crypto.encrypt12(bytes, CONTENT_TYPE.APPLICATION_DATA); await this.writer.write(this.buildRecord(CONTENT_TYPE.APPLICATION_DATA, ct)); } } async read(timeout = 5000) { const result = []; const start = Date.now(); while (Date.now() - start < timeout) { try { const readPromise = this.readData(); const timeoutPromise = new Promise(res => setTimeout(() => res(null), 1000)); const recData = await Promise.race([readPromise, timeoutPromise]); if (!recData) { if (result.length > 0) break; continue; } const rec = this.parseRecord(recData); if (rec.contentType === CONTENT_TYPE.APPLICATION_DATA) { let payload; if (this.tlsVersion === 'tls13') { const dec = await this.crypto.decryptRecord13(rec.fragment, false); if (dec.contentType === CONTENT_TYPE.APPLICATION_DATA) payload = dec.payload; else if (dec.contentType === CONTENT_TYPE.ALERT && dec.payload[1] === 0) break; else continue; } else { payload = await this.crypto.decrypt12(rec.fragment, CONTENT_TYPE.APPLICATION_DATA); } if (payload) result.push(payload); const combined = concat(...result); const text = new TextDecoder().decode(combined); if (text.includes('\r\n\r\n') && this.isHTTPComplete(text)) return combined; } else if (rec.contentType === CONTENT_TYPE.ALERT) { if (rec.fragment[1] === 0) break; } } catch (e) { if (result.length > 0) break; throw e; } } return concat(...result); } isHTTPComplete(text) { const hdrEnd = text.indexOf('\r\n\r\n'); if (hdrEnd === -1) return false; const hdrs = text.substring(0, hdrEnd).toLowerCase(); const body = text.substring(hdrEnd + 4); const clMatch = hdrs.match(/content-length:\s*(\d+)/); if (clMatch) return body.length >= parseInt(clMatch[1]); if (hdrs.includes('transfer-encoding: chunked')) return body.includes('0\r\n\r\n'); return body.length > 0; } async close() { try { await this.writer?.close(); } catch {} try { await this.socket?.close(); } catch {} this.connected = false; this.handshakeComplete = false; } } async function tlsRequest(options) { const { host, port = 443, servername, method = 'GET', path = '/', headers = {}, timeout = 15000 } = options; const socket = new TLSSocket(); try { await socket.connect({ host, port, servername: servername || host, timeout }); const hdrs = { 'Host': servername || host, 'User-Agent': 'Mozilla/5.0', 'Accept': '*/*', 'Connection': 'close', ...headers }; let req = `${method} ${path} HTTP/1.1\r\n`; for (const [k, v] of Object.entries(hdrs)) req += `${k}: ${v}\r\n`; req += '\r\n'; await socket.write(req); const res = await socket.read(timeout); const text = new TextDecoder().decode(res); const hdrEnd = text.indexOf('\r\n\r\n'); if (hdrEnd === -1) return { statusCode: 0, headers: {}, body: text }; const hdrPart = text.substring(0, hdrEnd); let body = text.substring(hdrEnd + 4); const statusMatch = hdrPart.split('\r\n')[0].match(/HTTP\/[\d.]+\s+(\d+)/); const statusCode = statusMatch ? parseInt(statusMatch[1]) : 0; const resHeaders = {}; for (const line of hdrPart.split('\r\n').slice(1)) { const idx = line.indexOf(':'); if (idx > 0) resHeaders[line.substring(0, idx).trim().toLowerCase()] = line.substring(idx + 1).trim(); } if (resHeaders['transfer-encoding'] === 'chunked') body = decodeChunked(body); return { statusCode, headers: resHeaders, body }; } finally { await socket.close(); } } function decodeChunked(data) { const result = []; let rem = data; while (rem.length > 0) { const le = rem.indexOf('\r\n'); if (le === -1) break; const size = parseInt(rem.substring(0, le).split(';')[0], 16); if (isNaN(size) || size === 0) break; const start = le + 2; if (start + size > rem.length) break; result.push(rem.substring(start, start + size)); rem = rem.substring(start + size + 2); } return result.join(''); } async function checkProxy(proxyString) { const [proxy, portStr = '443'] = proxyString.trim().split(':'); const port = parseInt(portStr); if (!proxy) { return { proxy: proxyString, error: 'Invalid format', proxyip: false }; } const startTime = Date.now(); try { const res = await tlsRequest({ host: proxy, port, servername: 'speed.cloudflare.com', method: 'GET', path: '/meta', headers: { 'Host': 'speed.cloudflare.com', 'Accept': 'application/json', 'Referer': 'https://speed.cloudflare.com/' }, timeout: 15000 }); const latency = Date.now() - startTime; if (res.statusCode !== 200) { return { proxy, port, error: `HTTP ${res.statusCode}`, latency, proxyip: false }; } try { const info = JSON.parse(res.body); if (info.clientIp) { const { clientIp, hostname, httpProtocol, ...other } = info; return { proxy, port, proxyip: true, ip: clientIp, latency: latency + "ms", ...other }; } return { proxy, port, proxyip: false }; } catch { return { proxy, port, error: `Invalid JSON: ${res.body.substring(0, 100)}`, proxyip: false }; } } catch (e) { const latency = Date.now() - startTime; return { proxy, port, error: e.message, proxyip: false }; } } function jsonResponse(data, headers = {}) { return new Response(JSON.stringify(data, null, 2), { headers: { 'Content-Type': 'application/json', ...headers } }); } export default { async fetch(request) { const url = new URL(request.url); const cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' }; if (request.method === 'OPTIONS') return new Response(null, { headers: cors }); const parts = url.pathname.split('/').filter(Boolean); if (parts.length === 0) { return jsonResponse({ usage: '/:proxy1:port1,proxy2:port2,...', example: '/103.160.63.232:443' }, cors); } const proxies = parts[0].split(',').slice(0, 10); const results = await Promise.all(proxies.map(checkProxy)); return jsonResponse(results.length === 1 ? results[0] : results, cors); } };